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.
- google/adk/agents/base_agent.py +7 -7
- google/adk/agents/callback_context.py +0 -1
- google/adk/agents/llm_agent.py +3 -8
- google/adk/auth/auth_credential.py +2 -1
- google/adk/auth/auth_handler.py +7 -3
- google/adk/cli/browser/index.html +1 -1
- google/adk/cli/browser/{main-ZBO76GRM.js → main-HWIBUY2R.js} +69 -53
- google/adk/cli/cli.py +54 -47
- google/adk/cli/cli_deploy.py +6 -1
- google/adk/cli/cli_eval.py +1 -1
- google/adk/cli/cli_tools_click.py +78 -13
- google/adk/cli/fast_api.py +6 -0
- google/adk/evaluation/agent_evaluator.py +2 -2
- google/adk/evaluation/response_evaluator.py +2 -2
- google/adk/evaluation/trajectory_evaluator.py +4 -5
- google/adk/events/event_actions.py +9 -4
- google/adk/flows/llm_flows/agent_transfer.py +1 -1
- google/adk/flows/llm_flows/base_llm_flow.py +1 -1
- google/adk/flows/llm_flows/contents.py +10 -6
- google/adk/flows/llm_flows/functions.py +38 -18
- google/adk/flows/llm_flows/instructions.py +2 -2
- google/adk/models/gemini_llm_connection.py +2 -2
- google/adk/models/llm_response.py +10 -1
- google/adk/planners/built_in_planner.py +1 -0
- google/adk/sessions/_session_util.py +29 -0
- google/adk/sessions/database_session_service.py +60 -43
- google/adk/sessions/state.py +1 -1
- google/adk/sessions/vertex_ai_session_service.py +7 -5
- google/adk/tools/agent_tool.py +2 -3
- google/adk/tools/application_integration_tool/__init__.py +2 -0
- google/adk/tools/application_integration_tool/application_integration_toolset.py +48 -26
- google/adk/tools/application_integration_tool/clients/connections_client.py +26 -54
- google/adk/tools/application_integration_tool/integration_connector_tool.py +159 -0
- google/adk/tools/function_tool.py +42 -0
- google/adk/tools/google_api_tool/google_api_tool_set.py +12 -9
- google/adk/tools/load_artifacts_tool.py +1 -1
- google/adk/tools/openapi_tool/auth/credential_exchangers/oauth2_exchanger.py +4 -4
- google/adk/tools/openapi_tool/openapi_spec_parser/openapi_toolset.py +1 -1
- google/adk/tools/openapi_tool/openapi_spec_parser/operation_parser.py +5 -12
- google/adk/tools/openapi_tool/openapi_spec_parser/rest_api_tool.py +46 -8
- google/adk/version.py +1 -1
- {google_adk-0.2.0.dist-info → google_adk-0.4.0.dist-info}/METADATA +28 -9
- {google_adk-0.2.0.dist-info → google_adk-0.4.0.dist-info}/RECORD +46 -44
- {google_adk-0.2.0.dist-info → google_adk-0.4.0.dist-info}/WHEEL +0 -0
- {google_adk-0.2.0.dist-info → google_adk-0.4.0.dist-info}/entry_points.txt +0 -0
- {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
|
-
) ->
|
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
|
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
|
-
|
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
|
-
|
113
|
-
|
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
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
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
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
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
|
-
|
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
|
-
|
169
|
-
|
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(
|
google/adk/cli/cli_deploy.py
CHANGED
@@ -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')
|
google/adk/cli/cli_eval.py
CHANGED
@@ -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(
|
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
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
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
|
370
|
-
|
371
|
-
|
372
|
-
|
373
|
-
|
374
|
-
|
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)
|
google/adk/cli/fast_api.py
CHANGED
@@ -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
|
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
|
-
|
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
|
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
|
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
|
35
|
-
use trajectories. An exact match scores a 1, 0 otherwise. The final
|
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
|
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
|
-
"""
|
52
|
-
|
53
|
-
|
54
|
-
|
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
|
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
|
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
|
-
|
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
|
206
|
-
|
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
|
-
|
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
|
-
#
|
169
|
+
# after_tool_callback (sync or async)
|
167
170
|
if agent.after_tool_callback:
|
168
|
-
|
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
|
175
|
-
|
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
|
-
|
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
|
246
|
-
|
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
|
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
|
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
|
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
|
-
#
|
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 = ''
|