google-adk 1.7.0__py3-none-any.whl → 1.8.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. google/adk/a2a/converters/request_converter.py +1 -2
  2. google/adk/a2a/logs/log_utils.py +1 -2
  3. google/adk/a2a/utils/__init__.py +0 -0
  4. google/adk/a2a/utils/agent_card_builder.py +544 -0
  5. google/adk/a2a/utils/agent_to_a2a.py +118 -0
  6. google/adk/agents/base_agent.py +6 -1
  7. google/adk/agents/config_schemas/AgentConfig.json +22 -0
  8. google/adk/agents/live_request_queue.py +15 -0
  9. google/adk/agents/llm_agent.py +11 -0
  10. google/adk/agents/loop_agent.py +6 -1
  11. google/adk/agents/remote_a2a_agent.py +2 -2
  12. google/adk/artifacts/gcs_artifact_service.py +86 -18
  13. google/adk/cli/browser/index.html +2 -2
  14. google/adk/cli/browser/{main-SRBSE46V.js → main-W7QZBYAR.js} +139 -139
  15. google/adk/cli/cli_eval.py +87 -12
  16. google/adk/cli/cli_tools_click.py +143 -82
  17. google/adk/cli/fast_api.py +136 -95
  18. google/adk/evaluation/eval_metrics.py +4 -0
  19. google/adk/evaluation/eval_sets_manager.py +5 -1
  20. google/adk/evaluation/final_response_match_v2.py +2 -2
  21. google/adk/evaluation/gcs_eval_sets_manager.py +2 -1
  22. google/adk/evaluation/local_eval_service.py +2 -2
  23. google/adk/evaluation/local_eval_set_results_manager.py +2 -2
  24. google/adk/evaluation/local_eval_sets_manager.py +1 -1
  25. google/adk/evaluation/metric_evaluator_registry.py +16 -6
  26. google/adk/evaluation/vertex_ai_eval_facade.py +7 -1
  27. google/adk/events/event.py +7 -2
  28. google/adk/flows/llm_flows/base_llm_flow.py +25 -6
  29. google/adk/flows/llm_flows/functions.py +13 -19
  30. google/adk/memory/in_memory_memory_service.py +1 -1
  31. google/adk/memory/vertex_ai_memory_bank_service.py +12 -10
  32. google/adk/models/anthropic_llm.py +2 -1
  33. google/adk/models/base_llm_connection.py +2 -0
  34. google/adk/models/gemini_llm_connection.py +17 -6
  35. google/adk/models/google_llm.py +35 -5
  36. google/adk/models/lite_llm.py +31 -18
  37. google/adk/sessions/database_session_service.py +25 -24
  38. google/adk/sessions/vertex_ai_session_service.py +13 -5
  39. google/adk/tools/__init__.py +2 -0
  40. google/adk/tools/_automatic_function_calling_util.py +20 -2
  41. google/adk/tools/agent_tool.py +14 -3
  42. google/adk/tools/base_toolset.py +22 -0
  43. google/adk/tools/bigquery/metadata_tool.py +2 -0
  44. google/adk/tools/bigquery/query_tool.py +15 -1
  45. google/adk/tools/computer_use/__init__.py +13 -0
  46. google/adk/tools/computer_use/base_computer.py +265 -0
  47. google/adk/tools/computer_use/computer_use_tool.py +166 -0
  48. google/adk/tools/computer_use/computer_use_toolset.py +220 -0
  49. google/adk/tools/exit_loop_tool.py +1 -0
  50. google/adk/tools/langchain_tool.py +14 -3
  51. google/adk/tools/openapi_tool/openapi_spec_parser/openapi_spec_parser.py +5 -0
  52. google/adk/version.py +1 -1
  53. {google_adk-1.7.0.dist-info → google_adk-1.8.0.dist-info}/METADATA +2 -1
  54. {google_adk-1.7.0.dist-info → google_adk-1.8.0.dist-info}/RECORD +57 -50
  55. {google_adk-1.7.0.dist-info → google_adk-1.8.0.dist-info}/WHEEL +0 -0
  56. {google_adk-1.7.0.dist-info → google_adk-1.8.0.dist-info}/entry_points.txt +0 -0
  57. {google_adk-1.7.0.dist-info → google_adk-1.8.0.dist-info}/licenses/LICENSE +0 -0
@@ -20,18 +20,21 @@ import json
20
20
  import logging
