Skip to content

Commit 82cbd26

Browse files
committed
feat(web): expose artifact metadata endpoints
1 parent 0094eea commit 82cbd26

File tree

4 files changed

+311
-19
lines changed

4 files changed

+311
-19
lines changed

src/google/adk/cli/adk_web_server.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@
6161
from ..agents.run_config import RunConfig
6262
from ..agents.run_config import StreamingMode
6363
from ..apps.app import App
64+
from ..artifacts.base_artifact_service import ArtifactVersion
6465
from ..artifacts.base_artifact_service import BaseArtifactService
6566
from ..auth.credential_service.base_credential_service import BaseCredentialService
6667
from ..errors.already_exists_error import AlreadyExistsError
@@ -1294,6 +1295,24 @@ async def load_artifact(
12941295
raise HTTPException(status_code=404, detail="Artifact not found")
12951296
return artifact
12961297

1298+
@app.get(
1299+
"/apps/{app_name}/users/{user_id}/sessions/{session_id}/artifacts/{artifact_name}/versions/metadata",
1300+
response_model=list[ArtifactVersion],
1301+
response_model_exclude_none=True,
1302+
)
1303+
async def list_artifact_versions_metadata(
1304+
app_name: str,
1305+
user_id: str,
1306+
session_id: str,
1307+
artifact_name: str,
1308+
) -> list[ArtifactVersion]:
1309+
return await self.artifact_service.list_artifact_versions(
1310+
app_name=app_name,
1311+
user_id=user_id,
1312+
session_id=session_id,
1313+
filename=artifact_name,
1314+
)
1315+
12971316
@app.get(
12981317
"/apps/{app_name}/users/{user_id}/sessions/{session_id}/artifacts/{artifact_name}/versions/{version_id}",
12991318
response_model_exclude_none=True,
@@ -1316,6 +1335,31 @@ async def load_artifact_version(
13161335
raise HTTPException(status_code=404, detail="Artifact not found")
13171336
return artifact
13181337

1338+
@app.get(
1339+
"/apps/{app_name}/users/{user_id}/sessions/{session_id}/artifacts/{artifact_name}/versions/{version_id}/metadata",
1340+
response_model=ArtifactVersion,
1341+
response_model_exclude_none=True,
1342+
)
1343+
async def get_artifact_version_metadata(
1344+
app_name: str,
1345+
user_id: str,
1346+
session_id: str,
1347+
artifact_name: str,
1348+
version_id: int,
1349+
) -> ArtifactVersion:
1350+
artifact_version = await self.artifact_service.get_artifact_version(
1351+
app_name=app_name,
1352+
user_id=user_id,
1353+
session_id=session_id,
1354+
filename=artifact_name,
1355+
version=version_id,
1356+
)
1357+
if not artifact_version:
1358+
raise HTTPException(
1359+
status_code=404, detail="Artifact version not found"
1360+
)
1361+
return artifact_version
1362+
13191363
@app.get(
13201364
"/apps/{app_name}/users/{user_id}/sessions/{session_id}/artifacts",
13211365
response_model_exclude_none=True,

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

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727

2828
import httpx
2929

30+
from ...artifacts.base_artifact_service import ArtifactVersion
3031
from ...events.event import Event
3132
from ...sessions.session import Session
3233
from ..adk_web_server import RunAgentRequest
@@ -265,3 +266,44 @@ async def run_agent(
265266
yield Event.model_validate(event_data)
266267
else:
267268
logger.debug("Non data line received: %s", line)
269+
270+
async def get_artifact_version_metadata(
271+
self,
272+
*,
273+
app_name: str,
274+
user_id: str,
275+
session_id: str,
276+
artifact_name: str,
277+
version: int,
278+
) -> ArtifactVersion:
279+
"""Retrieve metadata for a specific artifact version."""
280+
async with self._get_client() as client:
281+
response = await client.get(
282+
(
283+
f"/apps/{app_name}/users/{user_id}/sessions/{session_id}"
284+
f"/artifacts/{artifact_name}/versions/{version}/metadata"
285+
)
286+
)
287+
response.raise_for_status()
288+
return ArtifactVersion.model_validate(response.json())
289+
290+
async def list_artifact_versions_metadata(
291+
self,
292+
*,
293+
app_name: str,
294+
user_id: str,
295+
session_id: str,
296+
artifact_name: str,
297+
) -> list[ArtifactVersion]:
298+
"""List metadata for all versions of an artifact."""
299+
async with self._get_client() as client:
300+
response = await client.get(
301+
(
302+
f"/apps/{app_name}/users/{user_id}/sessions/{session_id}"
303+
f"/artifacts/{artifact_name}/versions/metadata"
304+
)
305+
)
306+
response.raise_for_status()
307+
return [
308+
ArtifactVersion.model_validate(item) for item in response.json()
309+
]

tests/unittests/cli/conformance/test_adk_web_server_client.py

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
from unittest.mock import MagicMock
1818
from unittest.mock import patch
1919

20+
from google.adk.artifacts.base_artifact_service import ArtifactVersion
2021
from google.adk.cli.adk_web_server import RunAgentRequest
2122
from google.adk.cli.conformance.adk_web_server_client import AdkWebServerClient
2223
from google.adk.events.event import Event
@@ -224,6 +225,84 @@ def mock_stream(*_args, **_kwargs):
224225
assert events[1].invocation_id == "test_invocation_2"
225226

226227

228+
@pytest.mark.asyncio
229+
async def test_get_artifact_version_metadata():
230+
client = AdkWebServerClient()
231+
mock_response = MagicMock()
232+
mock_response.json.return_value = {
233+
"version": 2,
234+
"canonicalUri": (
235+
"artifact://apps/app/users/user/sessions/session/"
236+
"artifacts/report/versions/2"
237+
),
238+
"customMetadata": {"foo": "bar"},
239+
"createTime": 123.4,
240+
"mimeType": "text/plain",
241+
}
242+
243+
with patch("httpx.AsyncClient") as mock_client_class:
244+
mock_client = AsyncMock()
245+
mock_client.get.return_value = mock_response
246+
mock_client_class.return_value = mock_client
247+
248+
metadata = await client.get_artifact_version_metadata(
249+
app_name="app",
250+
user_id="user",
251+
session_id="session",
252+
artifact_name="report",
253+
version=2,
254+
)
255+
256+
assert isinstance(metadata, ArtifactVersion)
257+
assert metadata.version == 2
258+
assert metadata.custom_metadata == {"foo": "bar"}
259+
mock_client.get.assert_called_once_with(
260+
"/apps/app/users/user/sessions/session/artifacts/report/versions/2/metadata"
261+
)
262+
mock_response.raise_for_status.assert_called_once()
263+
264+
265+
@pytest.mark.asyncio
266+
async def test_list_artifact_versions_metadata():
267+
client = AdkWebServerClient()
268+
mock_response = MagicMock()
269+
mock_response.json.return_value = [
270+
{
271+
"version": 0,
272+
"canonicalUri": "artifact://.../versions/0",
273+
"customMetadata": {},
274+
"createTime": 100.0,
275+
},
276+
{
277+
"version": 1,
278+
"canonicalUri": "artifact://.../versions/1",
279+
"customMetadata": {"foo": "bar"},
280+
"createTime": 200.0,
281+
"mimeType": "application/json",
282+
},
283+
]
284+
285+
with patch("httpx.AsyncClient") as mock_client_class:
286+
mock_client = AsyncMock()
287+
mock_client.get.return_value = mock_response
288+
mock_client_class.return_value = mock_client
289+
290+
metadata_list = await client.list_artifact_versions_metadata(
291+
app_name="app",
292+
user_id="user",
293+
session_id="session",
294+
artifact_name="report",
295+
)
296+
297+
assert len(metadata_list) == 2
298+
assert all(isinstance(item, ArtifactVersion) for item in metadata_list)
299+
assert metadata_list[1].custom_metadata == {"foo": "bar"}
300+
mock_client.get.assert_called_once_with(
301+
"/apps/app/users/user/sessions/session/artifacts/report/versions/metadata"
302+
)
303+
mock_response.raise_for_status.assert_called_once()
304+
305+
227306
@pytest.mark.asyncio
228307
async def test_close():
229308
client = AdkWebServerClient()

0 commit comments

Comments
 (0)