Skip to content

Commit 86e15ca

Browse files
google-genai-botcopybara-github
authored andcommitted
chore: Make ArtifactService transparent to AgentTools
PiperOrigin-RevId: 767225493
1 parent 92e7a4a commit 86e15ca

File tree

3 files changed

+148
-27
lines changed

3 files changed

+148
-27
lines changed
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
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+
from __future__ import annotations
16+
17+
from typing import Optional
18+
from typing import TYPE_CHECKING
19+
20+
from google.genai import types
21+
from typing_extensions import override
22+
23+
from ..artifacts.base_artifact_service import BaseArtifactService
24+
25+
if TYPE_CHECKING:
26+
from .tool_context import ToolContext
27+
28+
29+
class ForwardingArtifactService(BaseArtifactService):
30+
"""Artifact service that forwards to the parent tool context."""
31+
32+
def __init__(self, tool_context: ToolContext):
33+
self.tool_context = tool_context
34+
self._invocation_context = tool_context._invocation_context
35+
36+
@override
37+
async def save_artifact(
38+
self,
39+
*,
40+
app_name: str,
41+
user_id: str,
42+
session_id: str,
43+
filename: str,
44+
artifact: types.Part,
45+
) -> int:
46+
return await self.tool_context.save_artifact(
47+
filename=filename, artifact=artifact
48+
)
49+
50+
@override
51+
async def load_artifact(
52+
self,
53+
*,
54+
app_name: str,
55+
user_id: str,
56+
session_id: str,
57+
filename: str,
58+
version: Optional[int] = None,
59+
) -> Optional[types.Part]:
60+
return await self.tool_context.load_artifact(
61+
filename=filename, version=version
62+
)
63+
64+
@override
65+
async def list_artifact_keys(
66+
self, *, app_name: str, user_id: str, session_id: str
67+
) -> list[str]:
68+
return await self.tool_context.list_artifacts()
69+
70+
@override
71+
async def delete_artifact(
72+
self, *, app_name: str, user_id: str, session_id: str, filename: str
73+
) -> None:
74+
del app_name, user_id, session_id
75+
if self._invocation_context.artifact_service is None:
76+
raise ValueError("Artifact service is not initialized.")
77+
await self._invocation_context.artifact_service.delete_artifact(
78+
app_name=self._invocation_context.app_name,
79+
user_id=self._invocation_context.user_id,
80+
session_id=self._invocation_context.session.id,
81+
filename=filename,
82+
)
83+
84+
@override
85+
async def list_versions(
86+
self, *, app_name: str, user_id: str, session_id: str, filename: str
87+
) -> list[int]:
88+
del app_name, user_id, session_id
89+
if self._invocation_context.artifact_service is None:
90+
raise ValueError("Artifact service is not initialized.")
91+
return await self._invocation_context.artifact_service.list_versions(
92+
app_name=self._invocation_context.app_name,
93+
user_id=self._invocation_context.user_id,
94+
session_id=self._invocation_context.session.id,
95+
filename=filename,
96+
)

src/google/adk/tools/agent_tool.py

Lines changed: 2 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
from ..memory.in_memory_memory_service import InMemoryMemoryService
2626
from ..runners import Runner
2727
from ..sessions.in_memory_session_service import InMemorySessionService
28+
from ._forwarding_artifact_service import ForwardingArtifactService
2829
from .base_tool import BaseTool
2930
from .tool_context import ToolContext
3031

