Skip to content

Commit b433e91

Browse files
committed
Support multimodal content in ProcessorPart.from_function_response.
PiperOrigin-RevId: 828443519
1 parent ebcf642 commit b433e91

File tree

2 files changed

+116
-12
lines changed

2 files changed

+116
-12
lines changed

genai_processors/content_api.py

Lines changed: 55 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
import io
2121
import itertools
2222
import json
23-
from typing import Any, TypeVar
23+
from typing import Any, TypeVar, Union
2424

2525
from absl import logging
2626
from genai_processors import mime_types
@@ -328,23 +328,66 @@ def from_function_response(
328328
cls,
329329
*,
330330
name: str,
331-
response: dict[str, Any],
331+
response: Union[dict[str, Any], 'ProcessorContentTypes'],
332332
function_call_id: str | None = None,
333333
will_continue: bool | None = None,
334334
scheduling: genai_types.FunctionResponseScheduling | None = None,
335335
**kwargs,
336336
) -> 'ProcessorPart':
337-
"""Constructs a ProcessorPart as a function response."""
338-
part = genai_types.Part(
339-
function_response=genai_types.FunctionResponse(
340-
id=function_call_id,
341-
name=name,
342-
response=response,
343-
will_continue=will_continue,
344-
scheduling=scheduling,
345-
)
337+
"""Constructs a ProcessorPart as a function response.
338+
339+
Args:
340+
name: The name of the invoked function.
341+
response: The value returned by the function. It can be a
342+
JSON-serializable object or `ProcessorContentTypes`. If response is a
343+
JSON-serializable or is text-only it will be stored in the '.result'
344+
field of the function response. Otherwise it will be stored in `.parts`.
345+
function_call_id: The ID of the function call this is a response to if
346+
known.
347+
will_continue: Whether this is the last response from a generator
348+
function.
349+
scheduling: The scheduling policy for the function response. Controls
350+
whether the response generation will be triggered immediately or the
351+
function response is just added to the context.
352+
**kwargs: Additional keyword arguments to pass to the `ProcessorPart`
353+
constructor.
354+
355+
Returns:
356+
A ProcessorPart representing the function response.
357+
"""
358+
function_response_args = dict(
359+
id=function_call_id,
360+
name=name,
361+
will_continue=will_continue,
362+
scheduling=scheduling,
346363
)
347-
return cls(part, **kwargs)
364+
# For maximal compatibility we first try to interpret response as JSON
365+
# serializable object. This is what tools in Gemini API return historically.
366+
try:
367+
function_response = genai_types.FunctionResponse(
368+
response={'result': response}, **function_response_args
369+
)
370+
function_response.json()
371+
except ValueError:
372+
# Response is not JSON serializable. Then try to construct content.
373+
response_content = ProcessorContent(response)
374+
try:
375+
function_response = genai_types.FunctionResponse(
376+
response={'result': as_text(response_content, strict=True)},
377+
**function_response_args,
378+
)
379+
except ValueError:
380+
parts = [
381+
genai_types.FunctionResponsePart.from_bytes(
382+
data=part.bytes, mime_type=part.mimetype
383+
)
384+
for part in response_content
385+
]
386+
function_response = genai_types.FunctionResponse(
387+
parts=parts, **function_response_args
388+
)
389+
390+
return cls(genai_types.Part(function_response=function_response), **kwargs)
348391

349392
@classmethod
350393
def from_executable_code(

genai_processors/tests/content_api_test.py

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,67 @@ def test_from_bytes_with_non_text_mimetype(self):
8282
self.assertEqual(part.part.inline_data.data, bytes_data)
8383
self.assertEqual(part.part.inline_data.mime_type, mimetype)
8484

85+
def test_from_function_response_with_text_content(self):
86+
part = content_api.ProcessorPart.from_function_response(
87+
name='foo',
88+
response=content_api.ProcessorContent('bar'),
89+
)
90+
self.assertEqual(part.function_response.name, 'foo')
91+
self.assertEqual(part.function_response.response, {'result': 'bar'})
92+
self.assertIsNone(part.function_response.parts)
93+
94+
def test_from_function_response_with_dataclass(self):
95+
part = content_api.ProcessorPart.from_function_response(
96+
name='foo',
97+
response=Dataclass(foo='foo', bar=1),
98+
)
99+
self.assertEqual(part.function_response.name, 'foo')
100+
self.assertEqual(
101+
part.function_response.response, {'result': Dataclass(foo='foo', bar=1)}
102+
)
103+
self.assertIsNone(part.function_response.parts)
104+
105+
def test_from_function_response_with_image_content(self):
106+
image_bytes = _png_image_bytes()
107+
part = content_api.ProcessorPart.from_function_response(
108+
name='foo',
109+
response=content_api.ProcessorPart(image_bytes, mimetype='image/png'),
110+
)
111+
self.assertEqual(part.function_response.name, 'foo')
112+
self.assertIsNone(part.function_response.response)
113+
self.assertLen(part.function_response.parts, 1)
114+
self.assertEqual(
115+
part.function_response.parts[0].inline_data.mime_type, 'image/png'
116+
)
117+
self.assertEqual(
118+
part.function_response.parts[0].inline_data.data, image_bytes
119+
)
120+
121+
def test_from_function_response_with_mixed_content(self):
122+
image_bytes = _png_image_bytes()
123+
part = content_api.ProcessorPart.from_function_response(
124+
name='foo',
125+
response=[
126+
'Here is a black cat in a black room: ',
127+
content_api.ProcessorPart(image_bytes, mimetype='image/png'),
128+
],
129+
)
130+
self.assertEqual(
131+
part.function_response,
132+
genai_types.FunctionResponse(
133+
name='foo',
134+
parts=[
135+
genai_types.FunctionResponsePart.from_bytes(
136+
data=b'Here is a black cat in a black room: ',
137+
mime_type='text/plain',
138+
),
139+
genai_types.FunctionResponsePart.from_bytes(
140+
data=image_bytes, mime_type='image/png'
141+
),
142+
],
143+
),
144+
)
145+
85146
def test_eq_part_and_non_part(self):
86147
part = content_api.ProcessorPart('foo')
87148
self.assertNotEqual(part, object())

0 commit comments

Comments
 (0)