Skip to content

Commit ce3418a

Browse files
wukathcopybara-github
authored andcommitted
fix: Fix BuiltInCodeExecutor so that it can support visualizations
Previously BuiltInCodeExecutor was missing the logic to save output files from executed code as artifacts, so images/visualizations wouldn't show up in the UI. This fix will iterate through all parts of the LlmResponse, and if any of them are images, it will save the image data using artifact_service (similar to what is done in VertexAICodeExecutor). This fixes the backend, but there are still UI bugs that should be fixed -- events without content are currently ignored, so the image doesn't appear even though it is saved. We will add the UI fix in a separate change. PiperOrigin-RevId: 822245140
1 parent fe1fc75 commit ce3418a

2 files changed

Lines changed: 140 additions & 0 deletions

File tree

src/google/adk/flows/llm_flows/_code_execution.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import base64
2020
import copy
2121
import dataclasses
22+
import datetime
2223
import os
2324
import re
2425
from typing import AsyncGenerator
@@ -275,6 +276,43 @@ async def _run_post_processor(
275276
return
276277

277278
if isinstance(code_executor, BuiltInCodeExecutor):
279+
event_actions = EventActions()
280+
281+
# If an image is generated, save it to the artifact service and add it to
282+
# the event actions.
283+
for part in llm_response.content.parts:
284+
if part.inline_data and part.inline_data.mime_type.startswith('image/'):
285+
if invocation_context.artifact_service is None:
286+
raise ValueError('Artifact service is not initialized.')
287+
288+
if part.inline_data.display_name:
289+
file_name = part.inline_data.display_name
290+
else:
291+
now = datetime.datetime.now().astimezone()
292+
timestamp = now.strftime('%Y%m%d_%H%M%S')
293+
file_extension = part.inline_data.mime_type.split('/')[-1]
294+
file_name = f'{timestamp}.{file_extension}'
295+
296+
version = await invocation_context.artifact_service.save_artifact(
297+
app_name=invocation_context.app_name,
298+
user_id=invocation_context.user_id,
299+
session_id=invocation_context.session.id,
300+
filename=file_name,
301+
artifact=types.Part.from_bytes(
302+
data=part.inline_data.data,
303+
mime_type=part.inline_data.mime_type,
304+
),
305+
)
306+
event_actions.artifact_delta[file_name] = version
307+
part.inline_data = None
308+
part.text = f'artifact: {file_name}'
309+
310+
yield Event(
311+
invocation_id=invocation_context.invocation_id,
312+
author=agent.name,
313+
branch=invocation_context.branch,
314+
actions=event_actions,
315+
)
278316
return
279317

280318
code_executor_context = CodeExecutorContext(invocation_context.session.state)
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
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+
"""Unit tests for Code Execution logic."""
16+
17+
import datetime
18+
from unittest.mock import AsyncMock
19+
from unittest.mock import MagicMock
20+
from unittest.mock import patch
21+
22+
from google.adk.agents.llm_agent import Agent
23+
from google.adk.code_executors.built_in_code_executor import BuiltInCodeExecutor
24+
from google.adk.flows.llm_flows._code_execution import response_processor
25+
from google.adk.models.llm_response import LlmResponse
26+
from google.genai import types
27+
import pytest
28+
29+
from ... import testing_utils
30+
31+
32+
@pytest.mark.asyncio
33+
@patch('google.adk.flows.llm_flows._code_execution.datetime')
34+
async def test_builtin_code_executor_image_artifact_creation(mock_datetime):
35+
"""Test BuiltInCodeExecutor creates artifacts for images in response."""
36+
mock_now = datetime.datetime(2025, 1, 1, 12, 0, 0)
37+
mock_datetime.datetime.now.return_value.astimezone.return_value = mock_now
38+
code_executor = BuiltInCodeExecutor()
39+
agent = Agent(name='test_agent', code_executor=code_executor)
40+
invocation_context = await testing_utils.create_invocation_context(
41+
agent=agent, user_content='test message'
42+
)
43+
invocation_context.artifact_service = MagicMock()
44+
invocation_context.artifact_service.save_artifact = AsyncMock(
45+
return_value='v1'
46+
)
47+
llm_response = LlmResponse(
48+
content=types.Content(
49+
parts=[
50+
types.Part(
51+
inline_data=types.Blob(
52+
mime_type='image/png',
53+
data=b'image1',
54+
display_name='image_1.png',
55+
)
56+
),
57+
types.Part(text='this is text'),
58+
types.Part(
59+
inline_data=types.Blob(mime_type='image/jpeg', data=b'image2')
60+
),
61+
]
62+
)
63+
)
64+
65+
events = []
66+
async for event in response_processor.run_async(
67+
invocation_context, llm_response
68+
):
69+
events.append(event)
70+
71+
expected_timestamp = mock_now.strftime('%Y%m%d_%H%M%S')
72+
expected_filename2 = f'{expected_timestamp}.jpeg'
73+
74+
assert invocation_context.artifact_service.save_artifact.call_count == 2
75+
invocation_context.artifact_service.save_artifact.assert_any_call(
76+
app_name=invocation_context.app_name,
77+
user_id=invocation_context.user_id,
78+
session_id=invocation_context.session.id,
79+
filename='image_1.png',
80+
artifact=types.Part.from_bytes(data=b'image1', mime_type='image/png'),
81+
)
82+
invocation_context.artifact_service.save_artifact.assert_any_call(
83+
app_name=invocation_context.app_name,
84+
user_id=invocation_context.user_id,
85+
session_id=invocation_context.session.id,
86+
filename=expected_filename2,
87+
artifact=types.Part.from_bytes(data=b'image2', mime_type='image/jpeg'),
88+
)
89+
90+
assert len(events) == 1
91+
assert events[0].actions.artifact_delta == {
92+
'image_1.png': 'v1',
93+
expected_filename2: 'v1',
94+
}
95+
assert not events[0].content
96+
assert llm_response.content is not None
97+
assert len(llm_response.content.parts) == 3
98+
assert llm_response.content.parts[0].text == 'artifact: image_1.png'
99+
assert not llm_response.content.parts[0].inline_data
100+
assert llm_response.content.parts[1].text == 'this is text'
101+
assert llm_response.content.parts[2].text == f'artifact: {expected_filename2}'
102+
assert not llm_response.content.parts[2].inline_data

0 commit comments

Comments
 (0)