google-adk 0.2.0__py3-none-any.whl → 0.4.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. google/adk/agents/base_agent.py +7 -7
  2. google/adk/agents/callback_context.py +0 -1
  3. google/adk/agents/llm_agent.py +3 -8
  4. google/adk/auth/auth_credential.py +2 -1
  5. google/adk/auth/auth_handler.py +7 -3
  6. google/adk/cli/browser/index.html +1 -1
  7. google/adk/cli/browser/{main-ZBO76GRM.js → main-HWIBUY2R.js} +69 -53
  8. google/adk/cli/cli.py +54 -47
  9. google/adk/cli/cli_deploy.py +6 -1
  10. google/adk/cli/cli_eval.py +1 -1
  11. google/adk/cli/cli_tools_click.py +78 -13
  12. google/adk/cli/fast_api.py +6 -0
  13. google/adk/evaluation/agent_evaluator.py +2 -2
  14. google/adk/evaluation/response_evaluator.py +2 -2
  15. google/adk/evaluation/trajectory_evaluator.py +4 -5
  16. google/adk/events/event_actions.py +9 -4
  17. google/adk/flows/llm_flows/agent_transfer.py +1 -1
  18. google/adk/flows/llm_flows/base_llm_flow.py +1 -1
  19. google/adk/flows/llm_flows/contents.py +10 -6
  20. google/adk/flows/llm_flows/functions.py +38 -18
  21. google/adk/flows/llm_flows/instructions.py +2 -2
  22. google/adk/models/gemini_llm_connection.py +2 -2
  23. google/adk/models/llm_response.py +10 -1
  24. google/adk/planners/built_in_planner.py +1 -0
  25. google/adk/sessions/_session_util.py +29 -0
  26. google/adk/sessions/database_session_service.py +60 -43
  27. google/adk/sessions/state.py +1 -1
  28. google/adk/sessions/vertex_ai_session_service.py +7 -5
  29. google/adk/tools/agent_tool.py +2 -3
  30. google/adk/tools/application_integration_tool/__init__.py +2 -0
  31. google/adk/tools/application_integration_tool/application_integration_toolset.py +48 -26
  32. google/adk/tools/application_integration_tool/clients/connections_client.py +26 -54
  33. google/adk/tools/application_integration_tool/integration_connector_tool.py +159 -0
  34. google/adk/tools/function_tool.py +42 -0
  35. google/adk/tools/google_api_tool/google_api_tool_set.py +12 -9
  36. google/adk/tools/load_artifacts_tool.py +1 -1
  37. google/adk/tools/openapi_tool/auth/credential_exchangers/oauth2_exchanger.py +4 -4
  38. google/adk/tools/openapi_tool/openapi_spec_parser/openapi_toolset.py +1 -1
  39. google/adk/tools/openapi_tool/openapi_spec_parser/operation_parser.py +5 -12
  40. google/adk/tools/openapi_tool/openapi_spec_parser/rest_api_tool.py +46 -8
  41. google/adk/version.py +1 -1
  42. {google_adk-0.2.0.dist-info → google_adk-0.4.0.dist-info}/METADATA +28 -9
  43. {google_adk-0.2.0.dist-info → google_adk-0.4.0.dist-info}/RECORD +46 -44
  44. {google_adk-0.2.0.dist-info → google_adk-0.4.0.dist-info}/WHEEL +0 -0
  45. {google_adk-0.2.0.dist-info → google_adk-0.4.0.dist-info}/entry_points.txt +0 -0
  46. {google_adk-0.2.0.dist-info → google_adk-0.4.0.dist-info}/licenses/LICENSE +0 -0
google/adk/cli/cli.py CHANGED
@@ -39,12 +39,12 @@ class InputFile(BaseModel):
39
39
 