@@ -123,9 +124,7 @@ async def run_async(
123124
runner = Runner(
124125
app_name=self.agent.name,
125126
agent=self.agent,
126-
# TODO(kech): Remove the access to the invocation context.
127-
# It seems we don't need re-use artifact_service if we forward below.
128-
artifact_service=tool_context._invocation_context.artifact_service,
127+
artifact_service=ForwardingArtifactService(tool_context),
129128
session_service=InMemorySessionService(),
130129
memory_service=InMemoryMemoryService(),
131130
)
@@ -144,24 +143,6 @@ async def run_async(
144143
tool_context.state.update(event.actions.state_delta)
145144
last_event = event
146145

147-
if runner.artifact_service:
148-
# Forward all artifacts to parent session.
149-
artifact_names = await runner.artifact_service.list_artifact_keys(
150-
app_name=session.app_name,
151-
user_id=session.user_id,
152-
session_id=session.id,
153-
)
154-
for artifact_name in artifact_names:
155-
if artifact := await runner.artifact_service.load_artifact(
156-
app_name=session.app_name,
157-
user_id=session.user_id,
158-
session_id=session.id,
159-
filename=artifact_name,
160-
):
161-
await tool_context.save_artifact(
162-
filename=artifact_name, artifact=artifact
163-
)
164-
165146
if not last_event or not last_event.content or not last_event.content.parts:
166147
return ''
167148
if isinstance(self.agent, LlmAgent) and self.agent.output_schema:

tests/unittests/tools/test_agent_tool.py

Lines changed: 50 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,20 +13,15 @@
1313
# limitations under the License.
1414

1515
from google.adk.agents import Agent
16+
from google.adk.agents import SequentialAgent
1617
from google.adk.agents.callback_context import CallbackContext
1718
from google.adk.tools.agent_tool import AgentTool
1819
from google.genai.types import Part
1920
from pydantic import BaseModel
20-
import pytest
2121
from pytest import mark
2222

2323
from .. import testing_utils
2424

25-
pytestmark = pytest.mark.skip(
26-
reason='Skipping until tool.func evaluations are fixed (async)'
27-
)
28-
29-
3025
function_call_custom = Part.from_function_call(
3126
name='tool_agent', args={'custom_input': 'test1'}
3227
)
@@ -112,6 +107,55 @@ def test_update_state():
112107
assert runner.session.state['state_1'] == 'changed_value'
113108

114109

110+
def test_update_artifacts():
111+
"""The agent tool can read and write artifacts."""
112+
113+
async def before_tool_agent(callback_context: CallbackContext):
114+
# Artifact 1 should be available in the tool agent.
115+
artifact = await callback_context.load_artifact('artifact_1')
116+
await callback_context.save_artifact(
117+
'artifact_2', Part.from_text(text=artifact.text + ' 2')
118+
)
119+
120+
tool_agent = SequentialAgent(
121+
name='tool_agent',
122+
before_agent_callback=before_tool_agent,
123+
)
124+
125+
async def before_main_agent(callback_context: CallbackContext):
126+
await callback_context.save_artifact(
127+
'artifact_1', Part.from_text(text='test')
128+
)
129+
130+
async def after_main_agent(callback_context: CallbackContext):
131+
# Artifact 2 should be available after the tool agent.
132+
artifact_2 = await callback_context.load_artifact('artifact_2')
133+
await callback_context.save_artifact(
134+
'artifact_3', Part.from_text(text=artifact_2.text + ' 3')
135+
)
136+
137+
mock_model = testing_utils.MockModel.create(
138+
responses=[function_call_no_schema, 'response2']
139+
)
140+
root_agent = Agent(
141+
name='root_agent',
142+
before_agent_callback=before_main_agent,
143+
after_agent_callback=after_main_agent,
144+
tools=[AgentTool(agent=tool_agent)],
145+
model=mock_model,
146+
)
147+
148+
runner = testing_utils.InMemoryRunner(root_agent)
149+
runner.run('test1')
150+
151+
artifacts_path = f'test_app/test_user/{runner.session_id}'
152+
assert runner.runner.artifact_service.artifacts == {
153+
f'{artifacts_path}/artifact_1': [Part.from_text(text='test')],
154+
f'{artifacts_path}/artifact_2': [Part.from_text(text='test 2')],
155+
f'{artifacts_path}/artifact_3': [Part.from_text(text='test 2 3')],
156+
}
157+
158+
115159
@mark.parametrize(
116160
'env_variables',
117161
[

0 commit comments

Comments
 (0)