Skip to content

Commit 9fd48d7

Browse files
author
User
committed
fix(sdk): use build_replacements unresolved set instead of post-scan for curly templates
The curly template formatter raised a false TemplateFormatError when a substituted value happened to contain {{...}} patterns (e.g. template examples or code snippets in user input). Root cause: after substitution, compute_truly_unreplaced() re-scanned the rendered output for {{...}} patterns and intersected with the original placeholder set. It could not distinguish between genuinely unreplaced placeholders and {{...}} patterns injected by substituted values. Fix: use the 'unresolved' set already returned by build_replacements() to detect truly missing variables. This set is computed before substitution, so it is immune to patterns in substituted values. Changes: - types.py: use unresolved from build_replacements instead of compute_truly_unreplaced - handlers.py: same fix - Remove now-unused compute_truly_unreplaced from both files - Add unit tests covering the false-positive scenario Fixes #3770
1 parent a722743 commit 9fd48d7

File tree

3 files changed

+64
-25
lines changed

3 files changed

+64
-25
lines changed

sdk/agenta/sdk/types.py

Lines changed: 4 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -646,12 +646,6 @@ def _repl(m: re.Match) -> str:
646646
return _PLACEHOLDER_RE.sub(_repl, template)
647647

648648

649-
def compute_truly_unreplaced(original: set, rendered: str) -> set:
650-
"""Only count placeholders that were in the original template and remain."""
651-
now = set(extract_placeholders(rendered))
652-
return original & now
653-
654-
655649
def missing_lib_hints(unreplaced: set) -> Optional[str]:
656650
"""Suggest installing python-jsonpath if placeholders indicate json-path or json-pointer usage."""
657651
if any(expr.startswith("$") or expr.startswith("/") for expr in unreplaced):
@@ -722,20 +716,17 @@ def _format_with_template(self, content: str, kwargs: Dict[str, Any]) -> str:
722716
elif self.template_format == "curly":
723717
original_placeholders = set(extract_placeholders(content))
724718

725-
replacements, _unresolved = build_replacements(
719+
replacements, unresolved = build_replacements(
726720
original_placeholders, kwargs
727721
)
728722

729723
result = apply_replacements(content, replacements)
730724

731-
truly_unreplaced = compute_truly_unreplaced(
732-
original_placeholders, result
733-
)
734-
if truly_unreplaced:
735-
hint = missing_lib_hints(truly_unreplaced)
725+
if unresolved:
726+
hint = missing_lib_hints(unresolved)
736727
suffix = f" Hint: {hint}" if hint else ""
737728
raise TemplateFormatError(
738-
f"Unreplaced variables in curly template: {sorted(truly_unreplaced)}.{suffix}"
729+
f"Unreplaced variables in curly template: {sorted(unresolved)}.{suffix}"
739730
)
740731

741732
return result

sdk/agenta/sdk/workflows/handlers.py

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -253,12 +253,6 @@ def _repl(m: re.Match) -> str:
253253
return _PLACEHOLDER_RE.sub(_repl, template)
254254

255255

256-
def compute_truly_unreplaced(original: set, rendered: str) -> set:
257-
"""Only count placeholders that were in the original template and remain."""
258-
now = set(extract_placeholders(rendered))
259-
return original & now
260-
261-
262256
def missing_lib_hints(unreplaced: set) -> Optional[str]:
263257
"""Suggest installing python-jsonpath if placeholders indicate json-path or json-pointer usage."""
264258
if any(expr.startswith("$") or expr.startswith("/") for expr in unreplaced):
@@ -288,18 +282,16 @@ def _format_with_template(
288282
elif format == "curly":
289283
original_placeholders = set(extract_placeholders(content))
290284

291-
replacements, _unresolved = build_replacements(original_placeholders, kwargs)
285+
replacements, unresolved = build_replacements(original_placeholders, kwargs)
292286

293287
result = apply_replacements(content, replacements)
294288

295-
truly_unreplaced = compute_truly_unreplaced(original_placeholders, result)
296-
297-
if truly_unreplaced:
298-
hint = missing_lib_hints(truly_unreplaced)
289+
if unresolved:
290+
hint = missing_lib_hints(unresolved)
299291
suffix = f" Hint: {hint}" if hint else ""
300292
raise ValueError(
301293
f"Template variables not found or unresolved: "
302-
f"{', '.join(sorted(truly_unreplaced))}.{suffix}"
294+
f"{', '.join(sorted(unresolved))}.{suffix}"
303295
)
304296

305297
return result
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
"""Tests for curly template formatting — specifically the false-positive
2+
unreplaced-variable detection reported in #3770."""
3+
4+
import pytest
5+
6+
from agenta.sdk.types import PromptTemplate
7+
8+
9+
class TestCurlyTemplateSubstitution:
10+
"""Verify that substituted values containing {{...}} patterns do not
11+
trigger a false TemplateFormatError."""
12+
13+
def test_value_containing_placeholder_pattern(self):
14+
"""Substituted value that looks like a placeholder should not raise."""
15+
prompt = PromptTemplate(
16+
user_prompt="Hello {{name}}",
17+
template_format="curly",
18+
)
19+
result = prompt.format(name="I am {{name}}")
20+
assert result.messages[-1].content == "Hello I am {{name}}"
21+
22+
def test_value_containing_different_placeholder(self):
23+
"""Substituted value with a different placeholder name should not raise."""
24+
prompt = PromptTemplate(
25+
user_prompt="Result: {{output}}",
26+
template_format="curly",
27+
)
28+
result = prompt.format(output="Use {{variable}} in your template")
29+
assert result.messages[-1].content == "Result: Use {{variable}} in your template"
30+
31+
def test_multiple_vars_one_with_placeholder_value(self):
32+
"""Multiple variables where one value contains placeholder syntax."""
33+
prompt = PromptTemplate(
34+
user_prompt="{{greeting}}, {{name}}!",
35+
template_format="curly",
36+
)
37+
result = prompt.format(greeting="Hi", name="{{greeting}}")
38+
assert result.messages[-1].content == "Hi, {{greeting}}!"
39+
40+
def test_genuinely_missing_variable_still_raises(self):
41+
"""A truly missing variable should still raise an error."""
42+
prompt = PromptTemplate(
43+
user_prompt="Hello {{name}}, your id is {{id}}",
44+
template_format="curly",
45+
)
46+
with pytest.raises(Exception):
47+
prompt.format(name="Alice")
48+
49+
def test_all_variables_provided(self):
50+
"""Normal case — all variables provided, no placeholder patterns in values."""
51+
prompt = PromptTemplate(
52+
user_prompt="Hello {{name}}",
53+
template_format="curly",
54+
)
55+
result = prompt.format(name="World")
56+
assert result.messages[-1].content == "Hello World"

0 commit comments

Comments
 (0)