fix(eval): wire App plugins through eval paths#6480
Closed
doughayden wants to merge 1 commit into
Closed
Conversation
- Thread app= from cli, dev_server, and AgentEvaluator - Build eval Runner from app.model_copy with merged plugins - Share Runner construction across live and non-live paths - Add unit tests covering each eval surface Closes google#5503
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Link to Issue or Description of Change
1. Link to an existing issue (if applicable):
cli_evalbypassesApp.plugins, breaking observability plugins (e.g.,BigQueryAgentAnalyticsPlugin) during eval runs #5503Problem:
Eval inference builds its
Runnerfrom the bareroot_agent, so the wrappingApp's plugins never apply.EvaluationGenerator._generate_inferences_from_root_agentconstructsRunner(app_name=..., agent=root_agent, plugins=[<internal eval plugins>]), which silently dropsapp.pluginsfor the duration of an eval run.Three non-live surfaces reach that leaf, and all three build
LocalEvalServicewithout theApp:adk evalCLI (cli_eval/cli_tools_click), which is what #cli_evalbypassesApp.plugins, breaking observability plugins (e.g.,BigQueryAgentAnalyticsPlugin) during eval runs #5503 originally reportedAgentEvaluator(theevaluate/evaluate_eval_setpath), the programmatic and pytest entry point, so a scripted or CI eval bypasses observability and guardrail pluginsadk webeval tab (dev_server.run_eval)The result is that an eval scores a different agent than
adk webchat and the deployed server, both of which run the fullApp. It also degrades the LLM-judge metrics that readapp_details(developer instructions plus tool declarations), since those are shaped by plugins the eval never applied.Solution:
Thread
app=from all three callers into the shared inference leaf, which then builds the eval Runner fromapp.model_copy(update={"plugins": list(app.plugins) + internal_eval_plugins, "root_agent": root_agent}). The copy leaves the caller'sAppuntouched, merges the internal eval plugins with the user's rather than replacing them, and overridesroot_agentso the Runner still targets whatever agent the caller asked to evaluate, a sub-agent included. The App'scontext_cache_configandresumability_configcome along with it. When noAppis present, the bare-agent construction is unchanged.This also matches where the Runner API is already heading, since
Runner(plugins=...)is deprecated in favor ofapp=.Runner construction is shared between the non-live and live leaves through
_build_eval_runner_kwargs, souse_live=Truecarries the App the same way. Agent resolution incli_evalgainsget_app_or_root_agent, which surfaces theAppalongside the root agent and keepsget_root_agentas a thin wrapper for existing callers.Testing Plan
Unit Tests:
New tests cover each surface: the CLI resolver (App present, App absent,
appattribute that is not anApp,get_agent_asyncmodules, and the back-compat wrapper),AgentEvaluator._get_agent_for_evalreturning the pair,LocalEvalServiceforwardingappon both the live and non-live branches, and the Runner construction itself asserting thatagent=/plugins=are not passed whenapp=is and that the user's plugin survives the merge.Manual End-to-End (E2E) Tests:
A reproduction lives at doughayden/adk-issue-examples/06-eval_drops_app_plugins. An
LlmAgentis wrapped in anAppwhose plugins include aSentinelPluginthat appends to a module-level list every time it runs inside aRunner, so "did the App's plugins apply during eval" is directly observable. The script runs one real eval through the publicAgentEvaluator.evaluateentry point. Nothing is patched or stubbed in either run, so the second one exercises the full caller chain (AgentEvaluatortoLocalEvalServicetoEvaluationGenerator).Released
google-adk[eval]==2.5.0, from the example directory:The same script against this branch, with only the ADK build swapped:
(An unrelated
vertexai.preview.ragdeprecation warning is elided from both outputs.)The live path is covered by unit tests rather than here, since exercising it needs a live-API session.
Checklist
Additional context
This builds on #5534, which threads the
Appthroughcli_evalandevaluation_generatorand fixes the CLI path. The work here keeps that approach and extends it to the two surfaces #5534 does not reach,AgentEvaluatoranddev_server, plus the live inference leaf that landed after it was opened.get_root_agentrecently became async and gainedget_agent_asyncsupport, soget_app_or_root_agentmirrors that shape and handles both entrypoints. Existingget_root_agentcallers are unaffected.