1616
1717import logging
1818import re
19- from typing import Any
2019
2120from ..agents .readonly_context import ReadonlyContext
2221from ..sessions .state import State
@@ -47,11 +46,7 @@ async def build_instruction(
4746 ) -> str:
4847 return await inject_session_state(
4948 'You can inject a state variable like {var_name} or an artifact '
50- '{artifact.file_name} into the instruction template.'
51- 'You can also inject a nested variable like {var_name.nested_var}.'
52- 'If a variable or nested attribute may be missing, append `?` to the '
53- 'path or attribute name for optional handling, e.g. '
54- '{var_name.optional_nested_var?}.',
49+ '{artifact.file_name} into the instruction template.',
5550 readonly_context,
5651 )
5752
@@ -83,69 +78,14 @@ async def _async_sub(pattern, repl_async_fn, string) -> str:
8378 result .append (string [last_end :])
8479 return '' .join (result )
8580
86- def _get_nested_value (obj : Any , path : str ) -> Any :
87- """Retrieve nested value from an object based on dot-separated path."""
88- parts = path .split ('.' )
89- current = obj
90-
91- for part in parts :
92- if current is None :
93- return None
94-
95- optional = part .endswith ('?' )
96- key = part .removesuffix ('?' )
97-
98- # Try dictionary access first
99- if hasattr (current , '__getitem__' ):
100- try :
101- current = current [key ]
102- continue
103- except (KeyError , TypeError ) as e :
104- # If dict access fails, fall through to try getattr
105- # UNLESS it's a pure dict which definitely doesn't have attributes
106- if isinstance (current , dict ):
107- if optional :
108- return None
109- raise KeyError (f"Key '{ key } ' not found in path '{ path } '" ) from e
110- pass
111-
112- # Try attribute access
113- try :
114- current = getattr (current , key )
115- except AttributeError as e :
116- # Both dict access and attribute access failed.
117- if optional :
118- return None
119- raise KeyError (f"Key '{ key } ' not found in path '{ path } '" ) from e
120-
121- return current
122-
123- def _is_valid_path (path : str ) -> bool :
124- """Checks if the path is a valid state variable path."""
125- parts = path .split ('.' )
126- if not parts :
127- return False
128-
129- # Check first segment (can have prefix)
130- first_seg = parts [0 ].removesuffix ('?' )
131- if not _is_valid_state_name (first_seg ):
132- return False
133-
134- # Check subsequent segments (must be plain identifiers)
135- for part in parts [1 :]:
136- seg = part .removesuffix ('?' )
137- if not seg .isidentifier ():
138- return False
139-
140- return True
141-
142- async def _evaluate_path (path : str ) -> str :
143- if path .startswith ('artifact.' ):
144- var_name = path .removeprefix ('artifact.' )
145- optional = var_name .endswith ('?' )
146- if optional :
147- var_name = var_name .removesuffix ('?' )
148-
81+ async def _replace_match (match ) -> str :
82+ var_name = match .group ().lstrip ('{' ).rstrip ('}' ).strip ()
83+ optional = False
84+ if var_name .endswith ('?' ):
85+ optional = True
86+ var_name = var_name .removesuffix ('?' )
87+ if var_name .startswith ('artifact.' ):
88+ var_name = var_name .removeprefix ('artifact.' )
14989 if invocation_context .artifact_service is None :
15090 raise ValueError ('Artifact service is not initialized.' )
15191 artifact = await invocation_context .artifact_service .load_artifact (
@@ -164,45 +104,22 @@ async def _evaluate_path(path: str) -> str:
164104 raise KeyError (f'Artifact { var_name } not found.' )
165105 return str (artifact )
166106 else :
167- try :
168- value = _get_nested_value (invocation_context .session .state , path )
169-
107+ if not _is_valid_state_name (var_name ):
108+ return match .group ()
109+ if var_name in invocation_context .session .state :
110+ value = invocation_context .session .state [var_name ]
170111 if value is None :
171112 return ''
172113 return str (value )
173- except KeyError as e :
174- raise KeyError (f'Context variable not found: `{ path } `.' ) from e
175-
176- async def _replace_match (match ) -> str :
177- raw_match = match .group ()
178- leading_count = len (raw_match ) - len (raw_match .lstrip ('{' ))
179- trailing_count = len (raw_match ) - len (raw_match .rstrip ('}' ))
180- full_path = raw_match .lstrip ('{' ).rstrip ('}' ).strip ()
181-
182- if leading_count == trailing_count :
183- n = leading_count
184- is_valid = _is_valid_path (full_path ) or full_path .startswith ('artifact.' )
185- if n % 2 == 0 :
186- # Even: Escaped, no evaluation
187- if is_valid :
188- half_n = n // 2
189- return '{' * half_n + full_path + '}' * half_n
190- else :
191- return raw_match
192114 else :
193- # Odd: Evaluate and wrap
194- if not is_valid :
195- return raw_match
196- evaluated_value = await _evaluate_path (full_path )
197- wrap_braces = (n - 1 ) // 2
198- return '{' * wrap_braces + evaluated_value + '}' * wrap_braces
199- else :
200- # Asymmetric: fallback to old behavior (treat as N=1 if valid path)
201- if not _is_valid_path (full_path ) and not full_path .startswith (
202- 'artifact.'
203- ):
204- return raw_match
205- return await _evaluate_path (full_path )
115+ if optional :
116+ logger .debug (
117+ 'Context variable %s not found, replacing with empty string' ,
118+ var_name ,
119+ )
120+ return ''
121+ else :
122+ raise KeyError (f'Context variable not found: `{ var_name } `.' )
206123
207124 return await _async_sub (r'{+[^{}]*}+' , _replace_match , template )
208125
0 commit comments