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.
Files changed (94) hide show
  1. google/adk/agents/callback_context.py +2 -1
  2. google/adk/agents/readonly_context.py +3 -1
  3. google/adk/auth/auth_credential.py +4 -1
  4. google/adk/cli/browser/index.html +4 -4
  5. google/adk/cli/browser/{main-QOEMUXM4.js → main-PKDNKWJE.js} +59 -59
  6. google/adk/cli/browser/polyfills-B6TNHZQ6.js +17 -0
  7. google/adk/cli/cli.py +3 -2
  8. google/adk/cli/cli_eval.py +6 -85
  9. google/adk/cli/cli_tools_click.py +39 -10
  10. google/adk/cli/fast_api.py +53 -184
  11. google/adk/cli/utils/agent_loader.py +137 -0
  12. google/adk/cli/utils/cleanup.py +40 -0
  13. google/adk/cli/utils/evals.py +2 -1
  14. google/adk/cli/utils/logs.py +2 -7
  15. google/adk/code_executors/code_execution_utils.py +2 -1
  16. google/adk/code_executors/container_code_executor.py +0 -1
  17. google/adk/code_executors/vertex_ai_code_executor.py +6 -8
  18. google/adk/evaluation/eval_case.py +3 -1
  19. google/adk/evaluation/eval_metrics.py +74 -0
  20. google/adk/evaluation/eval_result.py +86 -0
  21. google/adk/evaluation/eval_set.py +2 -0
  22. google/adk/evaluation/eval_set_results_manager.py +47 -0
  23. google/adk/evaluation/eval_sets_manager.py +2 -1
  24. google/adk/evaluation/evaluator.py +2 -0
  25. google/adk/evaluation/local_eval_set_results_manager.py +113 -0
  26. google/adk/evaluation/local_eval_sets_manager.py +4 -4
  27. google/adk/evaluation/response_evaluator.py +2 -1
  28. google/adk/evaluation/trajectory_evaluator.py +3 -2
  29. google/adk/examples/base_example_provider.py +1 -0
  30. google/adk/flows/llm_flows/base_llm_flow.py +4 -6
  31. google/adk/flows/llm_flows/contents.py +3 -1
  32. google/adk/flows/llm_flows/instructions.py +7 -77
  33. google/adk/flows/llm_flows/single_flow.py +1 -1
  34. google/adk/models/base_llm.py +2 -1
  35. google/adk/models/base_llm_connection.py +2 -0
  36. google/adk/models/google_llm.py +4 -1
  37. google/adk/models/lite_llm.py +3 -2
  38. google/adk/models/llm_response.py +2 -1
  39. google/adk/runners.py +36 -4
  40. google/adk/sessions/_session_util.py +2 -1
  41. google/adk/sessions/database_session_service.py +5 -8
  42. google/adk/sessions/vertex_ai_session_service.py +28 -13
  43. google/adk/telemetry.py +4 -2
  44. google/adk/tools/agent_tool.py +1 -1
  45. google/adk/tools/apihub_tool/apihub_toolset.py +1 -1
  46. google/adk/tools/apihub_tool/clients/apihub_client.py +10 -3
  47. google/adk/tools/apihub_tool/clients/secret_client.py +1 -0
  48. google/adk/tools/application_integration_tool/application_integration_toolset.py +6 -2
  49. google/adk/tools/application_integration_tool/clients/connections_client.py +8 -1
  50. google/adk/tools/application_integration_tool/clients/integration_client.py +3 -1
  51. google/adk/tools/application_integration_tool/integration_connector_tool.py +1 -1
  52. google/adk/tools/base_toolset.py +40 -2
  53. google/adk/tools/bigquery/__init__.py +38 -0
  54. google/adk/tools/bigquery/bigquery_credentials.py +217 -0
  55. google/adk/tools/bigquery/bigquery_tool.py +116 -0
  56. google/adk/tools/bigquery/bigquery_toolset.py +86 -0
  57. google/adk/tools/bigquery/client.py +33 -0
  58. google/adk/tools/bigquery/metadata_tool.py +249 -0
  59. google/adk/tools/bigquery/query_tool.py +76 -0
  60. google/adk/tools/function_parameter_parse_util.py +7 -0
  61. google/adk/tools/function_tool.py +33 -3
  62. google/adk/tools/get_user_choice_tool.py +1 -0
  63. google/adk/tools/google_api_tool/__init__.py +17 -11
  64. google/adk/tools/google_api_tool/google_api_tool.py +1 -1
  65. google/adk/tools/google_api_tool/google_api_toolset.py +0 -14
  66. google/adk/tools/google_api_tool/google_api_toolsets.py +8 -2
  67. google/adk/tools/google_search_tool.py +2 -2
  68. google/adk/tools/mcp_tool/conversion_utils.py +6 -2
  69. google/adk/tools/mcp_tool/mcp_session_manager.py +62 -188
  70. google/adk/tools/mcp_tool/mcp_tool.py +27 -24
  71. google/adk/tools/mcp_tool/mcp_toolset.py +76 -131
  72. google/adk/tools/openapi_tool/auth/credential_exchangers/base_credential_exchanger.py +1 -3
  73. google/adk/tools/openapi_tool/auth/credential_exchangers/service_account_exchanger.py +6 -7
  74. google/adk/tools/openapi_tool/common/common.py +5 -1
  75. google/adk/tools/openapi_tool/openapi_spec_parser/__init__.py +7 -2
  76. google/adk/tools/openapi_tool/openapi_spec_parser/openapi_toolset.py +2 -7
  77. google/adk/tools/openapi_tool/openapi_spec_parser/operation_parser.py +5 -1
  78. google/adk/tools/openapi_tool/openapi_spec_parser/rest_api_tool.py +11 -1
  79. google/adk/tools/toolbox_toolset.py +31 -3
  80. google/adk/utils/__init__.py +13 -0
  81. google/adk/utils/instructions_utils.py +131 -0
  82. google/adk/version.py +1 -1
  83. {google_adk-1.0.0.dist-info → google_adk-1.1.1.dist-info}/METADATA +12 -15
  84. {google_adk-1.0.0.dist-info → google_adk-1.1.1.dist-info}/RECORD +87 -78
  85. google/adk/agents/base_agent.py.orig +0 -330
  86. google/adk/cli/browser/polyfills-FFHMD2TL.js +0 -18
  87. google/adk/cli/fast_api.py.orig +0 -822
  88. google/adk/memory/base_memory_service.py.orig +0 -76
  89. google/adk/models/google_llm.py.orig +0 -305
  90. google/adk/tools/_built_in_code_execution_tool.py +0 -70
  91. google/adk/tools/mcp_tool/mcp_session_manager.py.orig +0 -322
  92. {google_adk-1.0.0.dist-info → google_adk-1.1.1.dist-info}/WHEEL +0 -0
  93. {google_adk-1.0.0.dist-info → google_adk-1.1.1.dist-info}/entry_points.txt +0 -0
  94. {google_adk-1.0.0.dist-info → google_adk-1.1.1.dist-info}/licenses/LICENSE +0 -0