40
40
  async def run_input_file(
41
41
  app_name: str,
42
+ user_id: str,
42
43
  root_agent: LlmAgent,
43
44
  artifact_service: BaseArtifactService,
44
- session: Session,
45
45
  session_service: BaseSessionService,
46
46
  input_path: str,
47
- ) -> None:
47
+ ) -> Session:
48
48
  runner = Runner(
49
49
  app_name=app_name,
50
50
  agent=root_agent,
@@ -55,9 +55,11 @@ async def run_input_file(
55
55
  input_file = InputFile.model_validate_json(f.read())
56
56
  input_file.state['_time'] = datetime.now()
57
57
 
58
- session.state = input_file.state
58
+ session = session_service.create_session(
59
+ app_name=app_name, user_id=user_id, state=input_file.state
60
+ )
59
61
  for query in input_file.queries:
60
- click.echo(f'user: {query}')
62
+ click.echo(f'[user]: {query}')
61
63
  content = types.Content(role='user', parts=[types.Part(text=query)])
62
64
  async for event in runner.run_async(
63
65
  user_id=session.user_id, session_id=session.id, new_message=content
@@ -65,23 +67,23 @@ async def run_input_file(
65
67
  if event.content and event.content.parts:
66
68
  if text := ''.join(part.text or '' for part in event.content.parts):
67
69
  click.echo(f'[{event.author}]: {text}')
70
+ return session
68
71
 
69
72
 
70
73
  async def run_interactively(
71
- app_name: str,
72
74
  root_agent: LlmAgent,
73
75
  artifact_service: BaseArtifactService,
74
76
  session: Session,
75
77
  session_service: BaseSessionService,
76
78
  ) -> None:
77
79
  runner = Runner(
78
- app_name=app_name,
80
+ app_name=session.app_name,
79
81
  agent=root_agent,
80
82
  artifact_service=artifact_service,
81
83
  session_service=session_service,
82
84
  )
83
85
  while True:
84
- query = input('user: ')
86
+ query = input('[user]: ')
85
87
  if not query or not query.strip():
86
88
  continue
87
89
  if query == 'exit':
@@ -100,7 +102,8 @@ async def run_cli(
100
102
  *,
101
103
  agent_parent_dir: str,
102
104
  agent_folder_name: str,
103
- json_file_path: Optional[str] = None,
105
+ input_file: Optional[str] = None,
106
+ saved_session_file: Optional[str] = None,
104
107
  save_session: bool,
105
108
  ) -> None:
106
109
  """Runs an interactive CLI for a certain agent.
@@ -109,8 +112,11 @@ async def run_cli(
109
112
  agent_parent_dir: str, the absolute path of the parent folder of the agent
110
113
  folder.
111
114
  agent_folder_name: str, the name of the agent folder.
112
- json_file_path: Optional[str], the absolute path to the json file, either
113
- *.input.json or *.session.json.
115
+ input_file: Optional[str], the absolute path to the json file that contains
116
+ the initial session state and user queries, exclusive with
117
+ saved_session_file.
118
+ saved_session_file: Optional[str], the absolute path to the json file that
119
+ contains a previously saved session, exclusive with input_file.
114
120
  save_session: bool, whether to save the session on exit.
115
121
  """
116
122
  if agent_parent_dir not in sys.path:
@@ -118,46 +124,50 @@ async def run_cli(
118
124
 
119
125
  artifact_service = InMemoryArtifactService()
120
126
  session_service = InMemorySessionService()
121
- session = session_service.create_session(
122
- app_name=agent_folder_name, user_id='test_user'
123
- )
124
127
 
125
128
  agent_module_path = os.path.join(agent_parent_dir, agent_folder_name)
126
129
  agent_module = importlib.import_module(agent_folder_name)
130
+ user_id = 'test_user'
131
+ session = session_service.create_session(
132
+ app_name=agent_folder_name, user_id=user_id
133
+ )
127
134
  root_agent = agent_module.agent.root_agent
128
135
  envs.load_dotenv_for_agent(agent_folder_name, agent_parent_dir)
129
- if json_file_path:
130
- if json_file_path.endswith('.input.json'):
131
- await run_input_file(
132
- app_name=agent_folder_name,
133
- root_agent=root_agent,
134
- artifact_service=artifact_service,
135
- session=session,
136
- session_service=session_service,
137
- input_path=json_file_path,
138
- )
139
- elif json_file_path.endswith('.session.json'):
140
- with open(json_file_path, 'r') as f:
141
- session = Session.model_validate_json(f.read())
142
- for content in session.get_contents():
143
- if content.role == 'user':
144
- print('user: ', content.parts[0].text)
136
+ if input_file:
137
+ session = await run_input_file(
138
+ app_name=agent_folder_name,
139
+ user_id=user_id,
140
+ root_agent=root_agent,
141
+ artifact_service=artifact_service,
142
+ session_service=session_service,
143
+ input_path=input_file,
144
+ )
145
+ elif saved_session_file:
146
+
147
+ loaded_session = None
148
+ with open(saved_session_file, 'r') as f:
149
+ loaded_session = Session.model_validate_json(f.read())
150
+
151
+ if loaded_session:
152
+ for event in loaded_session.events:
153
+ session_service.append_event(session, event)
154
+ content = event.content
155
+ if not content or not content.parts or not content.parts[0].text:
156
+ continue
157
+ if event.author == 'user':
158
+ click.echo(f'[user]: {content.parts[0].text}')
145
159
  else:
146
- print(content.parts[0].text)
147
- await run_interactively(
148
- agent_folder_name,
149
- root_agent,
150
- artifact_service,
151
- session,
152
- session_service,
153
- )
154
- else:
155
- print(f'Unsupported file type: {json_file_path}')
156
- exit(1)
160
+ click.echo(f'[{event.author}]: {content.parts[0].text}')
161
+
162
+ await run_interactively(
163
+ root_agent,
164
+ artifact_service,
165
+ session,
166
+ session_service,
167
+ )
157
168
  else:
158
- print(f'Running agent {root_agent.name}, type exit to exit.')
169
+ click.echo(f'Running agent {root_agent.name}, type exit to exit.')
159
170
  await run_interactively(
160
- agent_folder_name,
161
171
  root_agent,
162
172
  artifact_service,
163
173
  session,
@@ -165,11 +175,8 @@ async def run_cli(
165
175
  )
166
176
 
167
177
  if save_session:
168
- if json_file_path:
169
- session_path = json_file_path.replace('.input.json', '.session.json')
170
- else:
171
- session_id = input('Session ID to save: ')
172
- session_path = f'{agent_module_path}/{session_id}.session.json'
178
+ session_id = input('Session ID to save: ')
179
+ session_path = f'{agent_module_path}/{session_id}.session.json'
173
180
 
174
181
  # Fetch the session again to get all the details.
175
182
  session = session_service.get_session(
@@ -54,7 +54,7 @@ COPY "agents/{app_name}/" "/app/agents/{app_name}/"
54
54
 
55
55
  EXPOSE {port}
56
56
 
57
- CMD adk {command} --port={port} {trace_to_cloud_option} "/app/agents"
57
+ CMD adk {command} --port={port} {session_db_option} {trace_to_cloud_option} "/app/agents"
58
58
  """
59
59
 
60
60
 
@@ -85,6 +85,7 @@ def to_cloud_run(
85
85
  trace_to_cloud: bool,
86
86
  with_ui: bool,
87
87
  verbosity: str,
88
+ session_db_url: str,
88
89
  ):
89
90
  """Deploys an agent to Google Cloud Run.
90
91
 
@@ -112,6 +113,7 @@ def to_cloud_run(
112
113
  trace_to_cloud: Whether to enable Cloud Trace.
113
114
  with_ui: Whether to deploy with UI.
114
115
  verbosity: The verbosity level of the CLI.
116
+ session_db_url: The database URL to connect the session.
115
117
  """
116
118
  app_name = app_name or os.path.basename(agent_folder)
117
119
 
@@ -144,6 +146,9 @@ def to_cloud_run(
144
146
  port=port,
145
147
  command='web' if with_ui else 'api_server',
146
148
  install_agent_deps=install_agent_deps,
149
+ session_db_option=f'--session_db_url={session_db_url}'
150
+ if session_db_url
151
+ else '',
147
152
  trace_to_cloud_option='--trace_to_cloud' if trace_to_cloud else '',
148
153
  )
149
154
  dockerfile_path = os.path.join(temp_folder, 'Dockerfile')
@@ -256,7 +256,7 @@ def run_evals(
256
256
  )
257
257
 
258
258
  if final_eval_status == EvalStatus.PASSED:
259
- result = "✅ Passsed"
259
+ result = "✅ Passed"
260
260
  else:
261
261
  result = "❌ Failed"
262
262
 
@@ -96,6 +96,23 @@ def cli_create_cmd(
96
96
  )
97
97
 
98
98
 
99
+ def validate_exclusive(ctx, param, value):
100
+ # Store the validated parameters in the context
101
+ if not hasattr(ctx, "exclusive_opts"):
102
+ ctx.exclusive_opts = {}
103
+
104
+ # If this option has a value and we've already seen another exclusive option
105
+ if value is not None and any(ctx.exclusive_opts.values()):
106
+ exclusive_opt = next(key for key, val in ctx.exclusive_opts.items() if val)
107
+ raise click.UsageError(
108
+ f"Options '{param.name}' and '{exclusive_opt}' cannot be set together."
109
+ )
110
+
111
+ # Record this option's value
112
+ ctx.exclusive_opts[param.name] = value is not None
113
+ return value
114
+
115
+
99
116
  @main.command("run")
100
117
  @click.option(
101
118
  "--save_session",
@@ -105,13 +122,43 @@ def cli_create_cmd(
105
122
  default=False,
106
123
  help="Optional. Whether to save the session to a json file on exit.",
107
124
  )
125
+ @click.option(
126
+ "--replay",
127
+ type=click.Path(
128
+ exists=True, dir_okay=False, file_okay=True, resolve_path=True
129
+ ),
130
+ help=(
131
+ "The json file that contains the initial state of the session and user"
132
+ " queries. A new session will be created using this state. And user"
133
+ " queries are run againt the newly created session. Users cannot"
134
+ " continue to interact with the agent."
135
+ ),
136
+ callback=validate_exclusive,
137
+ )
138
+ @click.option(
139
+ "--resume",
140
+ type=click.Path(
141
+ exists=True, dir_okay=False, file_okay=True, resolve_path=True
142
+ ),
143
+ help=(
144
+ "The json file that contains a previously saved session (by"
145
+ "--save_session option). The previous session will be re-displayed. And"
146
+ " user can continue to interact with the agent."
147
+ ),
148
+ callback=validate_exclusive,
149
+ )
108
150
  @click.argument(
109
151
  "agent",
110
152
  type=click.Path(
111
153
  exists=True, dir_okay=True, file_okay=False, resolve_path=True
112
154
  ),
113
155
  )
114
- def cli_run(agent: str, save_session: bool):
156
+ def cli_run(
157
+ agent: str,
158
+ save_session: bool,
159
+ replay: Optional[str],
160
+ resume: Optional[str],
161
+ ):
115
162
  """Runs an interactive CLI for a certain agent.
116
163
 
117
164
  AGENT: The path to the agent source code folder.
@@ -129,6 +176,8 @@ def cli_run(agent: str, save_session: bool):
129
176
  run_cli(
130
177
  agent_parent_dir=agent_parent_folder,
131
178
  agent_folder_name=agent_folder_name,
179
+ input_file=replay,
180
+ saved_session_file=resume,
132
181
  save_session=save_session,
133
182
  )
134
183
  )
@@ -245,12 +294,13 @@ def cli_eval(
245
294
  @click.option(
246
295
  "--session_db_url",
247
296
  help=(
248
- "Optional. The database URL to store the session.\n\n - Use"
249
- " 'agentengine://<agent_engine_resource_id>' to connect to Vertex"
250
- " managed session service.\n\n - Use 'sqlite://<path_to_sqlite_file>'"
251
- " to connect to a SQLite DB.\n\n - See"
252
- " https://docs.sqlalchemy.org/en/20/core/engines.html#backend-specific-urls"
253
- " for more details on supported DB URLs."
297
+ """Optional. The database URL to store the session.
298
+
299
+ - Use 'agentengine://<agent_engine_resource_id>' to connect to Agent Engine sessions.
300
+
301
+ - Use 'sqlite://<path_to_sqlite_file>' to connect to a SQLite DB.
302
+
303
+ - See https://docs.sqlalchemy.org/en/20/core/engines.html#backend-specific-urls for more details on supported DB URLs."""
254
304
  ),
255
305
  )
256
306
  @click.option(
@@ -366,12 +416,13 @@ def cli_web(
366
416
  @click.option(
367
417
  "--session_db_url",
368
418
  help=(
369
- "Optional. The database URL to store the session.\n\n - Use"
370
- " 'agentengine://<agent_engine_resource_id>' to connect to Vertex"
371
- " managed session service.\n\n - Use 'sqlite://<path_to_sqlite_file>'"
372
- " to connect to a SQLite DB.\n\n - See"
373
- " https://docs.sqlalchemy.org/en/20/core/engines.html#backend-specific-urls"
374
- " for more details on supported DB URLs."
419
+ """Optional. The database URL to store the session.
420
+
421
+ - Use 'agentengine://<agent_engine_resource_id>' to connect to Agent Engine sessions.
422
+
423
+ - Use 'sqlite://<path_to_sqlite_file>' to connect to a SQLite DB.
424
+
425
+ - See https://docs.sqlalchemy.org/en/20/core/engines.html#backend-specific-urls for more details on supported DB URLs."""
375
426
  ),
376
427
  )
377
428
  @click.option(
@@ -541,6 +592,18 @@ def cli_api_server(
541
592
  default="WARNING",
542
593
  help="Optional. Override the default verbosity level.",
543
594
  )
595
+ @click.option(
596
+ "--session_db_url",
597
+ help=(
598
+ """Optional. The database URL to store the session.
599
+
600
+ - Use 'agentengine://<agent_engine_resource_id>' to connect to Agent Engine sessions.
601
+
602
+ - Use 'sqlite://<path_to_sqlite_file>' to connect to a SQLite DB.
603
+
604
+ - See https://docs.sqlalchemy.org/en/20/core/engines.html#backend-specific-urls for more details on supported DB URLs."""
605
+ ),
606
+ )
544
607
  @click.argument(
545
608
  "agent",
546
609
  type=click.Path(
@@ -558,6 +621,7 @@ def cli_deploy_cloud_run(
558
621
  trace_to_cloud: bool,
559
622
  with_ui: bool,
560
623
  verbosity: str,
624
+ session_db_url: str,
561
625
  ):
562
626
  """Deploys an agent to Cloud Run.
563
627
 
@@ -579,6 +643,7 @@ def cli_deploy_cloud_run(
579
643
  trace_to_cloud=trace_to_cloud,
580
644
  with_ui=with_ui,
581
645
  verbosity=verbosity,
646
+ session_db_url=session_db_url,
582
647
  )
583
648
  except Exception as e:
584
649
  click.secho(f"Deploy failed: {e}", fg="red", err=True)
@@ -756,6 +756,12 @@ def get_fast_api_app(
756
756
  except Exception as e:
757
757
  logger.exception("Error during live websocket communication: %s", e)
758
758
  traceback.print_exc()
759
+ WEBSOCKET_INTERNAL_ERROR_CODE = 1011
760
+ WEBSOCKET_MAX_BYTES_FOR_REASON = 123
761
+ await websocket.close(
762
+ code=WEBSOCKET_INTERNAL_ERROR_CODE,
763
+ reason=str(e)[:WEBSOCKET_MAX_BYTES_FOR_REASON],
764
+ )
759
765
  finally:
760
766
  for task in pending:
761
767
  task.cancel()
@@ -55,7 +55,7 @@ def load_json(file_path: str) -> Union[Dict, List]:
55
55
 
56
56
 
57
57
  class AgentEvaluator:
58
- """An evaluator for Agents, mainly intented for helping with test cases."""
58
+ """An evaluator for Agents, mainly intended for helping with test cases."""
59
59
 
60
60
  @staticmethod
61
61
  def find_config_for_test_file(test_file: str):
@@ -91,7 +91,7 @@ class AgentEvaluator:
91
91
  look for 'root_agent' in the loaded module.
92
92
  eval_dataset: The eval data set. This can be either a string representing
93
93
  full path to the file containing eval dataset, or a directory that is
94
- recusively explored for all files that have a `.test.json` suffix.
94
+ recursively explored for all files that have a `.test.json` suffix.
95
95
  num_runs: Number of times all entries in the eval dataset should be
96
96
  assessed.
97
97
  agent_name: The name of the agent.
@@ -35,7 +35,7 @@ class ResponseEvaluator:
35
35
  Args:
36
36
  raw_eval_dataset: The dataset that will be evaluated.
37
37
  evaluation_criteria: The evaluation criteria to be used. This method
38
- support two criterias, `response_evaluation_score` and
38
+ support two criteria, `response_evaluation_score` and
39
39
  `response_match_score`.
40
40
  print_detailed_results: Prints detailed results on the console. This is
41
41
  usually helpful during debugging.
@@ -56,7 +56,7 @@ class ResponseEvaluator:
56
56
  Value range: [0, 5], where 0 means that the agent's response is not
57
57
  coherent, while 5 means it is . High values are good.
58
58
  A note on raw_eval_dataset:
59
- The dataset should be a list session, where each sesssion is represented
59
+ The dataset should be a list session, where each session is represented
60
60
  as a list of interaction that need evaluation. Each evaluation is
61
61
  represented as a dictionary that is expected to have values for the
62
62
  following keys:
@@ -31,10 +31,9 @@ class TrajectoryEvaluator:
31
31
  ):
32
32
  r"""Returns the mean tool use accuracy of the eval dataset.
33
33
 
34
- Tool use accuracy is calculated by comparing the expected and actuall tool
35
- use trajectories. An exact match scores a 1, 0 otherwise. The final number
36
- is an
37
- average of these individual scores.
34
+ Tool use accuracy is calculated by comparing the expected and the actual
35
+ tool use trajectories. An exact match scores a 1, 0 otherwise. The final
36
+ number is an average of these individual scores.
38
37
 
39
38
  Value range: [0, 1], where 0 is means none of the too use entries aligned,
40
39
  and 1 would mean all of them aligned. Higher value is good.
@@ -45,7 +44,7 @@ class TrajectoryEvaluator:
45
44
  usually helpful during debugging.
46
45
 
47
46
  A note on eval_dataset:
48
- The dataset should be a list session, where each sesssion is represented
47
+ The dataset should be a list session, where each session is represented
49
48
  as a list of interaction that need evaluation. Each evaluation is
50
49
  represented as a dictionary that is expected to have values for the
51
50
  following keys:
@@ -48,8 +48,13 @@ class EventActions(BaseModel):
48
48
  """The agent is escalating to a higher level agent."""
49
49
 
50
50
  requested_auth_configs: dict[str, AuthConfig] = Field(default_factory=dict)
51
- """Will only be set by a tool response indicating tool request euc.
52
- dict key is the function call id since one function call response (from model)
53
- could correspond to multiple function calls.
54
- dict value is the required auth config.
51
+ """Authentication configurations requested by tool responses.
52
+
53
+ This field will only be set by a tool response event indicating tool request
54
+ auth credential.
55
+ - Keys: The function call id. Since one function response event could contain
56
+ multiple function responses that correspond to multiple function calls. Each
57
+ function call could request different auth configs. This id is used to
58
+ identify the function call.
59
+ - Values: The requested auth config.
55
60
  """
@@ -94,7 +94,7 @@ can answer it.
94
94
 
95
95
  If another agent is better for answering the question according to its
96
96
  description, call `{_TRANSFER_TO_AGENT_FUNCTION_NAME}` function to transfer the
97
- question to that agent. When transfering, do not generate any text other than
97
+ question to that agent. When transferring, do not generate any text other than
98
98
  the function call.
99
99
  """
100
100
 
@@ -115,7 +115,7 @@ class BaseLlmFlow(ABC):
115
115
  yield event
116
116
  # send back the function response
117
117
  if event.get_function_responses():
118
- logger.debug('Sending back last function resonse event: %s', event)
118
+ logger.debug('Sending back last function response event: %s', event)
119
119
  invocation_context.live_request_queue.send_content(event.content)
120
120
  if (
121
121
  event.content
@@ -15,9 +15,7 @@
15
15
  from __future__ import annotations
16
16
 
17
17
  import copy
18
- from typing import AsyncGenerator
19
- from typing import Generator
20
- from typing import Optional
18
+ from typing import AsyncGenerator, Generator, Optional
21
19
 
22
20
  from google.genai import types
23
21
  from typing_extensions import override
@@ -111,7 +109,7 @@ def _rearrange_events_for_latest_function_response(
111
109
  """Rearrange the events for the latest function_response.
112
110
 
113
111
  If the latest function_response is for an async function_call, all events
114
- bewteen the initial function_call and the latest function_response will be
112
+ between the initial function_call and the latest function_response will be
115
113
  removed.
116
114
 
117
115
  Args:
@@ -202,8 +200,14 @@ def _get_contents(
202
200
  # Parse the events, leaving the contents and the function calls and
203
201
  # responses from the current agent.
204
202
  for event in events:
205
- if not event.content or not event.content.role:
206
- # Skip events without content, or generated neither by user nor by model.
203
+ if (
204
+ not event.content
205
+ or not event.content.role
206
+ or not event.content.parts
207
+ or event.content.parts[0].text == ''
208
+ ):
209
+ # Skip events without content, or generated neither by user nor by model
210
+ # or has empty text.
207
211
  # E.g. events purely for mutating session states.
208
212
  continue
209
213
  if not _is_event_belongs_to_branch(current_branch, event):
@@ -151,28 +151,33 @@ async def handle_function_calls_async(
151
151
  # do not use "args" as the variable name, because it is a reserved keyword
152
152
  # in python debugger.
153
153
  function_args = function_call.args or {}
154
- function_response = None
155
- # Calls the tool if before_tool_callback does not exist or returns None.
154
+ function_response: Optional[dict] = None
155
+
156
+ # before_tool_callback (sync or async)
156
157
  if agent.before_tool_callback:
157
158
  function_response = agent.before_tool_callback(
158
159
  tool=tool, args=function_args, tool_context=tool_context
159
160
  )
161
+ if inspect.isawaitable(function_response):
162
+ function_response = await function_response
160
163
 
161
164
  if not function_response:
162
165
  function_response = await __call_tool_async(
163
166
  tool, args=function_args, tool_context=tool_context
164
167
  )
165
168
 
166
- # Calls after_tool_callback if it exists.
169
+ # after_tool_callback (sync or async)
167
170
  if agent.after_tool_callback:
168
- new_response = agent.after_tool_callback(
171
+ altered_function_response = agent.after_tool_callback(
169
172
  tool=tool,
170
173
  args=function_args,
171
174
  tool_context=tool_context,
172
175
  tool_response=function_response,
173
176
  )
174
- if new_response:
175
- function_response = new_response
177
+ if inspect.isawaitable(altered_function_response):
178
+ altered_function_response = await altered_function_response
179
+ if altered_function_response is not None:
180
+ function_response = altered_function_response
176
181
 
177
182
  if tool.is_long_running:
178
183
  # Allow long running function to return None to not provide function response.
@@ -223,11 +228,17 @@ async def handle_function_calls_live(
223
228
  # in python debugger.
224
229
  function_args = function_call.args or {}
225
230
  function_response = None
226
- # Calls the tool if before_tool_callback does not exist or returns None.
231
+ # # Calls the tool if before_tool_callback does not exist or returns None.
232
+ # if agent.before_tool_callback:
233
+ # function_response = agent.before_tool_callback(
234
+ # tool, function_args, tool_context
235
+ # )
227
236
  if agent.before_tool_callback:
228
237
  function_response = agent.before_tool_callback(
229
- tool, function_args, tool_context
238
+ tool=tool, args=function_args, tool_context=tool_context
230
239
  )
240
+ if inspect.isawaitable(function_response):
241
+ function_response = await function_response
231
242
 
232
243
  if not function_response:
233
244
  function_response = await _process_function_live_helper(
@@ -235,15 +246,26 @@ async def handle_function_calls_live(
235
246
  )
236
247
 
237
248
  # Calls after_tool_callback if it exists.
249
+ # if agent.after_tool_callback:
250
+ # new_response = agent.after_tool_callback(
251
+ # tool,
252
+ # function_args,
253
+ # tool_context,
254
+ # function_response,
255
+ # )
256
+ # if new_response:
257
+ # function_response = new_response
238
258
  if agent.after_tool_callback:
239
- new_response = agent.after_tool_callback(
240
- tool,
241
- function_args,
242
- tool_context,
243
- function_response,
259
+ altered_function_response = agent.after_tool_callback(
260
+ tool=tool,
261
+ args=function_args,
262
+ tool_context=tool_context,
263
+ tool_response=function_response,
244
264
  )
245
- if new_response:
246
- function_response = new_response
265
+ if inspect.isawaitable(altered_function_response):
266
+ altered_function_response = await altered_function_response
267
+ if altered_function_response is not None:
268
+ function_response = altered_function_response
247
269
 
248
270
  if tool.is_long_running:
249
271
  # Allow async function to return None to not provide function response.
@@ -310,9 +332,7 @@ async def _process_function_live_helper(
310
332
  function_response = {
311
333
  'status': f'No active streaming function named {function_name} found'
312
334
  }
313
- elif inspect.isasyncgenfunction(tool.func):
314
- print('is async')
315
-
335
+ elif hasattr(tool, "func") and inspect.isasyncgenfunction(tool.func):
316
336
  # for streaming tool use case
317
337
  # we require the function to be a async generator function
318
338
  async def run_tool_and_update_queue(tool, function_args, tool_context):
@@ -52,7 +52,7 @@ class _InstructionsLlmRequestProcessor(BaseLlmRequestProcessor):
52
52
  # Appends global instructions if set.
53
53
  if (
54
54
  isinstance(root_agent, LlmAgent) and root_agent.global_instruction
55
- ): # not emtpy str
55
+ ): # not empty str
56
56
  raw_si = root_agent.canonical_global_instruction(
57
57
  ReadonlyContext(invocation_context)
58
58
  )
@@ -60,7 +60,7 @@ class _InstructionsLlmRequestProcessor(BaseLlmRequestProcessor):
60
60
  llm_request.append_instructions([si])
61
61
 
62
62
  # Appends agent instructions if set.
63
- if agent.instruction: # not emtpy str
63
+ if agent.instruction: # not empty str
64
64
  raw_si = agent.canonical_instruction(ReadonlyContext(invocation_context))
65
65
  si = _populate_values(raw_si, invocation_context)
66
66
  llm_request.append_instructions([si])
@@ -152,7 +152,7 @@ class GeminiLlmConnection(BaseLlmConnection):
152
152
  ):
153
153
  # TODO: Right now, we just support output_transcription without
154
154
  # changing interface and data protocol. Later, we can consider to
155
- # support output_transcription as a separete field in LlmResponse.
155
+ # support output_transcription as a separate field in LlmResponse.
156
156
 
157
157
  # Transcription is always considered as partial event
158
158
  # We rely on other control signals to determine when to yield the
@@ -179,7 +179,7 @@ class GeminiLlmConnection(BaseLlmConnection):
179
179
  # in case of empty content or parts, we sill surface it
180
180
  # in case it's an interrupted message, we merge the previous partial
181
181
  # text. Other we don't merge. because content can be none when model
182
- # safty threshold is triggered
182
+ # safety threshold is triggered
183
183
  if message.server_content.interrupted and text:
184
184
  yield self.__build_full_text_response(text)
185
185
  text = ''