Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/graphon/nodes/base/template.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ def from_answer_template(cls, template_str: str) -> Template:
# We need to split the template by variable placeholders while preserving order
# Create a regex pattern that matches variable placeholders
pattern = (
r"\{\{(#[a-zA-Z0-9_]{1,50}(?:\.[a-zA-Z_][a-zA-Z0-9_]{0,29}){1,10}#)\}\}"
r"\{\{(#[a-zA-Z0-9_-]{1,50}(?:\.[a-zA-Z_][a-zA-Z0-9_]{0,29}){1,10}#)\}\}"
)

# Split template while keeping the delimiters (variable placeholders)
Expand Down
4 changes: 2 additions & 2 deletions src/graphon/nodes/base/variable_template_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,11 @@
from .entities import VariableSelector

REGEX = re.compile(
r"\{\{(#[a-zA-Z0-9_]{1,50}(\.[a-zA-Z_][a-zA-Z0-9_]{0,29}){1,10}#)\}\}",
r"\{\{(#[a-zA-Z0-9_-]{1,50}(\.[a-zA-Z_][a-zA-Z0-9_]{0,29}){1,10}#)\}\}",
)

SELECTOR_PATTERN = re.compile(
r"\{\{(#[a-zA-Z0-9_]{1,50}(?:\.[a-zA-Z_][a-zA-Z0-9_]{0,29}){1,10}#)\}\}",
r"\{\{(#[a-zA-Z0-9_-]{1,50}(?:\.[a-zA-Z_][a-zA-Z0-9_]{0,29}){1,10}#)\}\}",
)


Expand Down
2 changes: 1 addition & 1 deletion src/graphon/variables/template_resolution.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from graphon.variables.segments import Segment

VARIABLE_PATTERN = re.compile(
r"\{\{#([a-zA-Z0-9_]{1,50}(?:\.[a-zA-Z_][a-zA-Z0-9_]{0,29}){1,10})#\}\}",
r"\{\{#([a-zA-Z0-9_-]{1,50}(?:\.[a-zA-Z_][a-zA-Z0-9_]{0,29}){1,10})#\}\}",
)


Expand Down
13 changes: 13 additions & 0 deletions tests/nodes/base/test_template.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,19 @@


class TestTemplate:
def test_from_answer_template_preserves_hyphenated_selectors(self) -> None:
template = Template.from_answer_template(
"Words: {{#input-check.result.word#}} / {{#input_check.word#}}.",
)

assert template.segments == [
TextSegment("Words: "),
VariableSegment(["input-check", "result", "word"]),
TextSegment(" / "),
VariableSegment(["input_check", "word"]),
TextSegment("."),
]

def test_from_answer_template_simple(self) -> None:
template_str = "Hello, {{#node1.name#}}!"
template = Template.from_answer_template(template_str)
Expand Down
22 changes: 22 additions & 0 deletions tests/nodes/base/test_variable_template_parser.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,31 @@
import dataclasses

import pytest

from graphon.nodes.base import variable_template_parser
from graphon.nodes.base.entities import VariableSelector


@pytest.mark.parametrize(
"node_id",
["input-check", "550e8400-e29b-41d4-a716-446655440000", "-input-"],
)
def test_extracts_and_formats_hyphenated_node_references(node_id: str) -> None:
variable = "#" + node_id + ".result.word#"
template = "Word: {{" + variable + "}}"
expected = [
VariableSelector(variable=variable, value_selector=[node_id, "result", "word"]),
]

parser = variable_template_parser.VariableTemplateParser(template)

assert (
variable_template_parser.extract_selectors_from_template(template) == expected
)
assert parser.extract_variable_selectors() == expected
assert parser.format({variable: "red"}) == "Word: red"


def test_extract_selectors_from_template() -> None:
template = (
"Hello, {{#sys.user_id#}}! Your query is {{#node_id.custom_query#}}. "
Expand Down
29 changes: 29 additions & 0 deletions tests/nodes/llm/test_node.py
Original file line number Diff line number Diff line change
Expand Up @@ -478,6 +478,35 @@ def test_fetch_inputs_normalizes_none_to_empty_string() -> None:
assert node._fetch_inputs(node.node_data) == {"#source.value#": ""}


@pytest.mark.parametrize("word", ["red", "blue"])
def test_run_sends_hyphenated_node_output_to_model(word: str) -> None:
model = MagicMock(
provider="openai",
model_name="gpt-4o",
parameters={},
stop=(),
)
model.get_model_schema.return_value.supports_prompt_content_type.return_value = True
model.invoke_llm.return_value = _stream_results(
_stream_chunk("ok", usage=LLMUsage.empty_usage(), finish_reason="stop"),
)
node = _build_llm_node(
model_instance=model,
prompt_text="Please process: {{#input-check.validated_input#}}",
variables=[(("input-check", "validated_input"), word)],
)

completed = next(
event for event in node._run() if isinstance(event, StreamCompletedEvent)
)

assert completed.node_run_result.status == WorkflowNodeExecutionStatus.SUCCEEDED
model.invoke_llm.assert_called_once()
messages = model.invoke_llm.call_args.kwargs["prompt_messages"]
assert messages[0].content == f"Please process: {word}"
assert completed.node_run_result.inputs["#input-check.validated_input#"] == word


def test_run_does_not_reuse_file_outputs_after_failure(
monkeypatch: pytest.MonkeyPatch,
) -> None:
Expand Down
27 changes: 27 additions & 0 deletions tests/variables/test_template_resolution.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,37 @@
import pytest

from graphon.runtime.read_only_wrappers import ReadOnlyVariablePoolWrapper
from graphon.runtime.variable_pool import VariablePool
from graphon.variables.segments import ArrayObjectSegment
from graphon.variables.template_resolution import convert_template


class TestConvertTemplate:
@pytest.mark.parametrize(
"node_id",
["input-check", "550e8400-e29b-41d4-a716-446655440000", "-input-"],
)
def test_resolves_hyphenated_node_ids(self, node_id: str) -> None:
pool = VariablePool.empty()
pool.add((node_id, "result"), {"word": "red"})
pool.add(("input_check", "result"), {"word": "blue"})
template = "{{#" + node_id + ".result.word#}} / {{#input_check.result.word#}}"

rendered = convert_template(ReadOnlyVariablePoolWrapper(pool), template)

assert rendered.text == "red / blue"

@pytest.mark.parametrize(
"selector",
["input check.word", "input-check.bad-name", "a" * 51 + ".word"],
)
def test_preserves_unsupported_references(self, selector: str) -> None:
pool = VariablePool.empty()
pool.add(selector.split("."), "must not be substituted")
template = "{{#" + selector + "#}}"

assert convert_template(pool, template).text == template

def test_resolves_variables_from_read_only_pool(self) -> None:
pool = VariablePool.empty()
pool.add(("start", "name"), "Joe")
Expand Down
Loading