Skip to content

Commit 3399ab9

Browse files
committed
fix: restore double-brace escaping in instruction templates
Double braces (e.g. {{city}}) should produce literal {city} in the rendered instruction, not trigger a session state lookup. This escaping worked in v1.25.0 but regressed in v1.25.1 because the regex r'{+[^{}]*}+' matches {{city}} and _replace_match strips all braces before looking up the variable name. The fix adds an early return in _replace_match: when the matched text starts with {{ and ends with }}, peel one layer of braces and return the literal string without any state/artifact lookup. Fixes #4606
1 parent 8f54281 commit 3399ab9

2 files changed

Lines changed: 47 additions & 1 deletion

File tree

src/google/adk/utils/instructions_utils.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,11 @@ async def _async_sub(pattern, repl_async_fn, string) -> str:
7979
return ''.join(result)
8080

8181
async def _replace_match(match) -> str:
82-
var_name = match.group().lstrip('{').rstrip('}').strip()
82+
raw = match.group()
83+
# Double (or more) braces are escape sequences: {{x}} → {x}
84+
if raw.startswith('{{') and raw.endswith('}}'):
85+
return raw[1:-1]
86+
var_name = raw.lstrip('{').rstrip('}').strip()
8387
optional = False
8488
if var_name.endswith('?'):
8589
optional = True

tests/unittests/utils/test_instructions_utils.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,3 +267,45 @@ async def test_inject_session_state_with_optional_missing_state_returns_empty():
267267
instruction_template, invocation_context
268268
)
269269
assert populated_instruction == "Optional value: "
270+
271+
272+
@pytest.mark.asyncio
273+
async def test_double_braces_escape_to_literal():
274+
"""Double braces {{x}} should produce literal {x}, not a state lookup."""
275+
instruction_template = 'Generate a keyword like "roofing cost in {{city}}".'
276+
invocation_context = await _create_test_readonly_context()
277+
278+
populated_instruction = await instructions_utils.inject_session_state(
279+
instruction_template, invocation_context
280+
)
281+
assert populated_instruction == (
282+
'Generate a keyword like "roofing cost in {city}".'
283+
)
284+
285+
286+
@pytest.mark.asyncio
287+
async def test_double_braces_mixed_with_state_variable():
288+
"""Double braces should escape while single braces still resolve state."""
289+
instruction_template = "Hello {user_name}, use {{placeholder}} in prompts."
290+
invocation_context = await _create_test_readonly_context(
291+
state={"user_name": "Alice"}
292+
)
293+
294+
populated_instruction = await instructions_utils.inject_session_state(
295+
instruction_template, invocation_context
296+
)
297+
assert populated_instruction == (
298+
"Hello Alice, use {placeholder} in prompts."
299+
)
300+
301+
302+
@pytest.mark.asyncio
303+
async def test_triple_braces_peel_one_layer():
304+
"""Triple braces {{{x}}} should peel one layer to {{x}}."""
305+
instruction_template = "Escaped: {{{example}}}"
306+
invocation_context = await _create_test_readonly_context()
307+
308+
populated_instruction = await instructions_utils.inject_session_state(
309+
instruction_template, invocation_context
310+
)
311+
assert populated_instruction == "Escaped: {{example}}"

0 commit comments

Comments
 (0)