@@ -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
- agent_dir: str,
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("", agent_dir)
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
- # During shutdown, properly clean up all toolsets
262
- logger.info(
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(agent_dir=agent_dir)
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("", agent_dir)
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() / agent_dir
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, agent_dir, eval_set_id) -> str:
400
+ def _get_eval_set_file_path(app_name, agents_dir, eval_set_id) -> str:
463
401
  return os.path.join(
464
- agent_dir,
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
- await _get_root_agent_async(app_name)
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 = await _get_root_agent_async(app_name)
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
- timestamp = time.time()
598
- eval_set_result_name = app_name + "_" + eval_set_id + "_" + str(timestamp)
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
- eval_result = EvalSetResult.model_validate_json(eval_result_data)
650
- return eval_result
651
- except ValidationError as e:
652
- logger.exception("get_eval_result validation error: %s", e)
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
- app_eval_history_directory = os.path.join(
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
- app_id = agent_engine_id if agent_engine_id else req.app_name
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=app_id, user_id=req.user_id, session_id=req.session_id
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
- app_id = agent_engine_id if agent_engine_id else req.app_name
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=app_id, user_id=req.user_id, session_id=req.session_id
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
- app_id = agent_engine_id if agent_engine_id else app_name
726
+ app_name = agent_engine_id if agent_engine_id else app_name
836
727
  session = await session_service.get_session(
837
- app_name=app_id, user_id=user_id, session_id=session_id
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 = await _get_root_agent_async(app_name)
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
- app_id = agent_engine_id if agent_engine_id else app_name
783
+ app_name = agent_engine_id if agent_engine_id else app_name
893
784
  session = await session_service.get_session(
894
- app_name=app_id, user_id=user_id, session_id=session_id
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), agent_dir)
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 = await _get_root_agent_async(app_name)
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 redirect_to_dev_ui():
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 dev_ui():
999
- return FileResponse(BASE_DIR / "browser/index.html")
865
+ async def redirect_dev_ui_add_slash():
866
+ return RedirectResponse("/dev-ui/")
1000
867
 
1001
868
  app.mount(
1002
- "/", StaticFiles(directory=ANGULAR_DIST_PATH, html=True), name="static"
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()
@@ -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, Tuple
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
@@ -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(
@@ -19,7 +19,8 @@ import binascii
19
19
  import copy
20
20
  import dataclasses
21
21
  import re
22
- from typing import List, Optional
22
+ from typing import List
23
+ from typing import Optional
23
24
 
24
25
  from google.genai import types
25
26
 
@@ -27,7 +27,6 @@ from .base_code_executor import BaseCodeExecutor
27
27
  from .code_execution_utils import CodeExecutionInput
28
28
  from .code_execution_utils import CodeExecutionResult
29
29
 
30
-
31
30
  DEFAULT_IMAGE_TAG = 'adk-code-executor:latest'
32
31
 
33
32