google-adk 1.0.0__py3-none-any.whl → 1.1.1__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/callback_context.py +2 -1
- google/adk/agents/readonly_context.py +3 -1
- google/adk/auth/auth_credential.py +4 -1
- google/adk/cli/browser/index.html +4 -4
- google/adk/cli/browser/{main-QOEMUXM4.js → main-PKDNKWJE.js} +59 -59
- google/adk/cli/browser/polyfills-B6TNHZQ6.js +17 -0
- google/adk/cli/cli.py +3 -2
- google/adk/cli/cli_eval.py +6 -85
- google/adk/cli/cli_tools_click.py +39 -10
- google/adk/cli/fast_api.py +53 -184
- google/adk/cli/utils/agent_loader.py +137 -0
- google/adk/cli/utils/cleanup.py +40 -0
- google/adk/cli/utils/evals.py +2 -1
- google/adk/cli/utils/logs.py +2 -7
- google/adk/code_executors/code_execution_utils.py +2 -1
- google/adk/code_executors/container_code_executor.py +0 -1
- google/adk/code_executors/vertex_ai_code_executor.py +6 -8
- google/adk/evaluation/eval_case.py +3 -1
- google/adk/evaluation/eval_metrics.py +74 -0
- google/adk/evaluation/eval_result.py +86 -0
- google/adk/evaluation/eval_set.py +2 -0
- google/adk/evaluation/eval_set_results_manager.py +47 -0
- google/adk/evaluation/eval_sets_manager.py +2 -1
- google/adk/evaluation/evaluator.py +2 -0
- google/adk/evaluation/local_eval_set_results_manager.py +113 -0
- google/adk/evaluation/local_eval_sets_manager.py +4 -4
- google/adk/evaluation/response_evaluator.py +2 -1
- google/adk/evaluation/trajectory_evaluator.py +3 -2
- google/adk/examples/base_example_provider.py +1 -0
- google/adk/flows/llm_flows/base_llm_flow.py +4 -6
- google/adk/flows/llm_flows/contents.py +3 -1
- google/adk/flows/llm_flows/instructions.py +7 -77
- google/adk/flows/llm_flows/single_flow.py +1 -1
- google/adk/models/base_llm.py +2 -1
- google/adk/models/base_llm_connection.py +2 -0
- google/adk/models/google_llm.py +4 -1
- google/adk/models/lite_llm.py +3 -2
- google/adk/models/llm_response.py +2 -1
- google/adk/runners.py +36 -4
- google/adk/sessions/_session_util.py +2 -1
- google/adk/sessions/database_session_service.py +5 -8
- google/adk/sessions/vertex_ai_session_service.py +28 -13
- google/adk/telemetry.py +4 -2
- google/adk/tools/agent_tool.py +1 -1
- google/adk/tools/apihub_tool/apihub_toolset.py +1 -1
- google/adk/tools/apihub_tool/clients/apihub_client.py +10 -3
- google/adk/tools/apihub_tool/clients/secret_client.py +1 -0
- google/adk/tools/application_integration_tool/application_integration_toolset.py +6 -2
- google/adk/tools/application_integration_tool/clients/connections_client.py +8 -1
- google/adk/tools/application_integration_tool/clients/integration_client.py +3 -1
- google/adk/tools/application_integration_tool/integration_connector_tool.py +1 -1
- google/adk/tools/base_toolset.py +40 -2
- google/adk/tools/bigquery/__init__.py +38 -0
- google/adk/tools/bigquery/bigquery_credentials.py +217 -0
- google/adk/tools/bigquery/bigquery_tool.py +116 -0
- google/adk/tools/bigquery/bigquery_toolset.py +86 -0
- google/adk/tools/bigquery/client.py +33 -0
- google/adk/tools/bigquery/metadata_tool.py +249 -0
- google/adk/tools/bigquery/query_tool.py +76 -0
- google/adk/tools/function_parameter_parse_util.py +7 -0
- google/adk/tools/function_tool.py +33 -3
- google/adk/tools/get_user_choice_tool.py +1 -0
- google/adk/tools/google_api_tool/__init__.py +17 -11
- google/adk/tools/google_api_tool/google_api_tool.py +1 -1
- google/adk/tools/google_api_tool/google_api_toolset.py +0 -14
- google/adk/tools/google_api_tool/google_api_toolsets.py +8 -2
- google/adk/tools/google_search_tool.py +2 -2
- google/adk/tools/mcp_tool/conversion_utils.py +6 -2
- google/adk/tools/mcp_tool/mcp_session_manager.py +62 -188
- google/adk/tools/mcp_tool/mcp_tool.py +27 -24
- google/adk/tools/mcp_tool/mcp_toolset.py +76 -131
- google/adk/tools/openapi_tool/auth/credential_exchangers/base_credential_exchanger.py +1 -3
- google/adk/tools/openapi_tool/auth/credential_exchangers/service_account_exchanger.py +6 -7
- google/adk/tools/openapi_tool/common/common.py +5 -1
- google/adk/tools/openapi_tool/openapi_spec_parser/__init__.py +7 -2
- google/adk/tools/openapi_tool/openapi_spec_parser/openapi_toolset.py +2 -7
- google/adk/tools/openapi_tool/openapi_spec_parser/operation_parser.py +5 -1
- google/adk/tools/openapi_tool/openapi_spec_parser/rest_api_tool.py +11 -1
- google/adk/tools/toolbox_toolset.py +31 -3
- google/adk/utils/__init__.py +13 -0
- google/adk/utils/instructions_utils.py +131 -0
- google/adk/version.py +1 -1
- {google_adk-1.0.0.dist-info → google_adk-1.1.1.dist-info}/METADATA +12 -15
- {google_adk-1.0.0.dist-info → google_adk-1.1.1.dist-info}/RECORD +87 -78
- google/adk/agents/base_agent.py.orig +0 -330
- google/adk/cli/browser/polyfills-FFHMD2TL.js +0 -18
- google/adk/cli/fast_api.py.orig +0 -822
- google/adk/memory/base_memory_service.py.orig +0 -76
- google/adk/models/google_llm.py.orig +0 -305
- google/adk/tools/_built_in_code_execution_tool.py +0 -70
- google/adk/tools/mcp_tool/mcp_session_manager.py.orig +0 -322
- {google_adk-1.0.0.dist-info → google_adk-1.1.1.dist-info}/WHEEL +0 -0
- {google_adk-1.0.0.dist-info → google_adk-1.1.1.dist-info}/entry_points.txt +0 -0
- {google_adk-1.0.0.dist-info → google_adk-1.1.1.dist-info}/licenses/LICENSE +0 -0
google/adk/cli/fast_api.py
CHANGED
@@ -13,16 +13,13 @@
|
|
13
13
|
# limitations under the License.
|
14
14
|
|
15
15
|
|
16
|
+
from __future__ import annotations
|
17
|
+
|
16
18
|
import asyncio
|
17
19
|
from contextlib import asynccontextmanager
|
18
|
-
import importlib
|
19
|
-
import inspect
|
20
|
-
import json
|
21
20
|
import logging
|
22
21
|
import os
|
23
22
|
from pathlib import Path
|
24
|
-
import signal
|
25
|
-
import sys
|
26
23
|
import time
|
27
24
|
import traceback
|
28
25
|
import typing
|
@@ -55,15 +52,18 @@ from starlette.types import Lifespan
|
|
55
52
|
from typing_extensions import override
|
56
53
|
|
57
54
|
from ..agents import RunConfig
|
58
|
-
from ..agents.base_agent import BaseAgent
|
59
55
|
from ..agents.live_request_queue import LiveRequest
|
60
56
|
from ..agents.live_request_queue import LiveRequestQueue
|
61
57
|
from ..agents.llm_agent import Agent
|
62
|
-
from ..agents.llm_agent import LlmAgent
|
63
58
|
from ..agents.run_config import StreamingMode
|
64
|
-
from ..artifacts import InMemoryArtifactService
|
59
|
+
from ..artifacts.in_memory_artifact_service import InMemoryArtifactService
|
65
60
|
from ..evaluation.eval_case import EvalCase
|
66
61
|
from ..evaluation.eval_case import SessionInput
|
62
|
+
from ..evaluation.eval_metrics import EvalMetric
|
63
|
+
from ..evaluation.eval_metrics import EvalMetricResult
|
64
|
+
from ..evaluation.eval_metrics import EvalMetricResultPerInvocation
|
65
|
+
from ..evaluation.eval_result import EvalSetResult
|
66
|
+
from ..evaluation.local_eval_set_results_manager import LocalEvalSetResultsManager
|
67
67
|
from ..evaluation.local_eval_sets_manager import LocalEvalSetsManager
|
68
68
|
from ..events.event import Event
|
69
69
|
from ..memory.in_memory_memory_service import InMemoryMemoryService
|
@@ -72,23 +72,18 @@ from ..sessions.database_session_service import DatabaseSessionService
|
|
72
72
|
from ..sessions.in_memory_session_service import InMemorySessionService
|
73
73
|
from ..sessions.session import Session
|
74
74
|
from ..sessions.vertex_ai_session_service import VertexAiSessionService
|
75
|
-
from ..tools.base_toolset import BaseToolset
|
76
75
|
from .cli_eval import EVAL_SESSION_ID_PREFIX
|
77
|
-
from .cli_eval import EvalCaseResult
|
78
|
-
from .cli_eval import EvalMetric
|
79
|
-
from .cli_eval import EvalMetricResult
|
80
|
-
from .cli_eval import EvalMetricResultPerInvocation
|
81
|
-
from .cli_eval import EvalSetResult
|
82
76
|
from .cli_eval import EvalStatus
|
77
|
+
from .utils import cleanup
|
83
78
|
from .utils import common
|
84
79
|
from .utils import create_empty_state
|
85
80
|
from .utils import envs
|
86
81
|
from .utils import evals
|
82
|
+
from .utils.agent_loader import AgentLoader
|
87
83
|
|
88
84
|
logger = logging.getLogger("google_adk." + __name__)
|
89
85
|
|
90
86
|
_EVAL_SET_FILE_EXTENSION = ".evalset.json"
|
91
|
-
_EVAL_SET_RESULT_FILE_EXTENSION = ".evalset_result.json"
|
92
87
|
|
93
88
|
|
94
89
|
class ApiServerSpanExporter(export.SpanExporter):
|
@@ -196,7 +191,7 @@ class GetEventGraphResult(common.BaseModel):
|
|
196
191
|
|
197
192
|
def get_fast_api_app(
|
198
193
|
*,
|
199
|
-
|
194
|
+
agents_dir: str,
|
200
195
|
session_db_url: str = "",
|
201
196
|
allow_origins: Optional[list[str]] = None,
|
202
197
|
web: bool,
|
@@ -215,7 +210,7 @@ def get_fast_api_app(
|
|
215
210
|
memory_exporter = InMemoryExporter(session_trace_dict)
|
216
211
|
provider.add_span_processor(export.SimpleSpanProcessor(memory_exporter))
|
217
212
|
if trace_to_cloud:
|
218
|
-
envs.load_dotenv_for_agent("",
|
213
|
+
envs.load_dotenv_for_agent("", agents_dir)
|
219
214
|
if project_id := os.environ.get("GOOGLE_CLOUD_PROJECT", None):
|
220
215
|
processor = export.BatchSpanProcessor(
|
221
216
|
CloudTraceSpanExporter(project_id=project_id)
|
@@ -229,27 +224,8 @@ def get_fast_api_app(
|
|
229
224
|
|
230
225
|
trace.set_tracer_provider(provider)
|
231
226
|
|
232
|
-
toolsets_to_close: set[BaseToolset] = set()
|
233
|
-
|
234
227
|
@asynccontextmanager
|
235
228
|
async def internal_lifespan(app: FastAPI):
|
236
|
-
# Set up signal handlers for graceful shutdown
|
237
|
-
original_sigterm = signal.getsignal(signal.SIGTERM)
|
238
|
-
original_sigint = signal.getsignal(signal.SIGINT)
|
239
|
-
|
240
|
-
def cleanup_handler(sig, frame):
|
241
|
-
# Log the signal
|
242
|
-
logger.info("Received signal %s, performing pre-shutdown cleanup", sig)
|
243
|
-
# Do synchronous cleanup if needed
|
244
|
-
# Then call original handler if it exists
|
245
|
-
if sig == signal.SIGTERM and callable(original_sigterm):
|
246
|
-
original_sigterm(sig, frame)
|
247
|
-
elif sig == signal.SIGINT and callable(original_sigint):
|
248
|
-
original_sigint(sig, frame)
|
249
|
-
|
250
|
-
# Install cleanup handlers
|
251
|
-
signal.signal(signal.SIGTERM, cleanup_handler)
|
252
|
-
signal.signal(signal.SIGINT, cleanup_handler)
|
253
229
|
|
254
230
|
try:
|
255
231
|
if lifespan:
|
@@ -258,46 +234,8 @@ def get_fast_api_app(
|
|
258
234
|
else:
|
259
235
|
yield
|
260
236
|
finally:
|
261
|
-
#
|
262
|
-
|
263
|
-
"Server shutdown initiated, cleaning up %s toolsets",
|
264
|
-
len(toolsets_to_close),
|
265
|
-
)
|
266
|
-
|
267
|
-
# Create tasks for all toolset closures to run concurrently
|
268
|
-
cleanup_tasks = []
|
269
|
-
for toolset in toolsets_to_close:
|
270
|
-
task = asyncio.create_task(close_toolset_safely(toolset))
|
271
|
-
cleanup_tasks.append(task)
|
272
|
-
|
273
|
-
if cleanup_tasks:
|
274
|
-
# Wait for all cleanup tasks with timeout
|
275
|
-
done, pending = await asyncio.wait(
|
276
|
-
cleanup_tasks,
|
277
|
-
timeout=10.0, # 10 second timeout for cleanup
|
278
|
-
return_when=asyncio.ALL_COMPLETED,
|
279
|
-
)
|
280
|
-
|
281
|
-
# If any tasks are still pending, log it
|
282
|
-
if pending:
|
283
|
-
logger.warning(
|
284
|
-
f"{len(pending)} toolset cleanup tasks didn't complete in time"
|
285
|
-
)
|
286
|
-
for task in pending:
|
287
|
-
task.cancel()
|
288
|
-
|
289
|
-
# Restore original signal handlers
|
290
|
-
signal.signal(signal.SIGTERM, original_sigterm)
|
291
|
-
signal.signal(signal.SIGINT, original_sigint)
|
292
|
-
|
293
|
-
async def close_toolset_safely(toolset):
|
294
|
-
"""Safely close a toolset with error handling."""
|
295
|
-
try:
|
296
|
-
logger.info(f"Closing toolset: {type(toolset).__name__}")
|
297
|
-
await toolset.close()
|
298
|
-
logger.info(f"Successfully closed toolset: {type(toolset).__name__}")
|
299
|
-
except Exception as e:
|
300
|
-
logger.error(f"Error closing toolset {type(toolset).__name__}: {e}")
|
237
|
+
# Create tasks for all runner closures to run concurrently
|
238
|
+
await cleanup.close_runners(list(runner_dict.values()))
|
301
239
|
|
302
240
|
# Run the FastAPI server.
|
303
241
|
app = FastAPI(lifespan=internal_lifespan)
|
@@ -311,17 +249,14 @@ def get_fast_api_app(
|
|
311
249
|
allow_headers=["*"],
|
312
250
|
)
|
313
251
|
|
314
|
-
if agent_dir not in sys.path:
|
315
|
-
sys.path.append(agent_dir)
|
316
|
-
|
317
252
|
runner_dict = {}
|
318
|
-
root_agent_dict = {}
|
319
253
|
|
320
254
|
# Build the Artifact service
|
321
255
|
artifact_service = InMemoryArtifactService()
|
322
256
|
memory_service = InMemoryMemoryService()
|
323
257
|
|
324
|
-
eval_sets_manager = LocalEvalSetsManager(
|
258
|
+
eval_sets_manager = LocalEvalSetsManager(agents_dir=agents_dir)
|
259
|
+
eval_set_results_manager = LocalEvalSetResultsManager(agents_dir=agents_dir)
|
325
260
|
|
326
261
|
# Build the Session service
|
327
262
|
agent_engine_id = ""
|
@@ -331,7 +266,7 @@ def get_fast_api_app(
|
|
331
266
|
agent_engine_id = session_db_url.split("://")[1]
|
332
267
|
if not agent_engine_id:
|
333
268
|
raise click.ClickException("Agent engine id can not be empty.")
|
334
|
-
envs.load_dotenv_for_agent("",
|
269
|
+
envs.load_dotenv_for_agent("", agents_dir)
|
335
270
|
session_service = VertexAiSessionService(
|
336
271
|
os.environ["GOOGLE_CLOUD_PROJECT"],
|
337
272
|
os.environ["GOOGLE_CLOUD_LOCATION"],
|
@@ -341,9 +276,12 @@ def get_fast_api_app(
|
|
341
276
|
else:
|
342
277
|
session_service = InMemorySessionService()
|
343
278
|
|
279
|
+
# initialize Agent Loader
|
280
|
+
agent_loader = AgentLoader(agents_dir)
|
281
|
+
|
344
282
|
@app.get("/list-apps")
|
345
283
|
def list_apps() -> list[str]:
|
346
|
-
base_path = Path.cwd() /
|
284
|
+
base_path = Path.cwd() / agents_dir
|
347
285
|
if not base_path.exists():
|
348
286
|
raise HTTPException(status_code=404, detail="Path not found")
|
349
287
|
if not base_path.is_dir():
|
@@ -459,9 +397,9 @@ def get_fast_api_app(
|
|
459
397
|
app_name=app_name, user_id=user_id, state=state
|
460
398
|
)
|
461
399
|
|
462
|
-
def _get_eval_set_file_path(app_name,
|
400
|
+
def _get_eval_set_file_path(app_name, agents_dir, eval_set_id) -> str:
|
463
401
|
return os.path.join(
|
464
|
-
|
402
|
+
agents_dir,
|
465
403
|
app_name,
|
466
404
|
eval_set_id + _EVAL_SET_FILE_EXTENSION,
|
467
405
|
)
|
@@ -509,7 +447,7 @@ def get_fast_api_app(
|
|
509
447
|
|
510
448
|
# Populate the session with initial session state.
|
511
449
|
initial_session_state = create_empty_state(
|
512
|
-
|
450
|
+
agent_loader.load_agent(app_name)
|
513
451
|
)
|
514
452
|
|
515
453
|
new_eval_case = EvalCase(
|
@@ -551,8 +489,6 @@ def get_fast_api_app(
|
|
551
489
|
|
552
490
|
# Create a mapping from eval set file to all the evals that needed to be
|
553
491
|
# run.
|
554
|
-
envs.load_dotenv_for_agent(os.path.basename(app_name), agent_dir)
|
555
|
-
|
556
492
|
eval_set = eval_sets_manager.get_eval_set(app_name, eval_set_id)
|
557
493
|
|
558
494
|
if req.eval_ids:
|
@@ -562,7 +498,7 @@ def get_fast_api_app(
|
|
562
498
|
logger.info("Eval ids to run list is empty. We will run all eval cases.")
|
563
499
|
eval_set_to_evals = {eval_set_id: eval_set.eval_cases}
|
564
500
|
|
565
|
-
root_agent =
|
501
|
+
root_agent = agent_loader.load_agent(app_name)
|
566
502
|
run_eval_results = []
|
567
503
|
eval_case_results = []
|
568
504
|
async for eval_case_result in run_evals(
|
@@ -594,32 +530,10 @@ def get_fast_api_app(
|
|
594
530
|
)
|
595
531
|
eval_case_results.append(eval_case_result)
|
596
532
|
|
597
|
-
|
598
|
-
|
599
|
-
eval_set_result = EvalSetResult(
|
600
|
-
eval_set_result_id=eval_set_result_name,
|
601
|
-
eval_set_result_name=eval_set_result_name,
|
602
|
-
eval_set_id=eval_set_id,
|
603
|
-
eval_case_results=eval_case_results,
|
604
|
-
creation_timestamp=timestamp,
|
533
|
+
eval_set_results_manager.save_eval_set_result(
|
534
|
+
app_name, eval_set_id, eval_case_results
|
605
535
|
)
|
606
536
|
|
607
|
-
# Write eval result file, with eval_set_result_name.
|
608
|
-
app_eval_history_dir = os.path.join(
|
609
|
-
agent_dir, app_name, ".adk", "eval_history"
|
610
|
-
)
|
611
|
-
if not os.path.exists(app_eval_history_dir):
|
612
|
-
os.makedirs(app_eval_history_dir)
|
613
|
-
# Convert to json and write to file.
|
614
|
-
eval_set_result_json = eval_set_result.model_dump_json()
|
615
|
-
eval_set_result_file_path = os.path.join(
|
616
|
-
app_eval_history_dir,
|
617
|
-
eval_set_result_name + _EVAL_SET_RESULT_FILE_EXTENSION,
|
618
|
-
)
|
619
|
-
logger.info("Writing eval result to file: %s", eval_set_result_file_path)
|
620
|
-
with open(eval_set_result_file_path, "w") as f:
|
621
|
-
f.write(json.dumps(eval_set_result_json, indent=2))
|
622
|
-
|
623
537
|
return run_eval_results
|
624
538
|
|
625
539
|
@app.get(
|
@@ -631,25 +545,14 @@ def get_fast_api_app(
|
|
631
545
|
eval_result_id: str,
|
632
546
|
) -> EvalSetResult:
|
633
547
|
"""Gets the eval result for the given eval id."""
|
634
|
-
# Load the eval set file data
|
635
|
-
maybe_eval_result_file_path = (
|
636
|
-
os.path.join(
|
637
|
-
agent_dir, app_name, ".adk", "eval_history", eval_result_id
|
638
|
-
)
|
639
|
-
+ _EVAL_SET_RESULT_FILE_EXTENSION
|
640
|
-
)
|
641
|
-
if not os.path.exists(maybe_eval_result_file_path):
|
642
|
-
raise HTTPException(
|
643
|
-
status_code=404,
|
644
|
-
detail=f"Eval result `{eval_result_id}` not found.",
|
645
|
-
)
|
646
|
-
with open(maybe_eval_result_file_path, "r") as file:
|
647
|
-
eval_result_data = json.load(file) # Load JSON into a list
|
648
548
|
try:
|
649
|
-
|
650
|
-
|
651
|
-
|
652
|
-
|
549
|
+
return eval_set_results_manager.get_eval_set_result(
|
550
|
+
app_name, eval_result_id
|
551
|
+
)
|
552
|
+
except ValueError as ve:
|
553
|
+
raise HTTPException(status_code=404, detail=str(ve)) from ve
|
554
|
+
except ValidationError as ve:
|
555
|
+
raise HTTPException(status_code=500, detail=str(ve)) from ve
|
653
556
|
|
654
557
|
@app.get(
|
655
558
|
"/apps/{app_name}/eval_results",
|
@@ -657,19 +560,7 @@ def get_fast_api_app(
|
|
657
560
|
)
|
658
561
|
def list_eval_results(app_name: str) -> list[str]:
|
659
562
|
"""Lists all eval results for the given app."""
|
660
|
-
|
661
|
-
agent_dir, app_name, ".adk", "eval_history"
|
662
|
-
)
|
663
|
-
|
664
|
-
if not os.path.exists(app_eval_history_directory):
|
665
|
-
return []
|
666
|
-
|
667
|
-
eval_result_files = [
|
668
|
-
file.removesuffix(_EVAL_SET_RESULT_FILE_EXTENSION)
|
669
|
-
for file in os.listdir(app_eval_history_directory)
|
670
|
-
if file.endswith(_EVAL_SET_RESULT_FILE_EXTENSION)
|
671
|
-
]
|
672
|
-
return eval_result_files
|
563
|
+
return eval_set_results_manager.list_eval_set_results(app_name)
|
673
564
|
|
674
565
|
@app.delete("/apps/{app_name}/users/{user_id}/sessions/{session_id}")
|
675
566
|
async def delete_session(app_name: str, user_id: str, session_id: str):
|
@@ -769,9 +660,9 @@ def get_fast_api_app(
|
|
769
660
|
@app.post("/run", response_model_exclude_none=True)
|
770
661
|
async def agent_run(req: AgentRunRequest) -> list[Event]:
|
771
662
|
# Connect to managed session if agent_engine_id is set.
|
772
|
-
|
663
|
+
app_name = agent_engine_id if agent_engine_id else req.app_name
|
773
664
|
session = await session_service.get_session(
|
774
|
-
app_name=
|
665
|
+
app_name=app_name, user_id=req.user_id, session_id=req.session_id
|
775
666
|
)
|
776
667
|
if not session:
|
777
668
|
raise HTTPException(status_code=404, detail="Session not found")
|
@@ -790,10 +681,10 @@ def get_fast_api_app(
|
|
790
681
|
@app.post("/run_sse")
|
791
682
|
async def agent_run_sse(req: AgentRunRequest) -> StreamingResponse:
|
792
683
|
# Connect to managed session if agent_engine_id is set.
|
793
|
-
|
684
|
+
app_name = agent_engine_id if agent_engine_id else req.app_name
|
794
685
|
# SSE endpoint
|
795
686
|
session = await session_service.get_session(
|
796
|
-
app_name=
|
687
|
+
app_name=app_name, user_id=req.user_id, session_id=req.session_id
|
797
688
|
)
|
798
689
|
if not session:
|
799
690
|
raise HTTPException(status_code=404, detail="Session not found")
|
@@ -832,9 +723,9 @@ def get_fast_api_app(
|
|
832
723
|
app_name: str, user_id: str, session_id: str, event_id: str
|
833
724
|
):
|
834
725
|
# Connect to managed session if agent_engine_id is set.
|
835
|
-
|
726
|
+
app_name = agent_engine_id if agent_engine_id else app_name
|
836
727
|
session = await session_service.get_session(
|
837
|
-
app_name=
|
728
|
+
app_name=app_name, user_id=user_id, session_id=session_id
|
838
729
|
)
|
839
730
|
session_events = session.events if session else []
|
840
731
|
event = next((x for x in session_events if x.id == event_id), None)
|
@@ -845,7 +736,7 @@ def get_fast_api_app(
|
|
845
736
|
|
846
737
|
function_calls = event.get_function_calls()
|
847
738
|
function_responses = event.get_function_responses()
|
848
|
-
root_agent =
|
739
|
+
root_agent = agent_loader.load_agent(app_name)
|
849
740
|
dot_graph = None
|
850
741
|
if function_calls:
|
851
742
|
function_call_highlights = []
|
@@ -889,9 +780,9 @@ def get_fast_api_app(
|
|
889
780
|
await websocket.accept()
|
890
781
|
|
891
782
|
# Connect to managed session if agent_engine_id is set.
|
892
|
-
|
783
|
+
app_name = agent_engine_id if agent_engine_id else app_name
|
893
784
|
session = await session_service.get_session(
|
894
|
-
app_name=
|
785
|
+
app_name=app_name, user_id=user_id, session_id=session_id
|
895
786
|
)
|
896
787
|
if not session:
|
897
788
|
# Accept first so that the client is aware of connection establishment,
|
@@ -946,36 +837,12 @@ def get_fast_api_app(
|
|
946
837
|
for task in pending:
|
947
838
|
task.cancel()
|
948
839
|
|
949
|
-
def _get_all_toolsets(agent: BaseAgent) -> set[BaseToolset]:
|
950
|
-
toolsets = set()
|
951
|
-
if isinstance(agent, LlmAgent):
|
952
|
-
for tool_union in agent.tools:
|
953
|
-
if isinstance(tool_union, BaseToolset):
|
954
|
-
toolsets.add(tool_union)
|
955
|
-
for sub_agent in agent.sub_agents:
|
956
|
-
toolsets.update(_get_all_toolsets(sub_agent))
|
957
|
-
return toolsets
|
958
|
-
|
959
|
-
async def _get_root_agent_async(app_name: str) -> Agent:
|
960
|
-
"""Returns the root agent for the given app."""
|
961
|
-
if app_name in root_agent_dict:
|
962
|
-
return root_agent_dict[app_name]
|
963
|
-
agent_module = importlib.import_module(app_name)
|
964
|
-
if getattr(agent_module.agent, "root_agent"):
|
965
|
-
root_agent = agent_module.agent.root_agent
|
966
|
-
else:
|
967
|
-
raise ValueError(f'Unable to find "root_agent" from {app_name}.')
|
968
|
-
|
969
|
-
root_agent_dict[app_name] = root_agent
|
970
|
-
toolsets_to_close.update(_get_all_toolsets(root_agent))
|
971
|
-
return root_agent
|
972
|
-
|
973
840
|
async def _get_runner_async(app_name: str) -> Runner:
|
974
841
|
"""Returns the runner for the given app."""
|
975
|
-
envs.load_dotenv_for_agent(os.path.basename(app_name),
|
842
|
+
envs.load_dotenv_for_agent(os.path.basename(app_name), agents_dir)
|
976
843
|
if app_name in runner_dict:
|
977
844
|
return runner_dict[app_name]
|
978
|
-
root_agent =
|
845
|
+
root_agent = agent_loader.load_agent(app_name)
|
979
846
|
runner = Runner(
|
980
847
|
app_name=agent_engine_id if agent_engine_id else app_name,
|
981
848
|
agent=root_agent,
|
@@ -991,14 +858,16 @@ def get_fast_api_app(
|
|
991
858
|
ANGULAR_DIST_PATH = BASE_DIR / "browser"
|
992
859
|
|
993
860
|
@app.get("/")
|
994
|
-
async def
|
995
|
-
return RedirectResponse("/dev-ui")
|
861
|
+
async def redirect_root_to_dev_ui():
|
862
|
+
return RedirectResponse("/dev-ui/")
|
996
863
|
|
997
864
|
@app.get("/dev-ui")
|
998
|
-
async def
|
999
|
-
return
|
865
|
+
async def redirect_dev_ui_add_slash():
|
866
|
+
return RedirectResponse("/dev-ui/")
|
1000
867
|
|
1001
868
|
app.mount(
|
1002
|
-
"/",
|
869
|
+
"/dev-ui/",
|
870
|
+
StaticFiles(directory=ANGULAR_DIST_PATH, html=True),
|
871
|
+
name="static",
|
1003
872
|
)
|
1004
873
|
return app
|
@@ -0,0 +1,137 @@
|
|
1
|
+
# Copyright 2025 Google LLC
|
2
|
+
#
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
4
|
+
# you may not use this file except in compliance with the License.
|
5
|
+
# You may obtain a copy of the License at
|
6
|
+
#
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
8
|
+
#
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
12
|
+
# See the License for the specific language governing permissions and
|
13
|
+
# limitations under the License.
|
14
|
+
|
15
|
+
from __future__ import annotations
|
16
|
+
|
17
|
+
import importlib
|
18
|
+
import logging
|
19
|
+
import sys
|
20
|
+
|
21
|
+
from . import envs
|
22
|
+
from ...agents.base_agent import BaseAgent
|
23
|
+
|
24
|
+
logger = logging.getLogger("google_adk." + __name__)
|
25
|
+
|
26
|
+
|
27
|
+
class AgentLoader:
|
28
|
+
"""Centralized agent loading with proper isolation, caching, and .env loading.
|
29
|
+
Support loading agents from below folder/file structures:
|
30
|
+
a) agents_dir/agent_name.py (with root_agent or agent.root_agent in it)
|
31
|
+
b) agents_dir/agent_name_folder/__init__.py (with root_agent or agent.root_agent in the package)
|
32
|
+
c) agents_dir/agent_name_folder/agent.py (where agent.py has root_agent)
|
33
|
+
"""
|
34
|
+
|
35
|
+
def __init__(self, agents_dir: str):
|
36
|
+
self.agents_dir = agents_dir.rstrip("/")
|
37
|
+
self._original_sys_path = None
|
38
|
+
self._agent_cache: dict[str, BaseAgent] = {}
|
39
|
+
|
40
|
+
def _load_from_module_or_package(self, agent_name: str) -> BaseAgent:
|
41
|
+
# Load for case: Import "<agent_name>" (as a package or module)
|
42
|
+
# Covers structures:
|
43
|
+
# a) agents_dir/agent_name.py (with root_agent or agent.root_agent in it)
|
44
|
+
# b) agents_dir/agent_name_folder/__init__.py (with root_agent or agent.root_agent in the package)
|
45
|
+
try:
|
46
|
+
module_candidate = importlib.import_module(agent_name)
|
47
|
+
# Check for "root_agent" directly in "<agent_name>" module/package
|
48
|
+
if hasattr(module_candidate, "root_agent"):
|
49
|
+
logger.debug("Found root_agent directly in %s", agent_name)
|
50
|
+
return module_candidate.root_agent
|
51
|
+
# Check for "<agent_name>.agent.root_agent" structure (e.g. agent_name is a package,
|
52
|
+
# and it has an 'agent' submodule/attribute which in turn has 'root_agent')
|
53
|
+
if hasattr(module_candidate, "agent") and hasattr(
|
54
|
+
module_candidate.agent, "root_agent"
|
55
|
+
):
|
56
|
+
logger.debug("Found root_agent in %s.agent attribute", agent_name)
|
57
|
+
if isinstance(module_candidate.agent, BaseAgent):
|
58
|
+
return module_candidate.agent.root_agent
|
59
|
+
else:
|
60
|
+
logger.warning(
|
61
|
+
"Root agent found is not an instance of BaseAgent. But a type %s",
|
62
|
+
type(module_candidate.agent),
|
63
|
+
)
|
64
|
+
except ModuleNotFoundError:
|
65
|
+
logger.debug("Module %s itself not found.", agent_name)
|
66
|
+
# Re-raise as ValueError to be caught by the final error message construction
|
67
|
+
raise ValueError(
|
68
|
+
f"Module {agent_name} not found during import attempts."
|
69
|
+
) from None
|
70
|
+
except ImportError as e:
|
71
|
+
logger.warning("Error importing %s: %s", agent_name, e)
|
72
|
+
|
73
|
+
return None
|
74
|
+
|
75
|
+
def _load_from_submodule(self, agent_name: str) -> BaseAgent:
|
76
|
+
# Load for case: Import "<agent_name>.agent" and look for "root_agent"
|
77
|
+
# Covers structure: agents_dir/agent_name_folder/agent.py (where agent.py has root_agent)
|
78
|
+
try:
|
79
|
+
module_candidate = importlib.import_module(f"{agent_name}.agent")
|
80
|
+
if hasattr(module_candidate, "root_agent"):
|
81
|
+
logger.debug("Found root_agent in %s.agent", agent_name)
|
82
|
+
if isinstance(module_candidate.root_agent, BaseAgent):
|
83
|
+
return module_candidate.root_agent
|
84
|
+
else:
|
85
|
+
logger.warning(
|
86
|
+
"Root agent found is not an instance of BaseAgent. But a type %s",
|
87
|
+
type(module_candidate.root_agent),
|
88
|
+
)
|
89
|
+
except ModuleNotFoundError:
|
90
|
+
logger.debug(
|
91
|
+
"Module %s.agent not found, trying next pattern.", agent_name
|
92
|
+
)
|
93
|
+
except ImportError as e:
|
94
|
+
logger.warning("Error importing %s.agent: %s", agent_name, e)
|
95
|
+
|
96
|
+
return None
|
97
|
+
|
98
|
+
def _perform_load(self, agent_name: str) -> BaseAgent:
|
99
|
+
"""Internal logic to load an agent"""
|
100
|
+
# Add self.agents_dir to sys.path
|
101
|
+
if self.agents_dir not in sys.path:
|
102
|
+
sys.path.insert(0, self.agents_dir)
|
103
|
+
|
104
|
+
logger.debug(
|
105
|
+
"Loading .env for agent %s from %s", agent_name, self.agents_dir
|
106
|
+
)
|
107
|
+
envs.load_dotenv_for_agent(agent_name, str(self.agents_dir))
|
108
|
+
|
109
|
+
root_agent = self._load_from_module_or_package(agent_name)
|
110
|
+
if root_agent:
|
111
|
+
return root_agent
|
112
|
+
|
113
|
+
root_agent = self._load_from_submodule(agent_name)
|
114
|
+
if root_agent:
|
115
|
+
return root_agent
|
116
|
+
|
117
|
+
# If no root_agent was found by any pattern
|
118
|
+
raise ValueError(
|
119
|
+
f"No root_agent found for '{agent_name}'. Searched in"
|
120
|
+
f" '{agent_name}.agent.root_agent', '{agent_name}.root_agent', and"
|
121
|
+
f" via an 'agent' attribute within the '{agent_name}' module/package."
|
122
|
+
f" Ensure '{self.agents_dir}/{agent_name}' is structured correctly,"
|
123
|
+
" an .env file can be loaded if present, and a root_agent is"
|
124
|
+
" exposed."
|
125
|
+
)
|
126
|
+
|
127
|
+
def load_agent(self, agent_name: str) -> BaseAgent:
|
128
|
+
"""Load an agent module (with caching & .env) and return its root_agent (asynchronously)."""
|
129
|
+
if agent_name in self._agent_cache:
|
130
|
+
logger.debug("Returning cached agent for %s (async)", agent_name)
|
131
|
+
return self._agent_cache[agent_name]
|
132
|
+
|
133
|
+
logger.debug("Loading agent %s - not in cache.", agent_name)
|
134
|
+
# Assumes this method is called when the context manager (`with self:`) is active
|
135
|
+
agent = self._perform_load(agent_name)
|
136
|
+
self._agent_cache[agent_name] = agent
|
137
|
+
return agent
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# Copyright 2025 Google LLC
|
2
|
+
#
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
4
|
+
# you may not use this file except in compliance with the License.
|
5
|
+
# You may obtain a copy of the License at
|
6
|
+
#
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
8
|
+
#
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
12
|
+
# See the License for the specific language governing permissions and
|
13
|
+
# limitations under the License.
|
14
|
+
|
15
|
+
import asyncio
|
16
|
+
import logging
|
17
|
+
from typing import List
|
18
|
+
|
19
|
+
from ...runners import Runner
|
20
|
+
|
21
|
+
logger = logging.getLogger("google_adk." + __name__)
|
22
|
+
|
23
|
+
|
24
|
+
async def close_runners(runners: List[Runner]) -> None:
|
25
|
+
cleanup_tasks = [asyncio.create_task(runner.close()) for runner in runners]
|
26
|
+
if cleanup_tasks:
|
27
|
+
# Wait for all cleanup tasks with timeout
|
28
|
+
done, pending = await asyncio.wait(
|
29
|
+
cleanup_tasks,
|
30
|
+
timeout=30.0, # 30 second timeout for cleanup
|
31
|
+
return_when=asyncio.ALL_COMPLETED,
|
32
|
+
)
|
33
|
+
|
34
|
+
# If any tasks are still pending, log it
|
35
|
+
if pending:
|
36
|
+
logger.warning(
|
37
|
+
"%s runner close tasks didn't complete in time", len(pending)
|
38
|
+
)
|
39
|
+
for task in pending:
|
40
|
+
task.cancel()
|
google/adk/cli/utils/evals.py
CHANGED
@@ -12,7 +12,8 @@
|
|
12
12
|
# See the License for the specific language governing permissions and
|
13
13
|
# limitations under the License.
|
14
14
|
|
15
|
-
from typing import Any
|
15
|
+
from typing import Any
|
16
|
+
from typing import Tuple
|
16
17
|
|
17
18
|
from deprecated import deprecated
|
18
19
|
from google.genai import types as genai_types
|
google/adk/cli/utils/logs.py
CHANGED
@@ -12,9 +12,10 @@
|
|
12
12
|
# See the License for the specific language governing permissions and
|
13
13
|
# limitations under the License.
|
14
14
|
|
15
|
+
from __future__ import annotations
|
16
|
+
|
15
17
|
import logging
|
16
18
|
import os
|
17
|
-
import sys
|
18
19
|
import tempfile
|
19
20
|
import time
|
20
21
|
|
@@ -27,14 +28,8 @@ def setup_adk_logger(level=logging.INFO):
|
|
27
28
|
# Configure the root logger format and level.
|
28
29
|
logging.basicConfig(level=level, format=LOGGING_FORMAT)
|
29
30
|
|
30
|
-
# Set up adk_logger and log to stderr.
|
31
|
-
handler = logging.StreamHandler(sys.stderr)
|
32
|
-
handler.setLevel(level)
|
33
|
-
handler.setFormatter(logging.Formatter(LOGGING_FORMAT))
|
34
|
-
|
35
31
|
adk_logger = logging.getLogger('google_adk')
|
36
32
|
adk_logger.setLevel(level)
|
37
|
-
adk_logger.addHandler(handler)
|
38
33
|
|
39
34
|
|
40
35
|
def log_to_tmp_folder(
|