Skip to content

Commit 17e0c3a

Browse files
committed
fix(cli_eval): add get_app_or_root_agent resolver
Eval flows currently access `agent_module.agent.root_agent` directly, which drops the wrapping `App` (and therefore its plugins, context-cache config, and resumability config). Add `get_app_or_root_agent` that returns the `(app, root_agent)` pair, mirroring the resolution order `AgentLoader._load_from_module_or_package` already uses on the web / run paths. Keep `get_root_agent` as a back-compat wrapper. This commit is the resolver and unit tests only; subsequent commits plumb the App through `EvaluationGenerator` and `LocalEvalService` so plugins fire during eval runs.
1 parent 3e282d2 commit 17e0c3a

2 files changed

Lines changed: 90 additions & 4 deletions

File tree

src/google/adk/cli/cli_eval.py

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,9 @@
2424
import click
2525
from google.genai import types as genai_types
2626

27+
from ..agents.base_agent import BaseAgent
2728
from ..agents.llm_agent import Agent
29+
from ..apps.app import App
2830
from ..evaluation.base_eval_service import BaseEvalService
2931
from ..evaluation.base_eval_service import EvaluateConfig
3032
from ..evaluation.base_eval_service import EvaluateRequest
@@ -86,11 +88,33 @@ def get_default_metric_info(
8688
)
8789

8890

89-
def get_root_agent(agent_module_file_path: str) -> Agent:
90-
"""Returns root agent given the agent module."""
91+
def get_app_or_root_agent(
92+
agent_module_file_path: str,
93+
) -> tuple[Optional[App], BaseAgent]:
94+
"""Returns the (app, root_agent) pair for the given agent module.
95+
96+
Resolution order mirrors `AgentLoader._load_from_module_or_package`:
97+
if the module exposes an `App` instance via `agent.app`, that App and its
98+
`root_agent` are returned. Otherwise `app` is None and the bare
99+
`agent.root_agent` is returned. This lets eval flows participate in the
100+
App's plugin / cache / resumability lifecycle when one is defined, while
101+
preserving the bare-`root_agent` path for projects that don't use App.
102+
"""
91103
agent_module = _get_agent_module(agent_module_file_path)
92-
root_agent = agent_module.agent.root_agent
93-
return root_agent
104+
app = getattr(agent_module.agent, "app", None)
105+
if isinstance(app, App):
106+
return app, app.root_agent
107+
return None, agent_module.agent.root_agent
108+
109+
110+
def get_root_agent(agent_module_file_path: str) -> Agent:
111+
"""Returns root agent given the agent module.
112+
113+
Kept for backward compatibility. New callers should prefer
114+
`get_app_or_root_agent`, which also surfaces the wrapping `App` (if any)
115+
so plugins, context-cache, and resumability configs are honored.
116+
"""
117+
return get_app_or_root_agent(agent_module_file_path)[1]
94118

95119

96120
def try_get_reset_func(agent_module_file_path: str) -> Any:

tests/unittests/cli/utils/test_cli_eval.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@
1919
from types import SimpleNamespace
2020
from unittest import mock
2121

22+
from google.adk.agents.base_agent import BaseAgent
23+
from google.adk.apps.app import App
24+
2225

2326
def test_get_eval_sets_manager_local(monkeypatch):
2427
mock_local_manager = mock.MagicMock()
@@ -49,3 +52,62 @@ def test_get_eval_sets_manager_gcs(monkeypatch):
4952
)
5053
assert manager == mock_gcs_manager
5154
mock_create_gcs.assert_called_once_with("gs://bucket")
55+
56+
57+
def _patch_agent_module(monkeypatch, agent_namespace):
58+
"""Patches `_get_agent_module` to return a stub whose `.agent` matches."""
59+
monkeypatch.setattr(
60+
"google.adk.cli.cli_eval._get_agent_module",
61+
lambda _path: SimpleNamespace(agent=agent_namespace),
62+
)
63+
64+
65+
def test_get_app_or_root_agent_with_app(monkeypatch):
66+
"""When the module exposes an App, both app and its root_agent are returned."""
67+
root_agent = BaseAgent(name="root_agent")
68+
app = App(name="my_app", root_agent=root_agent)
69+
_patch_agent_module(monkeypatch, SimpleNamespace(root_agent=root_agent, app=app))
70+
71+
from google.adk.cli.cli_eval import get_app_or_root_agent
72+
73+
resolved_app, resolved_root = get_app_or_root_agent("some/path")
74+
assert resolved_app is app
75+
assert resolved_root is root_agent
76+
77+
78+
def test_get_app_or_root_agent_without_app(monkeypatch):
79+
"""When only `root_agent` is exposed, app is None."""
80+
root_agent = BaseAgent(name="root_agent")
81+
_patch_agent_module(monkeypatch, SimpleNamespace(root_agent=root_agent))
82+
83+
from google.adk.cli.cli_eval import get_app_or_root_agent
84+
85+
resolved_app, resolved_root = get_app_or_root_agent("some/path")
86+
assert resolved_app is None
87+
assert resolved_root is root_agent
88+
89+
90+
def test_get_app_or_root_agent_app_attribute_not_an_app_instance(monkeypatch):
91+
"""If `app` exists but is not an App, it is ignored and we fall back."""
92+
root_agent = BaseAgent(name="root_agent")
93+
_patch_agent_module(
94+
monkeypatch,
95+
SimpleNamespace(root_agent=root_agent, app="not-an-app"),
96+
)
97+
98+
from google.adk.cli.cli_eval import get_app_or_root_agent
99+
100+
resolved_app, resolved_root = get_app_or_root_agent("some/path")
101+
assert resolved_app is None
102+
assert resolved_root is root_agent
103+
104+
105+
def test_get_root_agent_back_compat(monkeypatch):
106+
"""Existing `get_root_agent` callers keep getting the bare agent back."""
107+
root_agent = BaseAgent(name="root_agent")
108+
app = App(name="my_app", root_agent=root_agent)
109+
_patch_agent_module(monkeypatch, SimpleNamespace(root_agent=root_agent, app=app))
110+
111+
from google.adk.cli.cli_eval import get_root_agent
112+
113+
assert get_root_agent("some/path") is root_agent

0 commit comments

Comments
 (0)