Skip to content

Commit e0050b9

Browse files
authored
Merge branch 'main' into adk-runner-helper
2 parents febb47b + 546c2a6 commit e0050b9

File tree

15 files changed

+353
-106
lines changed

15 files changed

+353
-106
lines changed
File renamed without changes.

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ dependencies = [
3838
"google-cloud-secret-manager>=2.22.0, <3.0.0", # Fetching secrets in RestAPI Tool
3939
"google-cloud-spanner>=3.56.0, <4.0.0", # For Spanner database
4040
"google-cloud-speech>=2.30.0, <3.0.0", # For Audio Transcription
41-
"google-cloud-storage>=2.18.0, <3.0.0", # For GCS Artifact service
41+
"google-cloud-storage>=3.0.0, <4.0.0", # For GCS Artifact service
4242
"google-genai>=1.45.0, <2.0.0", # Google GenAI SDK
4343
"graphviz>=0.20.2, <1.0.0", # Graphviz for graph rendering
4444
"mcp>=1.8.0, <2.0.0;python_version>='3.10'", # For MCP Toolset

src/google/adk/agents/llm_agent.py

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -689,9 +689,43 @@ def __get_agent_to_run(self, agent_name: str) -> BaseAgent:
689689
"""Find the agent to run under the root agent by name."""
690690
agent_to_run = self.root_agent.find_agent(agent_name)
691691
if not agent_to_run:
692-
raise ValueError(f'Agent {agent_name} not found in the agent tree.')
692+
available = self._get_available_agent_names()
693+
error_msg = (
694+
f"Agent '{agent_name}' not found.\n"
695+
f"Available agents: {', '.join(available)}\n\n"
696+
'Possible causes:\n'
697+
' 1. Agent not registered before being referenced\n'
698+
' 2. Agent name mismatch (typo or case sensitivity)\n'
699+
' 3. Timing issue (agent referenced before creation)\n\n'
700+
'Suggested fixes:\n'
701+
' - Verify agent is registered with root agent\n'
702+
' - Check agent name spelling and case\n'
703+
' - Ensure agents are created before being referenced'
704+
)
705+
raise ValueError(error_msg)
693706
return agent_to_run
694707

708+
def _get_available_agent_names(self) -> list[str]:
709+
"""Helper to get all agent names in the tree for error reporting.
710+
711+
This is a private helper method used only for error message formatting.
712+
Traverses the agent tree starting from root_agent and collects all
713+
agent names for display in error messages.
714+
715+
Returns:
716+
List of all agent names in the agent tree.
717+
"""
718+
agents = []
719+
720+
def collect_agents(agent):
721+
agents.append(agent.name)
722+
if hasattr(agent, 'sub_agents') and agent.sub_agents:
723+
for sub_agent in agent.sub_agents:
724+
collect_agents(sub_agent)
725+
726+
collect_agents(self.root_agent)
727+
return agents
728+
695729
def __get_transfer_to_agent_or_none(
696730
self, event: Event, from_agent: str
697731
) -> Optional[BaseAgent]:

src/google/adk/agents/remote_a2a_agent.py

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -356,29 +356,33 @@ def _construct_message_parts_from_session(
356356
"""
357357
message_parts: list[A2APart] = []
358358
context_id = None
359+
360+
events_to_process = []
359361
for event in reversed(ctx.session.events):
360-
if _is_other_agent_reply(self.name, event):
361-
event = _present_other_agent_message(event)
362-
elif event.author == self.name:
362+
if event.author == self.name:
363363
# stop on content generated by current a2a agent given it should already
364364
# be in remote session
365365
if event.custom_metadata:
366366
metadata = event.custom_metadata
367367
context_id = metadata.get(A2A_METADATA_PREFIX + "context_id")
368368
break
369+
events_to_process.append(event)
370+
371+
for event in reversed(events_to_process):
372+
if _is_other_agent_reply(self.name, event):
373+
event = _present_other_agent_message(event)
369374

370375
if not event.content or not event.content.parts:
371376
continue
372377

373378
for part in event.content.parts:
374-
375379
converted_part = self._genai_part_converter(part)
376380
if converted_part:
377381
message_parts.append(converted_part)
378382
else:
379383
logger.warning("Failed to convert part to A2A format: %s", part)
380384

381-
return message_parts[::-1], context_id
385+
return message_parts, context_id
382386

383387
async def _handle_a2a_response(
384388
self, a2a_response: A2AClientEvent | A2AMessage, ctx: InvocationContext

src/google/adk/cli/cli_eval.py

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -214,14 +214,16 @@ def pretty_print_eval_result(eval_result: EvalCaseResult):
214214
expected_invocation = per_invocation_result.expected_invocation
215215
row_data = {
216216
"prompt": _convert_content_to_text(actual_invocation.user_content),
217-
"expected_response": _convert_content_to_text(
218-
expected_invocation.final_response if expected_invocation else None
217+
"expected_response": (
218+
_convert_content_to_text(expected_invocation.final_response)
219+
if expected_invocation
220+
else None
219221
),
220222
"actual_response": _convert_content_to_text(
221223
actual_invocation.final_response
222224
),
223-
"expected_tool_calls": _convert_tool_calls_to_text(
224-
expected_invocation.intermediate_data
225+
"expected_tool_calls": (
226+
_convert_tool_calls_to_text(expected_invocation.intermediate_data)
225227
if expected_invocation
226228
else None
227229
),
@@ -252,11 +254,23 @@ def pretty_print_eval_result(eval_result: EvalCaseResult):
252254
)
253255
click.echo("Invocation Details:")
254256
df = pd.DataFrame(data)
257+
258+
# Identify columns where ALL values are exactly None
259+
columns_to_keep = []
255260
for col in df.columns:
256-
if df[col].dtype == "object":
257-
df[col] = df[col].str.wrap(40)
258-
click.echo(tabulate(df, headers="keys", tablefmt="grid"))
259-
click.echo("\n\n") # Few empty lines for visual clarity
261+
# Check if all elements in the column are NOT None
262+
if not df[col].apply(lambda x: x is None).all():
263+
columns_to_keep.append(col)
264+
265+
# Select only the columns to keep
266+
df_result = df[columns_to_keep]
267+
268+
for col in df_result.columns:
269+
if df_result[col].dtype == "object":
270+
df_result[col] = df_result[col].str.wrap(40)
271+
272+
click.echo(tabulate(df_result, headers="keys", tablefmt="grid"))
273+
click.echo("\n\n") # Few empty lines for visual clarity
260274

261275

262276
def get_eval_sets_manager(

src/google/adk/evaluation/eval_case.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,10 @@ class Invocation(EvalBaseModel):
108108
"""Details about the App that was used for this invocation."""
109109

110110

111+
SessionState: TypeAlias = dict[str, Any]
112+
"""The state of the session."""
113+
114+
111115
class SessionInput(EvalBaseModel):
112116
"""Values that help initialize a Session."""
113117

@@ -117,7 +121,7 @@ class SessionInput(EvalBaseModel):
117121
user_id: str
118122
"""The user id."""
119123

120-
state: dict[str, Any] = Field(default_factory=dict)
124+
state: SessionState = Field(default_factory=dict)
121125
"""The state of the session."""
122126

123127

@@ -159,6 +163,9 @@ class EvalCase(EvalBaseModel):
159163
)
160164
"""A list of rubrics that are applicable to all the invocations in the conversation of this eval case."""
161165

166+
final_session_state: Optional[SessionState] = Field(default_factory=dict)
167+
"""The expected final session state at the end of the conversation."""
168+
162169
@model_validator(mode="after")
163170
def ensure_conversation_xor_conversation_scenario(self) -> EvalCase:
164171
if (self.conversation is None) == (self.conversation_scenario is None):

src/google/adk/flows/llm_flows/contents.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -594,7 +594,12 @@ def _is_event_belongs_to_branch(
594594
"""
595595
if not invocation_branch or not event.branch:
596596
return True
597-
return invocation_branch.startswith(event.branch)
597+
# We use dot to delimit branch nodes. To avoid simple prefix match
598+
# (e.g. agent_0 unexpectedly matching agent_00), require either perfect branch
599+
# match, or match prefix with an additional explicit '.'
600+
return invocation_branch == event.branch or invocation_branch.startswith(
601+
f'{event.branch}.'
602+
)
598603

599604

600605
def _is_function_call_event(event: Event, function_name: str) -> bool:

src/google/adk/flows/llm_flows/functions.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -716,10 +716,17 @@ def _get_tool(
716716
):
717717
"""Returns the tool corresponding to the function call."""
718718
if function_call.name not in tools_dict:
719-
raise ValueError(
720-
f'Function {function_call.name} is not found in the tools_dict:'
721-
f' {tools_dict.keys()}.'
719+
available = list(tools_dict.keys())
720+
error_msg = (
721+
f"Tool '{function_call.name}' not found.\nAvailable tools:"
722+
f" {', '.join(available)}\n\nPossible causes:\n 1. LLM hallucinated"
723+
' the function name - review agent instruction clarity\n 2. Tool not'
724+
' registered - verify agent.tools list\n 3. Name mismatch - check for'
725+
' typos\n\nSuggested fixes:\n - Review agent instruction to ensure'
726+
' tool usage is clear\n - Verify tool is included in agent.tools'
727+
' list\n - Check for typos in function name'
722728
)
729+
raise ValueError(error_msg)
723730

724731
return tools_dict[function_call.name]
725732

src/google/adk/sessions/vertex_ai_session_service.py

Lines changed: 8 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,6 @@
3535
from ..events.event import Event
3636
from ..events.event_actions import EventActions
3737
from ..utils.vertex_ai_utils import get_express_mode_api_key
38-
from ..utils.vertex_ai_utils import is_vertex_express_mode
3938
from .base_session_service import BaseSessionService
4039
from .base_session_service import GetSessionConfig
4140
from .base_session_service import ListSessionsResponse
@@ -115,53 +114,14 @@ async def create_session(
115114
config = {'session_state': state} if state else {}
116115
config.update(kwargs)
117116

118-
if is_vertex_express_mode(
119-
self._project, self._location, self._express_mode_api_key
120-
):
121-
config['wait_for_completion'] = False
122-
api_response = await api_client.aio.agent_engines.sessions.create(
123-
name=f'reasoningEngines/{reasoning_engine_id}',
124-
user_id=user_id,
125-
config=config,
126-
)
127-
logger.info('Create session response received.')
128-
session_id = api_response.name.split('/')[-3]
129-
130-
# Express mode doesn't support LRO, so we need to poll
131-
# the session resource.
132-
# TODO: remove this once LRO polling is supported in Express mode.
133-
@retry(
134-
stop=stop_after_attempt(6),
135-
wait=wait_exponential(multiplier=1, min=1, max=3),
136-
retry=retry_if_result(lambda response: not response),
137-
reraise=True,
138-
)
139-
async def _poll_session_resource():
140-
try:
141-
return await api_client.aio.agent_engines.sessions.get(
142-
name=f'reasoningEngines/{reasoning_engine_id}/sessions/{session_id}'
143-
)
144-
except ClientError:
145-
logger.info('Polling session resource')
146-
return None
147-
148-
try:
149-
await _poll_session_resource()
150-
except Exception as exc:
151-
raise ValueError('Failed to create session.') from exc
152-
153-
get_session_response = await api_client.aio.agent_engines.sessions.get(
154-
name=f'reasoningEngines/{reasoning_engine_id}/sessions/{session_id}'
155-
)
156-
else:
157-
api_response = await api_client.aio.agent_engines.sessions.create(
158-
name=f'reasoningEngines/{reasoning_engine_id}',
159-
user_id=user_id,
160-
config=config,
161-
)
162-
logger.debug('Create session response: %s', api_response)
163-
get_session_response = api_response.response
164-
session_id = get_session_response.name.split('/')[-1]
117+
api_response = await api_client.aio.agent_engines.sessions.create(
118+
name=f'reasoningEngines/{reasoning_engine_id}',
119+
user_id=user_id,
120+
config=config,
121+
)
122+
logger.debug('Create session response: %s', api_response)
123+
get_session_response = api_response.response
124+
session_id = get_session_response.name.split('/')[-1]
165125

166126
session = Session(
167127
app_name=app_name,

src/google/adk/utils/vertex_ai_utils.py

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -26,18 +26,6 @@
2626
from ..utils.env_utils import is_env_enabled
2727

2828

29-
def is_vertex_express_mode(
30-
project: Optional[str], location: Optional[str], api_key: Optional[str]
31-
) -> bool:
32-
"""Check if Vertex AI and API key are both enabled replacing project and location, meaning the user is using the Vertex Express Mode."""
33-
return (
34-
is_env_enabled('GOOGLE_GENAI_USE_VERTEXAI')
35-
and api_key is not None
36-
and project is None
37-
and location is None
38-
)
39-
40-
4129
def get_express_mode_api_key(
4230
project: Optional[str],
4331
location: Optional[str],

0 commit comments

Comments
 (0)