21
21
  import os
22
22
  from pathlib import Path
23
+ import shutil
23
24
  import time
24
25
  import traceback
25
26
  import typing
26
27
  from typing import Any
27
28
  from typing import List
28
29
  from typing import Literal
30
+ from typing import Mapping
29
31
  from typing import Optional
30
32
 
31
33
  import click
32
34
  from fastapi import FastAPI
33
35
  from fastapi import HTTPException
34
36
  from fastapi import Query
37
+ from fastapi import UploadFile
35
38
  from fastapi.middleware.cors import CORSMiddleware
36
39
  from fastapi.responses import RedirectResponse
37
40
  from fastapi.responses import StreamingResponse
@@ -41,7 +44,6 @@ from fastapi.websockets import WebSocketDisconnect
41
44
  from google.genai import types
42
45
  import graphviz
43
46
  from opentelemetry import trace
44
- from opentelemetry.exporter.cloud_trace import CloudTraceSpanExporter
45
47
  from opentelemetry.sdk.trace import export
46
48
  from opentelemetry.sdk.trace import ReadableSpan
47
49
  from opentelemetry.sdk.trace import TracerProvider
@@ -51,7 +53,6 @@ from starlette.types import Lifespan
51
53
  from typing_extensions import override
52
54
  from watchdog.events import FileSystemEventHandler
53
55
  from watchdog.observers import Observer
54
- import yaml
55
56
 
56
57
  from ..agents import RunConfig
57
58
  from ..agents.live_request_queue import LiveRequest
@@ -61,6 +62,9 @@ from ..artifacts.gcs_artifact_service import GcsArtifactService
61
62
  from ..artifacts.in_memory_artifact_service import InMemoryArtifactService
62
63
  from ..auth.credential_service.in_memory_credential_service import InMemoryCredentialService
63
64
  from ..errors.not_found_error import NotFoundError
65
+ from ..evaluation.base_eval_service import InferenceConfig
66
+ from ..evaluation.base_eval_service import InferenceRequest
67
+ from ..evaluation.constants import MISSING_EVAL_DEPENDENCIES_MESSAGE
64
68
  from ..evaluation.eval_case import EvalCase
65
69
  from ..evaluation.eval_case import SessionInput
66
70
  from ..evaluation.eval_metrics import EvalMetric
@@ -72,9 +76,7 @@ from ..evaluation.local_eval_sets_manager import LocalEvalSetsManager
72
76
  from ..events.event import Event
73
77
  from ..memory.in_memory_memory_service import InMemoryMemoryService
74
78
  from ..memory.vertex_ai_memory_bank_service import VertexAiMemoryBankService
75
- from ..memory.vertex_ai_rag_memory_service import VertexAiRagMemoryService
76
79
  from ..runners import Runner
77
- from ..sessions.database_session_service import DatabaseSessionService
78
80
  from ..sessions.in_memory_session_service import InMemorySessionService
79
81
  from ..sessions.session import Session
80
82
  from ..sessions.vertex_ai_session_service import VertexAiSessionService
@@ -198,6 +200,7 @@ class RunEvalResult(common.BaseModel):
198
200
  final_eval_status: EvalStatus
