Skip to content

Commit 8f0e973

Browse files
committed
fix(migration): allow ui widget action payloads
1 parent 8bdf463 commit 8f0e973

7 files changed

Lines changed: 61 additions & 15 deletions

File tree

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,8 @@
5050
from ...telemetry.tracing import tracer
5151
from ...tools.base_toolset import BaseToolset
5252
from ...tools.tool_context import ToolContext
53-
from ...utils.context_utils import Aclosing
5453
from ...utils import model_name_utils
54+
from ...utils.context_utils import Aclosing
5555
from .audio_cache_manager import AudioCacheManager
5656
from .functions import build_auth_request_event
5757

@@ -563,8 +563,8 @@ async def run_live(
563563
if llm_request.live_connect_config is None:
564564
llm_request.live_connect_config = types.LiveConnectConfig()
565565
if llm_request.live_connect_config.history_config is None:
566-
llm_request.live_connect_config.history_config = types.HistoryConfig(
567-
initial_history_in_client_content=True
566+
llm_request.live_connect_config.history_config = (
567+
types.HistoryConfig(initial_history_in_client_content=True)
568568
)
569569

570570
logger.info(

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

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,10 +83,15 @@ def _build_basic_request(
8383
llm_request.live_connect_config.realtime_input_config = (
8484
invocation_context.run_config.realtime_input_config
8585
)
86-
active_model_name = getattr(getattr(agent, 'canonical_live_model', None), 'model', None) or llm_request.model
86+
active_model_name = (
87+
getattr(getattr(agent, 'canonical_live_model', None), 'model', None)
88+
or llm_request.model
89+
)
8790
is_gemini_31 = model_name_utils.is_gemini_3_1_flash_live(active_model_name)
8891
llm_request.live_connect_config.enable_affective_dialog = (
89-
None if is_gemini_31 else invocation_context.run_config.enable_affective_dialog
92+
None
93+
if is_gemini_31
94+
else invocation_context.run_config.enable_affective_dialog
9095
)
9196
llm_request.live_connect_config.proactivity = (
9297
None if is_gemini_31 else invocation_context.run_config.proactivity

src/google/adk/models/gemini_llm_connection.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -88,11 +88,15 @@ async def send_history(self, history: list[types.Content]):
8888
# protocol error (invalid role mid-session), we consolidate previous multi-turn
8989
# interactions into a unified contextual preamble on a single user role turn.
9090
if is_gemini_31 and self._api_backend != GoogleLLMVariant.GEMINI_API:
91-
collapsed_text = "Previous conversation history:\n"
91+
collapsed_text = 'Previous conversation history:\n'
9292
for c in contents:
93-
text_parts = "".join(p.text for p in c.parts if p.text)
93+
text_parts = ''.join(p.text for p in c.parts if p.text)
9494
collapsed_text += f'[{c.role}]: {text_parts}\n'
95-
contents = [types.Content(role='user', parts=[types.Part.from_text(text=collapsed_text)])]
95+
contents = [
96+
types.Content(
97+
role='user', parts=[types.Part.from_text(text=collapsed_text)]
98+
)
99+
]
96100

97101
logger.debug('Sending history to live connection: %s', contents)
98102
await self._gemini_session.send_client_content(
@@ -281,7 +285,11 @@ async def receive(self) -> AsyncGenerator[LlmResponse, None]:
281285
is_thought = current_is_thought
282286
llm_response.partial = True
283287
# don't yield the merged text event when receiving audio data
284-
if text and not any(p.text for p in content.parts) and not has_inline_data:
288+
if (
289+
text
290+
and not any(p.text for p in content.parts)
291+
and not has_inline_data
292+
):
285293
yield self.__build_full_text_response(text, is_thought)
286294
text = ''
287295
is_thought = False

src/google/adk/sessions/migration/migrate_from_sqlalchemy_pickle.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@
8282
("google.adk.auth.auth_tool", "AuthConfig"),
8383
("google.adk.events.event_actions", "EventActions"),
8484
("google.adk.events.event_actions", "EventCompaction"),
85+
("google.adk.events.ui_widget", "UiWidget"),
8586
("google.adk.tools.tool_confirmation", "ToolConfirmation"),
8687
("google.genai.types", "Blob"),
8788
("google.genai.types", "CodeExecutionResult"),

tests/unittests/flows/llm_flows/test_base_llm_flow.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@
2424
from google.adk.events.event import Event
2525
from google.adk.flows.llm_flows.base_llm_flow import _handle_after_model_callback
2626
from google.adk.flows.llm_flows.base_llm_flow import BaseLlmFlow
27-
from google.adk.models.google_llm import Gemini, GoogleLLMVariant
27+
from google.adk.models.google_llm import Gemini
28+
from google.adk.models.google_llm import GoogleLLMVariant
2829
from google.adk.models.llm_request import LlmRequest
2930
from google.adk.models.llm_response import LlmResponse
3031
from google.adk.plugins.base_plugin import BasePlugin
@@ -1390,7 +1391,7 @@ async def mock_receive_2():
13901391

13911392
@pytest.mark.asyncio
13921393
@pytest.mark.parametrize(
1393-
"api_backend,should_have_history_config",
1394+
'api_backend,should_have_history_config',
13941395
[
13951396
(GoogleLLMVariant.GEMINI_API, True),
13961397
(GoogleLLMVariant.VERTEX_AI, False),
@@ -1424,8 +1425,11 @@ async def mock_receive():
14241425
flow = BaseLlmFlowForTesting()
14251426

14261427
with mock.patch.object(flow, '_send_to_model', new_callable=AsyncMock):
1428+
14271429
async def mock_preprocess(ctx, req):
1428-
req.contents = [types.Content(parts=[types.Part.from_text(text='history')])]
1430+
req.contents = [
1431+
types.Content(parts=[types.Part.from_text(text='history')])
1432+
]
14291433
yield Event(id=Event.new_id(), author='test')
14301434

14311435
with mock.patch.object(

tests/unittests/models/test_gemini_llm_connection.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1543,7 +1543,9 @@ async def mock_receive_generator():
15431543

15441544

15451545
@pytest.mark.asyncio
1546-
async def test_receive_multiplexed_parts(gemini_connection, mock_gemini_session):
1546+
async def test_receive_multiplexed_parts(
1547+
gemini_connection, mock_gemini_session
1548+
):
15471549
"""Test receive with multiplexed inline data and text content."""
15481550
mock_content = types.Content(
15491551
role='model',
@@ -1588,6 +1590,7 @@ async def mock_receive_generator():
15881590
async def test_send_history_gemini_31_turn_complete(mock_gemini_session):
15891591
"""Verify Gemini 3.1 Live history seeding explicitly appends turn_complete=True."""
15901592
from google.adk.models.google_llm import GoogleLLMVariant
1593+
15911594
conn = GeminiLlmConnection(
15921595
mock_gemini_session,
15931596
api_backend=GoogleLLMVariant.GEMINI_API,
@@ -1611,6 +1614,7 @@ async def test_send_history_gemini_31_turn_complete(mock_gemini_session):
16111614
async def test_send_history_collapse_vertex_ai(mock_gemini_session):
16121615
"""Verify history prompt collapse when seeding Gemini 3.1 Live on Vertex AI backend."""
16131616
from google.adk.models.google_llm import GoogleLLMVariant
1617+
16141618
conn = GeminiLlmConnection(
16151619
mock_gemini_session,
16161620
api_backend=GoogleLLMVariant.VERTEX_AI,
@@ -1625,10 +1629,15 @@ async def test_send_history_collapse_vertex_ai(mock_gemini_session):
16251629
await conn.send_history(mock_contents)
16261630

16271631
assert mock_gemini_session.send_client_content.call_count == 1
1628-
called_turns = mock_gemini_session.send_client_content.call_args.kwargs['turns']
1632+
called_turns = mock_gemini_session.send_client_content.call_args.kwargs[
1633+
'turns'
1634+
]
16291635
assert len(called_turns) == 1
16301636
assert called_turns[0].role == 'user'
16311637
assert 'Previous conversation history:' in called_turns[0].parts[0].text
16321638
assert '[user]: hi' in called_turns[0].parts[0].text
16331639
assert '[model]: hello' in called_turns[0].parts[0].text
1634-
assert mock_gemini_session.send_client_content.call_args.kwargs['turn_complete'] is True
1640+
assert (
1641+
mock_gemini_session.send_client_content.call_args.kwargs['turn_complete']
1642+
is True
1643+
)

tests/unittests/sessions/migration/test_migration.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
from google.adk.auth.auth_tool import AuthConfig
2525
from google.adk.events.event_actions import EventActions
2626
from google.adk.events.event_actions import EventCompaction
27+
from google.adk.events.ui_widget import UiWidget
2728
from google.adk.sessions.migration import _schema_check_utils
2829
from google.adk.sessions.migration import migrate_from_sqlalchemy_pickle as mfsp
2930
from google.adk.sessions.schemas import v0
@@ -335,6 +336,24 @@ def test_restricted_actions_unpickler_allows_datetime_state_delta():
335336
assert loaded_actions.state_delta["last_seen"] == last_seen
336337

337338

339+
def test_restricted_actions_unpickler_allows_ui_widgets():
340+
"""Standard UI widget action metadata should migrate by default."""
341+
actions = EventActions(
342+
render_ui_widgets=[
343+
UiWidget(
344+
id="widget-1",
345+
provider="mcp",
346+
payload={"resource_uri": "ui://widget"},
347+
)
348+
]
349+
)
350+
351+
loaded_actions = mfsp._restricted_pickle_loads(pickle.dumps(actions))
352+
353+
assert isinstance(loaded_actions, EventActions)
354+
assert loaded_actions.render_ui_widgets == actions.render_ui_widgets
355+
356+
338357
def test_migrate_from_sqlalchemy_pickle_ignores_non_object_json_fields():
339358
"""Event JSON model fields should only decode object payloads."""
340359
event = mfsp._row_to_event({

0 commit comments

Comments
 (0)