Skip to content

Commit 59fe9b3

Browse files
DeanChensjcopybara-github
authored andcommitted
fix: Rollback instruction util refactoring as its breaking internal customers
Co-authored-by: Shangjie Chen <deanchen@google.com> PiperOrigin-RevId: 935006608
1 parent 94fcdbb commit 59fe9b3

6 files changed

Lines changed: 21 additions & 553 deletions

File tree

.agents/skills/adk-sample-creator/SKILL.md

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,11 +30,6 @@ The `agent.py` should focus on demonstrating a specific feature or agent pattern
3030
> [!IMPORTANT]
3131
> **Model Selection**: Do not set the `model` parameter explicitly (e.g., `model="gemini-2.5-flash"`) on `Agent` instances in sample agents. Instead, let them default to the system-configured model, unless a specific model is explicitly requested by the user.
3232
33-
> [!IMPORTANT]
34-
> **Context Usage**: Prefer using the unified `Context` class (imported from
35-
> `google.adk`) instead of the legacy `ToolContext` or `CallbackContext`
36-
> aliases in callbacks and tool definitions.
37-
3833
Choose one of the following patterns:
3934

4035
#### Pattern A: Workflows (for complex graphs)

contributing/samples/core/nested_state/README.md

Lines changed: 0 additions & 65 deletions
This file was deleted.

contributing/samples/core/nested_state/__init__.py

Lines changed: 0 additions & 19 deletions
This file was deleted.

contributing/samples/core/nested_state/agent.py

Lines changed: 0 additions & 39 deletions
This file was deleted.

src/google/adk/utils/instructions_utils.py

Lines changed: 21 additions & 104 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616

1717
import logging
1818
import re
19-
from typing import Any
2019

2120
from ..agents.readonly_context import ReadonlyContext
2221
from ..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

Comments
 (0)