Skip to content

Commit ebf2c98

Browse files
Jacksunweicopybara-github
authored andcommitted
feat(conformance): Adds a minimal AdkWebServer http client for conformance tests to interact with
PiperOrigin-RevId: 803208215
1 parent 7b077ac commit ebf2c98

File tree

4 files changed

+448
-0
lines changed

4 files changed

+448
-0
lines changed
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
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.
Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
"""HTTP client for interacting with the ADK web server."""
2+
3+
# Copyright 2025 Google LLC
4+
#
5+
# Licensed under the Apache License, Version 2.0 (the "License");
6+
# you may not use this file except in compliance with the License.
7+
# You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing, software
12+
# distributed under the License is distributed on an "AS IS" BASIS,
13+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
# See the License for the specific language governing permissions and
15+
# limitations under the License.
16+
17+
from __future__ import annotations
18+
19+
from contextlib import asynccontextmanager
20+
import json
21+
import logging
22+
from typing import Any
23+
from typing import AsyncGenerator
24+
from typing import Dict
25+
from typing import Optional
26+
27+
import httpx
28+
29+
from ...events.event import Event
30+
from ...sessions.session import Session
31+
from ..adk_web_server import RunAgentRequest
32+
33+
logger = logging.getLogger("google_adk." + __name__)
34+
35+
36+
class AdkWebServerClient:
37+
"""HTTP client for interacting with the ADK web server for conformance tests.
38+
39+
Usage patterns:
40+
41+
# Pattern 1: Manual lifecycle management
42+
client = AdkWebServerClient()
43+
session = await client.create_session(app_name="app", user_id="user")
44+
async for event in client.run_agent(request):
45+
# Process events...
46+
await client.close() # Optional explicit cleanup
47+
48+
# Pattern 2: Automatic cleanup with context manager (recommended)
49+
async with AdkWebServerClient() as client:
50+
session = await client.create_session(app_name="app", user_id="user")
51+
async for event in client.run_agent(request):
52+
# Process events...
53+
# Client automatically closed here
54+
"""
55+
56+
def __init__(
57+
self, base_url: str = "http://127.0.0.1:8000", timeout: float = 30.0
58+
):
59+
"""Initialize the ADK web server client for conformance testing.
60+
61+
Args:
62+
base_url: Base URL of the ADK web server (default: http://127.0.0.1:8000)
63+
timeout: Request timeout in seconds (default: 30.0)
64+
"""
65+
self.base_url = base_url.rstrip("/")
66+
self.timeout = timeout
67+
self._client: Optional[httpx.AsyncClient] = None
68+
69+
@asynccontextmanager
70+
async def _get_client(self) -> AsyncGenerator[httpx.AsyncClient, None]:
71+
"""Get or create an HTTP client with proper lifecycle management.
72+
73+
Returns:
74+
AsyncGenerator yielding the HTTP client instance.
75+
"""
76+
if self._client is None:
77+
self._client = httpx.AsyncClient(
78+
base_url=self.base_url,
79+
timeout=httpx.Timeout(self.timeout),
80+
)
81+
try:
82+
yield self._client
83+
finally:
84+
pass # Keep client alive for reuse
85+
86+
async def close(self) -> None:
87+
"""Close the HTTP client and clean up resources."""
88+
if self._client:
89+
await self._client.aclose()
90+
self._client = None
91+
92+
async def __aenter__(self) -> "AdkWebServerClient":
93+
"""Async context manager entry.
94+
95+
Returns:
96+
The client instance for use in the async context.
97+
"""
98+
return self
99+
100+
async def __aexit__(self, exc_type, exc_val, exc_tb) -> None: # pylint: disable=unused-argument
101+
"""Async context manager exit that closes the HTTP client."""
102+
await self.close()
103+
104+
async def get_session(
105+
self, *, app_name: str, user_id: str, session_id: str
106+
) -> Session:
107+
"""Retrieve a specific session from the ADK web server.
108+
109+
Args:
110+
app_name: Name of the application
111+
user_id: User identifier
112+
session_id: Session identifier
113+
114+
Returns:
115+
The requested Session object
116+
117+
Raises:
118+
httpx.HTTPStatusError: If the request fails or session not found
119+
"""
120+
async with self._get_client() as client:
121+
response = await client.get(
122+
f"/apps/{app_name}/users/{user_id}/sessions/{session_id}"
123+
)
124+
response.raise_for_status()
125+
return Session.model_validate(response.json())
126+
127+
async def create_session(
128+
self,
129+
*,
130+
app_name: str,
131+
user_id: str,
132+
state: Optional[Dict[str, Any]] = None,
133+
) -> Session:
134+
"""Create a new session in the ADK web server.
135+
136+
Args:
137+
app_name: Name of the application
138+
user_id: User identifier
139+
state: Optional initial state for the session
140+
141+
Returns:
142+
The newly created Session object
143+
144+
Raises:
145+
httpx.HTTPStatusError: If the request fails
146+
"""
147+
async with self._get_client() as client:
148+
payload = {}
149+
if state is not None:
150+
payload["state"] = state
151+
152+
response = await client.post(
153+
f"/apps/{app_name}/users/{user_id}/sessions",
154+
json=payload,
155+
)
156+
response.raise_for_status()
157+
return Session.model_validate(response.json())
158+
159+
async def delete_session(
160+
self, *, app_name: str, user_id: str, session_id: str
161+
) -> None:
162+
"""Delete a session from the ADK web server.
163+
164+
Args:
165+
app_name: Name of the application
166+
user_id: User identifier
167+
session_id: Session identifier to delete
168+
169+
Raises:
170+
httpx.HTTPStatusError: If the request fails or session not found
171+
"""
172+
async with self._get_client() as client:
173+
response = await client.delete(
174+
f"/apps/{app_name}/users/{user_id}/sessions/{session_id}"
175+
)
176+
response.raise_for_status()
177+
178+
async def run_agent(
179+
self,
180+
request: RunAgentRequest,
181+
) -> AsyncGenerator[Event, None]:
182+
"""Run an agent with streaming Server-Sent Events response.
183+
184+
Args:
185+
request: The RunAgentRequest containing agent execution parameters
186+
187+
Yields:
188+
Event objects streamed from the agent execution
189+
190+
Raises:
191+
httpx.HTTPStatusError: If the request fails
192+
json.JSONDecodeError: If event data cannot be parsed
193+
"""
194+
# TODO: Prepare headers for conformance tracking
195+
headers = {}
196+
197+
async with self._get_client() as client:
198+
async with client.stream(
199+
"POST",
200+
"/run_sse",
201+
json=request.model_dump(by_alias=True, exclude_none=True),
202+
headers=headers,
203+
) as response:
204+
response.raise_for_status()
205+
async for line in response.aiter_lines():
206+
if line.startswith("data:") and (data := line[5:].strip()):
207+
try:
208+
event_data = json.loads(data)
209+
yield Event.model_validate(event_data)
210+
except (json.JSONDecodeError, ValueError) as exc:
211+
logger.warning("Failed to parse event data: %s", exc)
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
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.

0 commit comments

Comments
 (0)