Skip to content

Commit a574f92

Browse files
authored
Merge branch 'main' into upgrade-github-actions-node24
2 parents 9a559d7 + e0b9712 commit a574f92

8 files changed

Lines changed: 332 additions & 1 deletion

File tree

src/google/adk/cli/adk_web_server.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1347,6 +1347,24 @@ async def load_artifact(
13471347
raise HTTPException(status_code=404, detail="Artifact not found")
13481348
return artifact
13491349

1350+
@app.get(
1351+
"/apps/{app_name}/users/{user_id}/sessions/{session_id}/artifacts/{artifact_name}/versions/metadata",
1352+
response_model=list[ArtifactVersion],
1353+
response_model_exclude_none=True,
1354+
)
1355+
async def list_artifact_versions_metadata(
1356+
app_name: str,
1357+
user_id: str,
1358+
session_id: str,
1359+
artifact_name: str,
1360+
) -> list[ArtifactVersion]:
1361+
return await self.artifact_service.list_artifact_versions(
1362+
app_name=app_name,
1363+
user_id=user_id,
1364+
session_id=session_id,
1365+
filename=artifact_name,
1366+
)
1367+
13501368
@app.get(
13511369
"/apps/{app_name}/users/{user_id}/sessions/{session_id}/artifacts/{artifact_name}/versions/{version_id}",
13521370
response_model_exclude_none=True,
@@ -1416,6 +1434,31 @@ async def save_artifact(
14161434
)
14171435
return artifact_version
14181436

1437+
@app.get(
1438+
"/apps/{app_name}/users/{user_id}/sessions/{session_id}/artifacts/{artifact_name}/versions/{version_id}/metadata",
1439+
response_model=ArtifactVersion,
1440+
response_model_exclude_none=True,
1441+
)
1442+
async def get_artifact_version_metadata(
1443+
app_name: str,
1444+
user_id: str,
1445+
session_id: str,
1446+
artifact_name: str,
1447+
version_id: int,
1448+
) -> ArtifactVersion:
1449+
artifact_version = await self.artifact_service.get_artifact_version(
1450+
app_name=app_name,
1451+
user_id=user_id,
1452+
session_id=session_id,
1453+
filename=artifact_name,
1454+
version=version_id,
1455+
)
1456+
if not artifact_version:
1457+
raise HTTPException(
1458+
status_code=404, detail="Artifact version not found"
1459+
)
1460+
return artifact_version
1461+
14191462
@app.get(
14201463
"/apps/{app_name}/users/{user_id}/sessions/{session_id}/artifacts",
14211464
response_model_exclude_none=True,

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

Lines changed: 36 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
@@ -268,3 +269,38 @@ async def run_agent(
268269
yield Event.model_validate(event_data)
269270
else:
270271
logger.debug("Non data line received: %s", line)
272+
273+
async def get_artifact_version_metadata(
274+
self,
275+
*,
276+
app_name: str,
277+
user_id: str,
278+
session_id: str,
279+
artifact_name: str,
280+
version: int,
281+
) -> ArtifactVersion:
282+
"""Retrieve metadata for a specific artifact version."""
283+
async with self._get_client() as client:
284+
response = await client.get((
285+
f"/apps/{app_name}/users/{user_id}/sessions/{session_id}"
286+
f"/artifacts/{artifact_name}/versions/{version}/metadata"
287+
))
288+
response.raise_for_status()
289+
return ArtifactVersion.model_validate(response.json())
290+
291+
async def list_artifact_versions_metadata(
292+
self,
293+
*,
294+
app_name: str,
295+
user_id: str,
296+
session_id: str,
297+
artifact_name: str,
298+
) -> list[ArtifactVersion]:
299+
"""List metadata for all versions of an artifact."""
300+
async with self._get_client() as client:
301+
response = await client.get((
302+
f"/apps/{app_name}/users/{user_id}/sessions/{session_id}"
303+
f"/artifacts/{artifact_name}/versions/metadata"
304+
))
305+
response.raise_for_status()
306+
return [ArtifactVersion.model_validate(item) for item in response.json()]

src/google/adk/features/_feature_registry.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ class FeatureName(str, Enum):
4141
PROGRESSIVE_SSE_STREAMING = "PROGRESSIVE_SSE_STREAMING"
4242
PUBSUB_TOOL_CONFIG = "PUBSUB_TOOL_CONFIG"
4343
PUBSUB_TOOLSET = "PUBSUB_TOOLSET"
44+
SKILL_TOOLSET = "SKILL_TOOLSET"
4445
SPANNER_TOOLSET = "SPANNER_TOOLSET"
4546
SPANNER_TOOL_SETTINGS = "SPANNER_TOOL_SETTINGS"
4647
SPANNER_VECTOR_STORE = "SPANNER_VECTOR_STORE"
@@ -123,6 +124,9 @@ class FeatureConfig:
123124
FeatureName.PUBSUB_TOOLSET: FeatureConfig(
124125
FeatureStage.EXPERIMENTAL, default_on=True
125126
),
127+
FeatureName.SKILL_TOOLSET: FeatureConfig(
128+
FeatureStage.EXPERIMENTAL, default_on=True
129+
),
126130
FeatureName.SPANNER_TOOLSET: FeatureConfig(
127131
FeatureStage.EXPERIMENTAL, default_on=True
128132
),

src/google/adk/tools/skill_toolset.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@
2121
from google.genai import types
2222

2323
from ..agents.readonly_context import ReadonlyContext
24+
from ..features import experimental
25+
from ..features import FeatureName
2426
from ..models.llm_request import LlmRequest
2527
from ..skills import models
2628
from ..skills import prompt
@@ -29,6 +31,7 @@
2931
from .tool_context import ToolContext
3032

3133

34+
@experimental(FeatureName.SKILL_TOOLSET)
3235
class LoadSkillTool(BaseTool):
3336
"""Tool to load a skill's instructions."""
3437

@@ -79,6 +82,7 @@ async def run_async(
7982
}
8083

8184

85+
@experimental(FeatureName.SKILL_TOOLSET)
8286
class LoadSkillResourceTool(BaseTool):
8387
"""Tool to load resources (references or assets) from a skill."""
8488

@@ -167,6 +171,7 @@ async def run_async(
167171
}
168172

169173

174+
@experimental(FeatureName.SKILL_TOOLSET)
170175
class SkillToolset(BaseToolset):
171176
"""A toolset for managing and interacting with agent skills."""
172177

src/google/adk/utils/instructions_utils.py

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,16 @@ async def _async_sub(pattern, repl_async_fn, string) -> str:
7979
return ''.join(result)
8080

8181
async def _replace_match(match) -> str:
82-
var_name = match.group().lstrip('{').rstrip('}').strip()
82+
matched_text = match.group()
83+
if matched_text.startswith('{{') and matched_text.endswith('}}'):
84+
# Preserve escaped non-placeholder literals (e.g. JSON snippets),
85+
# but keep escaped placeholders as literal placeholders.
86+
escaped_value = matched_text[2:-2]
87+
if _is_escaped_placeholder(escaped_value):
88+
return matched_text[1:-1]
89+
return matched_text
90+
91+
var_name = matched_text.lstrip('{').rstrip('}').strip()
8392
optional = False
8493
if var_name.endswith('?'):
8594
optional = True
@@ -124,6 +133,21 @@ async def _replace_match(match) -> str:
124133
return await _async_sub(r'{+[^{}]*}+', _replace_match, template)
125134

126135

136+
def _is_escaped_placeholder(value: str) -> bool:
137+
"""Checks if escaped braces contain a supported placeholder pattern."""
138+
var_name = value.strip()
139+
if not var_name:
140+
return False
141+
142+
if var_name.endswith('?'):
143+
var_name = var_name.removesuffix('?')
144+
145+
if var_name.startswith('artifact.'):
146+
return True
147+
148+
return _is_valid_state_name(var_name)
149+
150+
127151
def _is_valid_state_name(var_name):
128152
"""Checks if the variable name is a valid state name.
129153

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
@@ -262,6 +263,84 @@ def mock_stream(*_args, **_kwargs):
262263
pass
263264

264265

266+
@pytest.mark.asyncio
267+
async def test_get_artifact_version_metadata():
268+
client = AdkWebServerClient()
269+
mock_response = MagicMock()
270+
mock_response.json.return_value = {
271+
"version": 2,
272+
"canonicalUri": (
273+
"artifact://apps/app/users/user/sessions/session/"
274+
"artifacts/report/versions/2"
275+
),
276+
"customMetadata": {"foo": "bar"},
277+
"createTime": 123.4,
278+
"mimeType": "text/plain",
279+
}
280+
281+
with patch("httpx.AsyncClient") as mock_client_class:
282+
mock_client = AsyncMock()
283+
mock_client.get.return_value = mock_response
284+
mock_client_class.return_value = mock_client
285+
286+
metadata = await client.get_artifact_version_metadata(
287+
app_name="app",
288+
user_id="user",
289+
session_id="session",
290+
artifact_name="report",
291+
version=2,
292+
)
293+
294+
assert isinstance(metadata, ArtifactVersion)
295+
assert metadata.version == 2
296+
assert metadata.custom_metadata == {"foo": "bar"}
297+
mock_client.get.assert_called_once_with(
298+
"/apps/app/users/user/sessions/session/artifacts/report/versions/2/metadata"
299+
)
300+
mock_response.raise_for_status.assert_called_once()
301+
302+
303+
@pytest.mark.asyncio
304+
async def test_list_artifact_versions_metadata():
305+
client = AdkWebServerClient()
306+
mock_response = MagicMock()
307+
mock_response.json.return_value = [
308+
{
309+
"version": 0,
310+
"canonicalUri": "artifact://.../versions/0",
311+
"customMetadata": {},
312+
"createTime": 100.0,
313+
},
314+
{
315+
"version": 1,
316+
"canonicalUri": "artifact://.../versions/1",
317+
"customMetadata": {"foo": "bar"},
318+
"createTime": 200.0,
319+
"mimeType": "application/json",
320+
},
321+
]
322+
323+
with patch("httpx.AsyncClient") as mock_client_class:
324+
mock_client = AsyncMock()
325+
mock_client.get.return_value = mock_response
326+
mock_client_class.return_value = mock_client
327+
328+
metadata_list = await client.list_artifact_versions_metadata(
329+
app_name="app",
330+
user_id="user",
331+
session_id="session",
332+
artifact_name="report",
333+
)
334+
335+
assert len(metadata_list) == 2
336+
assert all(isinstance(item, ArtifactVersion) for item in metadata_list)
337+
assert metadata_list[1].custom_metadata == {"foo": "bar"}
338+
mock_client.get.assert_called_once_with(
339+
"/apps/app/users/user/sessions/session/artifacts/report/versions/metadata"
340+
)
341+
mock_response.raise_for_status.assert_called_once()
342+
343+
265344
@pytest.mark.asyncio
266345
async def test_close():
267346
client = AdkWebServerClient()

0 commit comments

Comments
 (0)