Skip to content

Commit 364889a

Browse files
MarkDaoustcopybara-github
authored andcommitted
feat: Add Live API in-session context updates (send_context_update) and types across GenAI SDKs.
PiperOrigin-RevId: 1001127872
1 parent ba854b2 commit 364889a

5 files changed

Lines changed: 436 additions & 1 deletion

File tree

‎google/genai/_live_converters.py‎

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -378,6 +378,98 @@ def _LiveClientContent_to_vertex(
378378
return to_object
379379

380380

381+
def _LiveClientContextUpdateTools_to_mldev(
382+
api_client: BaseApiClient,
383+
from_object: Union[dict[str, Any], object],
384+
parent_object: Optional[dict[str, Any]] = None,
385+
) -> dict[str, Any]:
386+
to_object: dict[str, Any] = {}
387+
if getv(from_object, ['tools']) is not None:
388+
setv(
389+
to_object,
390+
['tools'],
391+
[
392+
_Tool_to_mldev(t.t_tool(api_client, item), to_object)
393+
for item in t.t_tools(api_client, getv(from_object, ['tools']))
394+
],
395+
)
396+
397+
return to_object
398+
399+
400+
def _LiveClientContextUpdateTools_to_vertex(
401+
api_client: BaseApiClient,
402+
from_object: Union[dict[str, Any], object],
403+
parent_object: Optional[dict[str, Any]] = None,
404+
) -> dict[str, Any]:
405+
to_object: dict[str, Any] = {}
406+
if getv(from_object, ['tools']) is not None:
407+
setv(
408+
to_object,
409+
['tools'],
410+
[
411+
_Tool_to_vertex(t.t_tool(api_client, item), to_object)
412+
for item in t.t_tools(api_client, getv(from_object, ['tools']))
413+
],
414+
)
415+
416+
return to_object
417+
418+
419+
def _LiveClientContextUpdate_to_mldev(
420+
api_client: BaseApiClient,
421+
from_object: Union[dict[str, Any], object],
422+
parent_object: Optional[dict[str, Any]] = None,
423+
) -> dict[str, Any]:
424+
to_object: dict[str, Any] = {}
425+
if getv(from_object, ['system_instruction']) is not None:
426+
setv(
427+
to_object,
428+
['systemInstruction'],
429+
_Content_to_mldev(
430+
t.t_content(getv(from_object, ['system_instruction'])), to_object
431+
),
432+
)
433+
434+
if getv(from_object, ['tools']) is not None:
435+
setv(
436+
to_object,
437+
['tools'],
438+
_LiveClientContextUpdateTools_to_mldev(
439+
api_client, getv(from_object, ['tools']), to_object
440+
),
441+
)
442+
443+
return to_object
444+
445+
446+
def _LiveClientContextUpdate_to_vertex(
447+
api_client: BaseApiClient,
448+
from_object: Union[dict[str, Any], object],
449+
parent_object: Optional[dict[str, Any]] = None,
450+
) -> dict[str, Any]:
451+
to_object: dict[str, Any] = {}
452+
if getv(from_object, ['system_instruction']) is not None:
453+
setv(
454+
to_object,
455+
['systemInstruction'],
456+
_Content_to_vertex(
457+
t.t_content(getv(from_object, ['system_instruction'])), to_object
458+
),
459+
)
460+
461+
if getv(from_object, ['tools']) is not None:
462+
setv(
463+
to_object,
464+
['tools'],
465+
_LiveClientContextUpdateTools_to_vertex(
466+
api_client, getv(from_object, ['tools']), to_object
467+
),
468+
)
469+
470+
return to_object
471+
472+
381473
def _LiveClientMessage_to_mldev(
382474
api_client: BaseApiClient,
383475
from_object: Union[dict[str, Any], object],
@@ -408,6 +500,15 @@ def _LiveClientMessage_to_mldev(
408500
if getv(from_object, ['tool_response']) is not None:
409501
setv(to_object, ['toolResponse'], getv(from_object, ['tool_response']))
410502

503+
if getv(from_object, ['context_update']) is not None:
504+
setv(
505+
to_object,
506+
['contextUpdate'],
507+
_LiveClientContextUpdate_to_mldev(
508+
api_client, getv(from_object, ['context_update']), to_object
509+
),
510+
)
511+
411512
return to_object
412513

413514

@@ -447,6 +548,15 @@ def _LiveClientMessage_to_vertex(
447548
if getv(from_object, ['tool_response']) is not None:
448549
setv(to_object, ['toolResponse'], getv(from_object, ['tool_response']))
449550

551+
if getv(from_object, ['context_update']) is not None:
552+
setv(
553+
to_object,
554+
['contextUpdate'],
555+
_LiveClientContextUpdate_to_vertex(
556+
api_client, getv(from_object, ['context_update']), to_object
557+
),
558+
)
559+
450560
return to_object
451561

452562

‎google/genai/live.py‎

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,8 @@ def __init__(
114114
self.session_id = session_id
115115
self.setup_complete = setup_complete
116116

117+
# Note: `send` is deprecated. Do not add options here; use specific
118+
# `send_*` methods instead.
117119
async def send(
118120
self,
119121
*,
@@ -445,6 +447,70 @@ async def send_tool_response(
445447

446448
await self._ws.send(json.dumps({'tool_response': tool_response_dict}))
447449

450+
async def send_context_update(
451+
self,
452+
*,
453+
system_instruction: Optional[types.ContentUnion] = None,
454+
tools: Optional[types.ToolListUnion] = None,
455+
) -> None:
456+
"""Send in-session context updates to the model.
457+
458+
Updates to the context of the current session. Only fields that are set will
459+
be updated. Updates are guaranteed to be processed *in order* with the rest of
460+
the inputs.
461+
462+
Note:
463+
`system_instruction` and `tools` are part of the model preamble, so updating
464+
them invalidates the prefix cache. Update them only when strictly necessary
465+
as it might have a performance impact on model generation.
466+
467+
Args:
468+
system_instruction: Updated system instruction for the model.
469+
tools: An updated list of tools the model may use to generate subsequent
470+
responses. Pass an empty list `[]` to clear all tools.
471+
472+
Example:
473+
474+
.. code-block:: python
475+
476+
# Update instructions:
477+
await session.send_context_update(
478+
system_instruction="You are now a technical expert. Be concise."
479+
)
480+
481+
# Update tools:
482+
await session.send_context_update(tools=[my_func])
483+
484+
# Clear tools:
485+
await session.send_context_update(tools=[])
486+
"""
487+
if system_instruction is None and tools is None:
488+
raise ValueError(
489+
'At least one of `system_instruction` or `tools` must be set.'
490+
)
491+
492+
tools_wrapper: Optional[types.LiveClientContextUpdateTools] = None
493+
if tools is not None:
494+
tools_wrapper = types.LiveClientContextUpdateTools(tools=tools)
495+
496+
update = types.LiveClientContextUpdate(
497+
system_instruction=system_instruction,
498+
tools=tools_wrapper,
499+
)
500+
501+
client_message = types.LiveClientMessage(context_update=update)
502+
if self._api_client.vertexai:
503+
message_dict = live_converters._LiveClientMessage_to_vertex(
504+
self._api_client, client_message
505+
)
506+
else:
507+
message_dict = live_converters._LiveClientMessage_to_mldev(
508+
self._api_client, client_message
509+
)
510+
message_dict = _common.convert_to_dict(message_dict)
511+
message_dict = _common.encode_unserializable_types(message_dict)
512+
await self._ws.send(json.dumps(message_dict))
513+
448514
async def receive(self) -> AsyncIterator[types.LiveServerMessage]:
449515
"""Receive model responses from the server.
450516
@@ -890,7 +956,6 @@ def _parse_client_message(
890956
function_responses=function_response_list
891957
)
892958
)
893-
894959
else:
895960
raise ValueError(
896961
f'Unsupported input type "{type(input)}" or input content "{input}"'

‎google/genai/tests/live/test_live.py‎

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
"""Tests for live.py."""
1818

1919
import contextlib
20+
import inspect
2021
import json
2122
import os
2223
import ssl
@@ -2420,3 +2421,34 @@ async def test_bidi_setup_replicated_voice_config_with_consent(vertexai):
24202421
replicated_sig['voice_consent_signature'].get('signature')
24212422
== 'test_sig_abc123'
24222423
)
2424+
2425+
2426+
def test_live_client_message_fields_have_send_methods():
2427+
ignored_message_fields = {'setup'}
2428+
ignored_inner_fields = {
2429+
'realtime_input': {'media_chunks'}, # Exposed as `media`
2430+
}
2431+
2432+
for field_name, field_info in types.LiveClientMessage.model_fields.items():
2433+
if field_name in ignored_message_fields:
2434+
continue
2435+
2436+
method_name = f'send_{field_name}'
2437+
method = getattr(live.AsyncSession, method_name, None)
2438+
assert callable(method), (
2439+
f'AsyncSession is missing `{method_name}` for'
2440+
f' LiveClientMessage.{field_name}'
2441+
)
2442+
2443+
sig_params = set(inspect.signature(method).parameters.keys()) - {'self'}
2444+
inner_type = typing.get_args(field_info.annotation)[0]
2445+
expected_fields = set(
2446+
inner_type.model_fields.keys()
2447+
) - ignored_inner_fields.get(field_name, set())
2448+
2449+
missing_params = expected_fields - sig_params
2450+
assert not missing_params, (
2451+
f'AsyncSession.{method_name} is missing parameters for'
2452+
f' {inner_type.__name__} fields: {sorted(missing_params)}'
2453+
)
2454+

0 commit comments

Comments
 (0)