199
201
  eval_metric_results: list[tuple[EvalMetric, EvalMetricResult]] = Field(
200
202
  deprecated=True,
203
+ default=[],
201
204
  description=(
202
205
  "This field is deprecated, use overall_eval_metric_results instead."
203
206
  ),
@@ -212,18 +215,11 @@ class GetEventGraphResult(common.BaseModel):
212
215
  dot_src: str
213
216
 
214
217
 
215
- class AgentBuildRequest(common.BaseModel):
216
- agent_name: str
217
- agent_type: str
218
- model: str
219
- description: str
220
- instruction: str
221
-
222
-
223
218
  def get_fast_api_app(
224
219
  *,
225
220
  agents_dir: str,
226
221
  session_service_uri: Optional[str] = None,
222
+ session_db_kwargs: Optional[Mapping[str, Any]] = None,
227
223
  artifact_service_uri: Optional[str] = None,
228
224
  memory_service_uri: Optional[str] = None,
229
225
  eval_storage_uri: Optional[str] = None,
@@ -248,6 +244,8 @@ def get_fast_api_app(
248
244
  memory_exporter = InMemoryExporter(session_trace_dict)
249
245
  provider.add_span_processor(export.SimpleSpanProcessor(memory_exporter))
250
246
  if trace_to_cloud:
247
+ from opentelemetry.exporter.cloud_trace import CloudTraceSpanExporter
248
+
251
249
  envs.load_dotenv_for_agent("", agents_dir)
252
250
  if project_id := os.environ.get("GOOGLE_CLOUD_PROJECT", None):
253
251
  processor = export.BatchSpanProcessor(
@@ -304,9 +302,36 @@ def get_fast_api_app(
304
302
  eval_sets_manager = LocalEvalSetsManager(agents_dir=agents_dir)
305
303
  eval_set_results_manager = LocalEvalSetResultsManager(agents_dir=agents_dir)
306
304
 
305
+ def _parse_agent_engine_resource_name(agent_engine_id_or_resource_name):
306
+ if not agent_engine_id_or_resource_name:
307
+ raise click.ClickException(
308
+ "Agent engine resource name or resource id can not be empty."
309
+ )
310
+
311
+ # "projects/my-project/locations/us-central1/reasoningEngines/1234567890",
312
+ if "/" in agent_engine_id_or_resource_name:
313
+ # Validate resource name.
314
+ if len(agent_engine_id_or_resource_name.split("/")) != 6:
315
+ raise click.ClickException(
316
+ "Agent engine resource name is mal-formatted. It should be of"
317
+ " format :"
318
+ " projects/{project_id}/locations/{location}/reasoningEngines/{resource_id}"
319
+ )
320
+ project = agent_engine_id_or_resource_name.split("/")[1]
321
+ location = agent_engine_id_or_resource_name.split("/")[3]
322
+ agent_engine_id = agent_engine_id_or_resource_name.split("/")[-1]
323
+ else:
324
+ envs.load_dotenv_for_agent("", agents_dir)
325
+ project = os.environ["GOOGLE_CLOUD_PROJECT"]
326
+ location = os.environ["GOOGLE_CLOUD_LOCATION"]
327
+ agent_engine_id = agent_engine_id_or_resource_name
328
+ return project, location, agent_engine_id
329
+
307
330
  # Build the Memory service
308
331
  if memory_service_uri:
309
332
  if memory_service_uri.startswith("rag://"):
333
+ from ..memory.vertex_ai_rag_memory_service import VertexAiRagMemoryService
334
+
310
335
  rag_corpus = memory_service_uri.split("://")[1]
311
336
  if not rag_corpus:
312
337
  raise click.ClickException("Rag corpus can not be empty.")
@@ -315,13 +340,13 @@ def get_fast_api_app(
315
340
  rag_corpus=f'projects/{os.environ["GOOGLE_CLOUD_PROJECT"]}/locations/{os.environ["GOOGLE_CLOUD_LOCATION"]}/ragCorpora/{rag_corpus}'
316
341
  )
317
342
  elif memory_service_uri.startswith("agentengine://"):
318
- agent_engine_id = memory_service_uri.split("://")[1]
319
- if not agent_engine_id:
320
- raise click.ClickException("Agent engine id can not be empty.")
321
- envs.load_dotenv_for_agent("", agents_dir)
343
+ agent_engine_id_or_resource_name = memory_service_uri.split("://")[1]
344
+ project, location, agent_engine_id = _parse_agent_engine_resource_name(
345
+ agent_engine_id_or_resource_name
346
+ )
322
347
  memory_service = VertexAiMemoryBankService(
323
- project=os.environ["GOOGLE_CLOUD_PROJECT"],
324
- location=os.environ["GOOGLE_CLOUD_LOCATION"],
348
+ project=project,
349
+ location=location,
325
350
  agent_engine_id=agent_engine_id,
326
351
  )
327
352
  else:
@@ -334,18 +359,24 @@ def get_fast_api_app(
334
359
  # Build the Session service
335
360
  if session_service_uri:
336
361
  if session_service_uri.startswith("agentengine://"):
337
- # Create vertex session service
338
- agent_engine_id = session_service_uri.split("://")[1]
339
- if not agent_engine_id:
340
- raise click.ClickException("Agent engine id can not be empty.")
341
- envs.load_dotenv_for_agent("", agents_dir)
362
+ agent_engine_id_or_resource_name = session_service_uri.split("://")[1]
363
+ project, location, agent_engine_id = _parse_agent_engine_resource_name(
364
+ agent_engine_id_or_resource_name
365
+ )
342
366
  session_service = VertexAiSessionService(
343
- project=os.environ["GOOGLE_CLOUD_PROJECT"],
344
- location=os.environ["GOOGLE_CLOUD_LOCATION"],
367
+ project=project,
368
+ location=location,
345
369
  agent_engine_id=agent_engine_id,
346
370
  )
347
371
  else:
348
- session_service = DatabaseSessionService(db_url=session_service_uri)
372
+ from ..sessions.database_session_service import DatabaseSessionService
373
+
374
+ # Database session additional settings
375
+ if session_db_kwargs is None:
376
+ session_db_kwargs = {}
377
+ session_service = DatabaseSessionService(
378
+ db_url=session_service_uri, **session_db_kwargs
379
+ )
349
380
  else:
350
381
  session_service = InMemorySessionService()
351
382
 
@@ -644,63 +675,66 @@ def get_fast_api_app(
644
675
  app_name: str, eval_set_id: str, req: RunEvalRequest
645
676
  ) -> list[RunEvalResult]:
646
677
  """Runs an eval given the details in the eval request."""
647
- from .cli_eval import run_evals
648
-
649
678
  # Create a mapping from eval set file to all the evals that needed to be
650
679
  # run.
651
- eval_set = eval_sets_manager.get_eval_set(app_name, eval_set_id)
680
+ try:
681
+ from ..evaluation.local_eval_service import LocalEvalService
682
+ from .cli_eval import _collect_eval_results
683
+ from .cli_eval import _collect_inferences
652
684
 
653
- if not eval_set:
654
- raise HTTPException(
655
- status_code=400, detail=f"Eval set `{eval_set_id}` not found."
656
- )
685
+ eval_set = eval_sets_manager.get_eval_set(app_name, eval_set_id)
657
686
 
658
- if req.eval_ids:
659
- eval_cases = [e for e in eval_set.eval_cases if e.eval_id in req.eval_ids]
660
- eval_set_to_evals = {eval_set_id: eval_cases}
661
- else:
662
- logger.info("Eval ids to run list is empty. We will run all eval cases.")
663
- eval_set_to_evals = {eval_set_id: eval_set.eval_cases}
687
+ if not eval_set:
688
+ raise HTTPException(
689
+ status_code=400, detail=f"Eval set `{eval_set_id}` not found."
690
+ )
664
691
 
665
- root_agent = agent_loader.load_agent(app_name)
666
- run_eval_results = []
667
- eval_case_results = []
668
- try:
669
- async for eval_case_result in run_evals(
670
- eval_set_to_evals,
671
- root_agent,
672
- getattr(root_agent, "reset_data", None),
673
- req.eval_metrics,
692
+ root_agent = agent_loader.load_agent(app_name)
693
+
694
+ eval_case_results = []
695
+
696
+ eval_service = LocalEvalService(
697
+ root_agent=root_agent,
698
+ eval_sets_manager=eval_sets_manager,
699
+ eval_set_results_manager=eval_set_results_manager,
674
700
  session_service=session_service,
675
701
  artifact_service=artifact_service,
676
- ):
677
- run_eval_results.append(
678
- RunEvalResult(
679
- app_name=app_name,
680
- eval_set_file=eval_case_result.eval_set_file,
681
- eval_set_id=eval_set_id,
682
- eval_id=eval_case_result.eval_id,
683
- final_eval_status=eval_case_result.final_eval_status,
684
- eval_metric_results=eval_case_result.eval_metric_results,
685
- overall_eval_metric_results=eval_case_result.overall_eval_metric_results,
686
- eval_metric_result_per_invocation=eval_case_result.eval_metric_result_per_invocation,
687
- user_id=eval_case_result.user_id,
688
- session_id=eval_case_result.session_id,
689
- )
690
- )
691
- eval_case_result.session_details = await session_service.get_session(
692
- app_name=app_name,
693
- user_id=eval_case_result.user_id,
694
- session_id=eval_case_result.session_id,
695
- )
696
- eval_case_results.append(eval_case_result)
702
+ )
703
+ inference_request = InferenceRequest(
704
+ app_name=app_name,
705
+ eval_set_id=eval_set.eval_set_id,
706
+ eval_case_ids=req.eval_ids,
707
+ inference_config=InferenceConfig(),
708
+ )
709
+ inference_results = await _collect_inferences(
710
+ inference_requests=[inference_request], eval_service=eval_service
711
+ )
712
+
713
+ eval_case_results = await _collect_eval_results(
714
+ inference_results=inference_results,
715
+ eval_service=eval_service,
716
+ eval_metrics=req.eval_metrics,
717
+ )
697
718
  except ModuleNotFoundError as e:
698
719
  logger.exception("%s", e)
699
- raise HTTPException(status_code=400, detail=str(e)) from e
720
+ raise HTTPException(
721
+ status_code=400, detail=MISSING_EVAL_DEPENDENCIES_MESSAGE
722
+ ) from e
700
723
 
701
- eval_set_results_manager.save_eval_set_result(
702
- app_name, eval_set_id, eval_case_results
703
- )
724
+ run_eval_results = []
725
+ for eval_case_result in eval_case_results:
726
+ run_eval_results.append(
727
+ RunEvalResult(
728
+ eval_set_file=eval_case_result.eval_set_file,
729
+ eval_set_id=eval_set_id,
730
+ eval_id=eval_case_result.eval_id,
731
+ final_eval_status=eval_case_result.final_eval_status,
732
+ overall_eval_metric_results=eval_case_result.overall_eval_metric_results,
733
+ eval_metric_result_per_invocation=eval_case_result.eval_metric_result_per_invocation,
734
+ user_id=eval_case_result.user_id,
735
+ session_id=eval_case_result.session_id,
736
+ )
737
+ )
704
738
 
705
739
  return run_eval_results
706
740
 
@@ -820,26 +854,30 @@ def get_fast_api_app(
820
854
 
821
855
  @working_in_progress("builder_save is not ready for use.")
822
856
  @app.post("/builder/save", response_model_exclude_none=True)
823
- async def builder_build(req: AgentBuildRequest):
857
+ async def builder_build(files: list[UploadFile]) -> bool:
824
858
  base_path = Path.cwd() / agents_dir
825
- agent = {
826
- "agent_class": req.agent_type,
827
- "name": req.agent_name,
828
- "model": req.model,
829
- "description": req.description,
830
- "instruction": f"""{req.instruction}""",
831
- }
832
- try:
833
- agent_dir = os.path.join(base_path, req.agent_name)
834
- os.makedirs(agent_dir, exist_ok=True)
835
- file_path = os.path.join(agent_dir, "root_agent.yaml")
836
- with open(file_path, "w") as file:
837
- yaml.dump(agent, file, default_flow_style=False)
838
- agent_loader.load_agent(agent_name=req.agent_name)
839
- return True
840
- except Exception as e:
841
- logger.exception("Error in builder_build: %s", e)
842
- return False
859
+
860
+ for file in files:
861
+ try:
862
+ # File name format: {app_name}/{agent_name}.yaml
863
+ if not file.filename:
864
+ logger.exception("Agent name is missing in the input files")
865
+ return False
866
+
867
+ agent_name, filename = file.filename.split("/")
868
+
869
+ agent_dir = os.path.join(base_path, agent_name)
870
+ os.makedirs(agent_dir, exist_ok=True)
871
+ file_path = os.path.join(agent_dir, filename)
872
+
873
+ with open(file_path, "wb") as buffer:
874
+ shutil.copyfileobj(file.file, buffer)
875
+
876
+ except Exception as e:
877
+ logger.exception("Error in builder_build: %s", e)
878
+ return False
879
+
880
+ return True
843
881
 
844
882
  @app.post("/run", response_model_exclude_none=True)
845
883
  async def agent_run(req: AgentRunRequest) -> list[Event]:
@@ -857,7 +895,8 @@ def get_fast_api_app(
857
895
  new_message=req.new_message,
858
896
  )
859
897
  ]
860
- logger.info("Generated %s events in agent run: %s", len(events), events)
898
+ logger.info("Generated %s events in agent run", len(events))
899
+ logger.debug("Events generated: %s", events)
861
900
  return events
862
901
 
863
902
  @app.post("/run_sse")
@@ -883,7 +922,7 @@ def get_fast_api_app(
883
922
  ):
884
923
  # Format as SSE data
885
924
  sse_event = event.model_dump_json(exclude_none=True, by_alias=True)
886
- logger.info("Generated event in agent run streaming: %s", sse_event)
925
+ logger.debug("Generated event in agent run streaming: %s", sse_event)
887
926
  yield f"data: {sse_event}\n\n"
888
927
  except Exception as e:
889
928
  logger.exception("Error in event_generator: %s", e)
@@ -1136,7 +1175,9 @@ def get_fast_api_app(
1136
1175
 
1137
1176
  app.mount(
1138
1177
  "/dev-ui/",
1139
- StaticFiles(directory=ANGULAR_DIST_PATH, html=True),
1178
+ StaticFiles(
1179
+ directory=ANGULAR_DIST_PATH, html=True, follow_symlink=True
1180
+ ),
1140
1181
  name="static",
1141
1182
  )
1142
1183
  return app
@@ -36,6 +36,10 @@ class PrebuiltMetrics(Enum):
36
36
 
37
37
  RESPONSE_MATCH_SCORE = "response_match_score"
38
38
 
39
+ SAFETY_V1 = "safety_v1"
40
+
41
+ FINAL_RESPONSE_MATCH_V2 = "final_response_match_v2"
42
+
39
43
 
40
44
  MetricName: TypeAlias = Union[str, PrebuiltMetrics]
41
45
 
@@ -36,7 +36,11 @@ class EvalSetsManager(ABC):
36
36
 
37
37
  @abstractmethod
38
38
  def list_eval_sets(self, app_name: str) -> list[str]:
39
- """Returns a list of EvalSets that belong to the given app_name."""
39
+ """Returns a list of EvalSets that belong to the given app_name.
40
+
41
+ Raises:
42
+ NotFoundError: If the app_name doesn't exist.
43
+ """
40
44
 
41
45
  @abstractmethod
42
46
  def get_eval_case(
@@ -21,7 +21,7 @@ from typing import Optional
21
21
  from typing_extensions import override
22
22
 
23
23
  from ..models.llm_response import LlmResponse
24
- from ..utils.feature_decorator import working_in_progress
24
+ from ..utils.feature_decorator import experimental
25
25
  from .eval_case import Invocation
26
26
  from .eval_metrics import EvalMetric
27
27
  from .evaluator import EvalStatus
@@ -125,7 +125,7 @@ def _parse_critique(response: str) -> Label:
125
125
  return label
126
126
 
127
127
 
128
- @working_in_progress
128
+ @experimental
129
129
  class FinalResponseMatchV2Evaluator(LlmAsJudge):
130
130
  """V2 final response match evaluator which uses an LLM to judge responses.
131
131
 
@@ -23,6 +23,7 @@ from google.cloud import exceptions as cloud_exceptions
23
23
  from google.cloud import storage
24
24
  from typing_extensions import override
25
25
 
26
+ from ..errors.not_found_error import NotFoundError
26
27
  from ._eval_sets_manager_utils import add_eval_case_to_eval_set
27
28
  from ._eval_sets_manager_utils import delete_eval_case_from_eval_set
28
29
  from ._eval_sets_manager_utils import get_eval_case_from_eval_set
@@ -130,7 +131,7 @@ class GcsEvalSetsManager(EvalSetsManager):
130
131
  eval_sets.append(eval_set_id)
131
132
  return sorted(eval_sets)
132
133
  except cloud_exceptions.NotFound as e:
133
- raise ValueError(
134
+ raise NotFoundError(
134
135
  f"App `{app_name}` not found in GCS bucket `{self.bucket_name}`."
135
136
  ) from e
136
137
 
@@ -30,7 +30,7 @@ from ..artifacts.in_memory_artifact_service import InMemoryArtifactService
30
30
  from ..errors.not_found_error import NotFoundError
31
31
  from ..sessions.base_session_service import BaseSessionService
32
32
  from ..sessions.in_memory_session_service import InMemorySessionService
33
- from ..utils.feature_decorator import working_in_progress
33
+ from ..utils.feature_decorator import experimental
34
34
  from .base_eval_service import BaseEvalService
35
35
  from .base_eval_service import EvaluateConfig
36
36
  from .base_eval_service import EvaluateRequest
@@ -60,7 +60,7 @@ def _get_session_id() -> str:
60
60
  return f'{EVAL_SESSION_ID_PREFIX}{str(uuid.uuid4())}'
61
61
 
62
62
 
63
- @working_in_progress("Incomplete feature, don't use yet")
63
+ @experimental
64
64
  class LocalEvalService(BaseEvalService):
65
65
  """An implementation of BaseEvalService, that runs the evals locally."""
66
66
 
@@ -60,7 +60,7 @@ class LocalEvalSetResultsManager(EvalSetResultsManager):
60
60
  eval_set_result.eval_set_result_name + _EVAL_SET_RESULT_FILE_EXTENSION,
61
61
  )
62
62
  logger.info("Writing eval result to file: %s", eval_set_result_file_path)
63
- with open(eval_set_result_file_path, "w") as f:
63
+ with open(eval_set_result_file_path, "w", encoding="utf-8") as f:
64
64
  f.write(json.dumps(eval_set_result_json, indent=2))
65
65
 
66
66
  @override
@@ -78,7 +78,7 @@ class LocalEvalSetResultsManager(EvalSetResultsManager):
78
78
  )
79
79
  if not os.path.exists(maybe_eval_result_file_path):
80
80
  raise NotFoundError(f"Eval set result `{eval_set_result_id}` not found.")
81
- with open(maybe_eval_result_file_path, "r") as file:
81
+ with open(maybe_eval_result_file_path, "r", encoding="utf-8") as file:
82
82
  eval_result_data = json.load(file)
83
83
  return EvalSetResult.model_validate_json(eval_result_data)
84
84
 
@@ -315,7 +315,7 @@ class LocalEvalSetsManager(EvalSetsManager):
315
315
  )
316
316
 
317
317
  def _write_eval_set_to_path(self, eval_set_path: str, eval_set: EvalSet):
318
- with open(eval_set_path, "w") as f:
318
+ with open(eval_set_path, "w", encoding="utf-8") as f:
319
319
  f.write(eval_set.model_dump_json(indent=2))
320
320
 
321
321
  def _save_eval_set(self, app_name: str, eval_set_id: str, eval_set: EvalSet):
@@ -21,7 +21,9 @@ from .eval_metrics import EvalMetric
21
21
  from .eval_metrics import MetricName
22
22
  from .eval_metrics import PrebuiltMetrics
23
23
  from .evaluator import Evaluator
24
+ from .final_response_match_v2 import FinalResponseMatchV2Evaluator
24
25
  from .response_evaluator import ResponseEvaluator
26
+ from .safety_evaluator import SafetyEvaluatorV1
25
27
  from .trajectory_evaluator import TrajectoryEvaluator
26
28
 
27
29
  logger = logging.getLogger("google_adk." + __name__)
@@ -71,16 +73,24 @@ def _get_default_metric_evaluator_registry() -> MetricEvaluatorRegistry:
71
73
  metric_evaluator_registry = MetricEvaluatorRegistry()
72
74
 
73
75
  metric_evaluator_registry.register_evaluator(
74
- metric_name=PrebuiltMetrics.TOOL_TRAJECTORY_AVG_SCORE,
75
- evaluator=type(TrajectoryEvaluator),
76
+ metric_name=PrebuiltMetrics.TOOL_TRAJECTORY_AVG_SCORE.value,
77
+ evaluator=TrajectoryEvaluator,
76
78
  )
77
79
  metric_evaluator_registry.register_evaluator(
78
- metric_name=PrebuiltMetrics.RESPONSE_EVALUATION_SCORE,
79
- evaluator=type(ResponseEvaluator),
80
+ metric_name=PrebuiltMetrics.RESPONSE_EVALUATION_SCORE.value,
81
+ evaluator=ResponseEvaluator,
80
82
  )
81
83
  metric_evaluator_registry.register_evaluator(
82
- metric_name=PrebuiltMetrics.RESPONSE_MATCH_SCORE,
83
- evaluator=type(ResponseEvaluator),
84
+ metric_name=PrebuiltMetrics.RESPONSE_MATCH_SCORE.value,
85
+ evaluator=ResponseEvaluator,
86
+ )
87
+ metric_evaluator_registry.register_evaluator(
88
+ metric_name=PrebuiltMetrics.SAFETY_V1.value,
89
+ evaluator=SafetyEvaluatorV1,
90
+ )
91
+ metric_evaluator_registry.register_evaluator(
92
+ metric_name=PrebuiltMetrics.FINAL_RESPONSE_MATCH_V2.value,
93
+ evaluator=FinalResponseMatchV2Evaluator,
84
94
  )
85
95
 
86
96
  return metric_evaluator_registry
@@ -14,6 +14,7 @@
14
14
 
15
15
  from __future__ import annotations
16
16
 
17
+ import math
17
18
  import os
18
19
  from typing import Optional
19
20
 
@@ -112,7 +113,12 @@ class _VertexAiEvalFacade(Evaluator):
112
113
  return ""
113
114
 
114
115
  def _get_score(self, eval_result) -> Optional[float]:
115
- if eval_result and eval_result.summary_metrics:
116
+ if (
117
+ eval_result
118
+ and eval_result.summary_metrics
119
+ and isinstance(eval_result.summary_metrics[0].mean_score, float)
120
+ and not math.isnan(eval_result.summary_metrics[0].mean_score)
121
+ ):
116
122
  return eval_result.summary_metrics[0].mean_score
117
123
 
118
124
  return None
@@ -42,7 +42,6 @@ class Event(LlmResponse):
42
42
  branch: The branch of the event.
43
43
  id: The unique identifier of the event.
44
44
  timestamp: The timestamp of the event.
45
- is_final_response: Whether the event is the final response of the agent.
46
45
  get_function_calls: Returns the function calls in the event.
47
46
  """
48
47
 
@@ -92,7 +91,13 @@ class Event(LlmResponse):
92
91
  self.id = Event.new_id()
93
92
 
94
93
  def is_final_response(self) -> bool:
95
- """Returns whether the event is the final response of the agent."""
94
+ """Returns whether the event is the final response of an agent.
95
+
96
+ NOTE: This method is ONLY for use by Agent Development Kit.
97
+
98
+ Note that when multiple agents participage in one invocation, there could be
99
+ one event has `is_final_response()` as True for each participating agent.
100
+ """
96
101
  if self.actions.skip_summarization or self.long_running_tool_ids:
97
102
  return True
98
103
  return (
@@ -42,6 +42,7 @@ from ...models.llm_response import LlmResponse
42
42
  from ...telemetry import trace_call_llm
43
43
  from ...telemetry import trace_send_data
44
44
  from ...telemetry import tracer
45
+ from ...tools.base_toolset import BaseToolset
45
46
  from ...tools.tool_context import ToolContext
46
47
 
47
48
  if TYPE_CHECKING:
@@ -194,7 +195,12 @@ class BaseLlmFlow(ABC):
194
195
  if live_request.close:
195
196
  await llm_connection.close()
196
197
  return
197
- if live_request.blob:
198
+
199
+ if live_request.activity_start:
200
+ await llm_connection.send_realtime(types.ActivityStart())
201
+ elif live_request.activity_end:
202
+ await llm_connection.send_realtime(types.ActivityEnd())
203
+ elif live_request.blob:
198
204
  # Cache audio data here for transcription
199
205
  if not invocation_context.transcription_cache:
200
206
  invocation_context.transcription_cache = []
@@ -205,6 +211,7 @@ class BaseLlmFlow(ABC):
205
211
  TranscriptionEntry(role='user', data=live_request.blob)
206
212
  )
207
213
  await llm_connection.send_realtime(live_request.blob)
214
+
208
215
  if live_request.content:
209
216
  await llm_connection.send_content(live_request.content)
210
217
 
@@ -335,13 +342,25 @@ class BaseLlmFlow(ABC):
335
342
  yield event
336
343
 
337
344
  # Run processors for tools.
338
- for tool in await agent.canonical_tools(
339
- ReadonlyContext(invocation_context)
340
- ):
345
+ for tool_union in agent.tools:
341
346
  tool_context = ToolContext(invocation_context)
342
- await tool.process_llm_request(
343
- tool_context=tool_context, llm_request=llm_request
347
+
348
+ # If it's a toolset, process it first
349
+ if isinstance(tool_union, BaseToolset):
350
+ await tool_union.process_llm_request(
351
+ tool_context=tool_context, llm_request=llm_request
352
+ )
353
+
354
+ from ...agents.llm_agent import _convert_tool_union_to_tools
355
+
356
+ # Then process all tools from this tool union
357
+ tools = await _convert_tool_union_to_tools(
358
+ tool_union, ReadonlyContext(invocation_context)
344
359
  )
360
+ for tool in tools:
361
+ await tool.process_llm_request(
362
+ tool_context=tool_context, llm_request=llm_request
363
+ )
345
364
 
346
365
  async def _postprocess_async(
347
366
  self,