Skip to content

Commit bf4ff31

Browse files
Jacksunweicopybara-github
authored andcommitted
feat(conformance): add CLI (adk conformance create) for generating conformance tests from spec.yaml file
- Add conformance command group with create subcommand - Implement category/name/spec.yaml with generated-*.yaml files - Support executing agents with queries and recording sessions - Create test cases with recorded llm interactions and tool calls/results Expected folder structure: ``` conformance_repo/ ├── agents/ # Agent definitions - contains all config-based agents shared by test cases. │ ├── single_basic/ │ ├── multi_basic/ │ └── single_tool_builtin/ │ └── tests/ # Test cases ├── core/ # Test category │ ├── desc_001/ # Individual test case │ │ ├── spec.yaml # Human-written specification │ │ ├── generated-session.yaml │ │ ├── generated-recordings.yaml │ │ └── ... # Potential future generated files │ ├── f_001/ │ │ ├── spec.yaml │ │ ├── generated-session.yaml │ │ ├── generated-recordings.yaml │ │ └── ... ``` Help text: ``` -> % adk conformance create --help Usage: adk conformance create [OPTIONS] [PATHS]... Generate ADK conformance test YAML files from TestCaseInput specifications. NOTE: this is work in progress. This command reads TestCaseInput specifications from input.yaml files, executes the specified test cases against agents, and generates conformance test files with recorded agent interactions as test.yaml files. Expected directory structure: category/name/input.yaml (TestCaseInput) -> category/name/test.yaml (TestCase) PATHS: One or more directories containing test case specifications. If no paths are provided, defaults to 'tests/' directory. Examples: Use default directory: adk conformance create Custom directories: adk conformance create tests/core tests/tools Options: --help Show this message and exit. ``` PiperOrigin-RevId: 808609547
1 parent 4cb07ba commit bf4ff31

File tree

3 files changed

+231
-1
lines changed

3 files changed

+231
-1
lines changed

src/google/adk/cli/cli_tools_click.py

Lines changed: 74 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,12 @@
1515
from __future__ import annotations
1616

1717
import asyncio
18-
import collections
1918
from contextlib import asynccontextmanager
2019
from datetime import datetime
2120
import functools
2221
import logging
2322
import os
23+
from pathlib import Path
2424
import tempfile
2525
from typing import Optional
2626

@@ -119,6 +119,66 @@ def deploy():
119119
pass
120120

121121

122+
@main.group()
123+
def conformance():
124+
"""Conformance testing tools for ADK."""
125+
pass
126+
127+
128+
@conformance.command("create", cls=HelpfulCommand)
129+
@click.argument(
130+
"paths",
131+
nargs=-1,
132+
type=click.Path(
133+
exists=True, dir_okay=True, file_okay=False, resolve_path=True
134+
),
135+
)
136+
@click.pass_context
137+
def cli_conformance_create(
138+
ctx,
139+
paths: tuple[str, ...],
140+
):
141+
"""Generate ADK conformance test YAML files from TestCaseInput specifications.
142+
143+
NOTE: this is work in progress.
144+
145+
This command reads TestCaseInput specifications from input.yaml files,
146+
executes the specified test cases against agents, and generates conformance
147+
test files with recorded agent interactions as test.yaml files.
148+
149+
Expected directory structure:
150+
category/name/input.yaml (TestCaseInput) -> category/name/test.yaml (TestCase)
151+
152+
PATHS: One or more directories containing test case specifications.
153+
If no paths are provided, defaults to 'tests/' directory.
154+
155+
Examples:
156+
157+
Use default directory: adk conformance create
158+
159+
Custom directories: adk conformance create tests/core tests/tools
160+
"""
161+
162+
try:
163+
from .conformance.cli_create import run_conformance_create
164+
except ImportError as e:
165+
click.secho(
166+
f"Error: Missing conformance testing dependencies: {e}",
167+
fg="red",
168+
err=True,
169+
)
170+
click.secho(
171+
"Please install the required conformance testing package dependencies.",
172+
fg="yellow",
173+
err=True,
174+
)
175+
ctx.exit(1)
176+
177+
# Default to tests/ directory if no paths provided
178+
test_paths = [Path(p) for p in paths] if paths else [Path("tests").resolve()]
179+
asyncio.run(run_conformance_create(test_paths))
180+
181+
122182
@main.command("create", cls=HelpfulCommand)
123183
@click.option(
124184
"--model",
@@ -697,6 +757,15 @@ def decorator(func):
697757
),
698758
default=None,
699759
)
760+
@click.option(
761+
"--extra_plugins",
762+
help=(
763+
"Optional. Comma-separated list of extra plugin classes or"
764+
" instances to enable (e.g., my.module.MyPluginClass or"
765+
" my.module.my_plugin_instance)."
766+
),
767+
multiple=True,
768+
)
700769
@functools.wraps(func)
701770
@click.pass_context
702771
def wrapper(ctx, *args, **kwargs):
@@ -743,6 +812,7 @@ def cli_web(
743812
artifact_storage_uri: Optional[str] = None, # Deprecated
744813
a2a: bool = False,
745814
reload_agents: bool = False,
815+
extra_plugins: Optional[list[str]] = None,
746816
):
747817
"""Starts a FastAPI server with Web UI for agents.
748818
@@ -794,6 +864,7 @@ async def _lifespan(app: FastAPI):
794864
host=host,
795865
port=port,
796866
reload_agents=reload_agents,
867+
extra_plugins=extra_plugins,
797868
)
798869
config = uvicorn.Config(
799870
app,
@@ -836,6 +907,7 @@ def cli_api_server(
836907
artifact_storage_uri: Optional[str] = None, # Deprecated
837908
a2a: bool = False,
838909
reload_agents: bool = False,
910+
extra_plugins: Optional[list[str]] = None,
839911
):
840912
"""Starts a FastAPI server for agents.
841913
@@ -865,6 +937,7 @@ def cli_api_server(
865937
host=host,
866938
port=port,
867939
reload_agents=reload_agents,
940+
extra_plugins=extra_plugins,
868941
),
869942
host=host,
870943
port=port,
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
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+
"""CLI commands for ADK conformance testing."""
16+
17+
from __future__ import annotations
18+
19+
from pathlib import Path
20+
21+
import click
22+
from google.genai import types
23+
24+
from ...utils.yaml_utils import dump_pydantic_to_yaml
25+
from ..adk_web_server import RunAgentRequest
26+
from ._generated_file_utils import load_test_case
27+
from .adk_web_server_client import AdkWebServerClient
28+
from .test_case import TestCase
29+
30+
31+
async def _create_conformance_test_files(
32+
test_case: TestCase,
33+
user_id: str = "adk_conformance_test_user",
34+
) -> Path:
35+
"""Generate conformance test files from TestCase."""
36+
# Clean existing generated files
37+
test_case_dir = test_case.dir
38+
39+
# Remove existing generated files to ensure clean state
40+
generated_session_file = test_case_dir / "generated-session.yaml"
41+
generated_recordings_file = test_case_dir / "generated-recordings.yaml"
42+
43+
generated_session_file.unlink(missing_ok=True)
44+
generated_recordings_file.unlink(missing_ok=True)
45+
46+
async with AdkWebServerClient() as client:
47+
# Create a new session for the test
48+
session = await client.create_session(
49+
app_name=test_case.test_spec.agent, user_id=user_id, state={}
50+
)
51+
52+
# Run the agent with the user messages
53+
for user_message_index, user_message in enumerate(
54+
test_case.test_spec.user_messages
55+
):
56+
content = types.Content(
57+
parts=[types.Part(text=user_message)], role="user"
58+
)
59+
async for _ in client.run_agent(
60+
RunAgentRequest(
61+
app_name=test_case.test_spec.agent,
62+
user_id=user_id,
63+
session_id=session.id,
64+
new_message=content,
65+
),
66+
mode="record",
67+
test_case_dir=str(test_case_dir),
68+
user_message_index=user_message_index,
69+
):
70+
pass
71+
72+
# Retrieve the updated session
73+
updated_session = await client.get_session(
74+
app_name=test_case.test_spec.agent,
75+
user_id=user_id,
76+
session_id=session.id,
77+
)
78+
79+
# Save session.yaml
80+
dump_pydantic_to_yaml(
81+
updated_session,
82+
generated_session_file,
83+
indent=2,
84+
sort_keys=False,
85+
exclude_none=True,
86+
)
87+
88+
return generated_session_file
89+
90+
91+
async def run_conformance_create(paths: list[Path]) -> None:
92+
"""Generate conformance tests from TestCaseInput files.
93+
94+
Args:
95+
paths: list of directories containing test cases input files (spec.yaml).
96+
"""
97+
click.echo("Generating ADK conformance tests...")
98+
99+
# Look for spec.yaml files and load TestCase objects
100+
test_cases: dict[Path, TestCase] = {}
101+
102+
for test_dir in paths:
103+
if not test_dir.exists():
104+
continue
105+
106+
for spec_file in test_dir.rglob("spec.yaml"):
107+
try:
108+
test_case_dir = spec_file.parent
109+
category = test_case_dir.parent.name
110+
name = test_case_dir.name
111+
test_spec = load_test_case(test_case_dir)
112+
test_case = TestCase(
113+
category=category,
114+
name=name,
115+
dir=test_case_dir,
116+
test_spec=test_spec,
117+
)
118+
test_cases[test_case_dir] = test_case
119+
click.echo(f"Loaded test spec: {category}/{name}")
120+
except Exception as e:
121+
click.secho(f"Failed to load {spec_file}: {e}", fg="red", err=True)
122+
123+
# Process all loaded test cases
124+
if test_cases:
125+
click.echo(f"\nProcessing {len(test_cases)} test cases...")
126+
127+
for test_case in test_cases.values():
128+
try:
129+
await _create_conformance_test_files(test_case)
130+
click.secho(
131+
"Generated conformance test files for:"
132+
f" {test_case.category}/{test_case.name}",
133+
fg="green",
134+
)
135+
except Exception as e:
136+
click.secho(
137+
f"Failed to generate {test_case.category}/{test_case.name}: {e}",
138+
fg="red",
139+
err=True,
140+
)
141+
else:
142+
click.secho("No test specs found to process.", fg="yellow")
143+
144+
click.secho("\nConformance test generation complete!", fg="blue")

src/google/adk/cli/conformance/test_case.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@
1414

1515
from __future__ import annotations
1616

17+
from dataclasses import dataclass
18+
from pathlib import Path
19+
1720
from pydantic import BaseModel
1821
from pydantic import ConfigDict
1922

@@ -37,3 +40,13 @@ class TestSpec(BaseModel):
3740

3841
user_messages: list[str]
3942
"""Sequence of user messages to send to the agent during test execution."""
43+
44+
45+
@dataclass
46+
class TestCase:
47+
"""Represents a single conformance test case."""
48+
49+
category: str
50+
name: str
51+
dir: Path
52+
test_spec: TestSpec

0 commit comments

Comments
 (0)