Skip to content

Commit 978a481

Browse files
renovate[bot]kevinmessiaenhenchaves
authored
fix(deps): update dependency google-genai to v2 (#2464)
* fix(deps): update dependency google-genai to v2 * fix(llm): update google interactions translator * fix(llm): require google genai v2 --------- Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Kevin Messiaen <114553769+kevinmessiaen@users.noreply.github.com> Co-authored-by: Henrique Chaves <44180294+henchaves@users.noreply.github.com>
1 parent 188519d commit 978a481

8 files changed

Lines changed: 123 additions & 107 deletions

File tree

AGENTS.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,9 @@ giskard-oss — behavioral config for interactive coding assistants with a human
3333
– When given a bug report with clear scope: just fix it
3434
– No `# type: ignore`, no patched test assertions — fix the root cause
3535

36+
### 7. Commit Hygiene
37+
– Use Conventional Commits for every commit message (for example, `fix(llm): update google interactions translator`)
38+
3639
## Task Management
3740
1. Brainstorm → Spec → Plan — save to the repo's established planning docs, preferably `docs/specs/` and `docs/plans/`
3841
2. Verify Plan — check in before starting

libs/giskard-llm/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ dependencies = [
1212

1313
[project.optional-dependencies]
1414
openai = ["openai>=1.30,<3"]
15-
google = ["google-genai>=1.0,<2"]
15+
google = ["google-genai>=2.0,<3"]
1616
anthropic = ["anthropic>=0.40,<1"]
1717
azure = ["giskard-llm[openai]"] # alias: azure/ and azure_ai/ use the openai SDK
1818
all = ["giskard-llm[openai,google,anthropic]"]

libs/giskard-llm/src/giskard/llm/translators/google_response.py

Lines changed: 38 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,11 @@
2525
from google.genai._interactions.types import (
2626
GenerationConfigParam,
2727
Interaction,
28+
ModelOutputStepParam,
29+
StepParam,
2830
TextContentParam,
2931
ToolParam,
30-
TurnParam,
32+
UserInputStepParam,
3133
interaction_create_params,
3234
)
3335
from httpx import Timeout as httpxTimeout
@@ -51,8 +53,6 @@ class InteractionCreateParams(TypedDict, total=False):
5153
KNOWN_RESPONSE_PARAMS = frozenset({"temperature", "timeout", "response_format"})
5254

5355

54-
_ROLE_MAP = {"user": "user", "assistant": "model"}
55-
5656
logger = logging.getLogger(__name__)
5757

5858

@@ -70,6 +70,15 @@ def _text_content(text: str) -> "TextContentParam":
7070
return {"type": "text", "text": text}
7171

7272

73+
def _message_step(
74+
role: Literal["user", "assistant"],
75+
content: Iterable["TextContentParam"],
76+
) -> "UserInputStepParam | ModelOutputStepParam":
77+
if role == "user":
78+
return {"content": content, "type": "user_input"}
79+
return {"content": content, "type": "model_output"}
80+
81+
7382
@ResponseInputText.register_serializer(_PROVIDER)
7483
def serialize_input_text(
7584
model: ResponseInputText, _info: SerializationInfo
@@ -94,75 +103,59 @@ def serialize_output_refusal(
94103
@ResponseEasyInputMessage.register_serializer(_PROVIDER)
95104
def serialize_easy_input_message(
96105
model: ResponseEasyInputMessage, info: SerializationInfo
97-
) -> "TurnParam | None":
106+
) -> "StepParam | None":
98107
if model.role == "developer" or model.role == "system":
99108
return None
100109

101110
if isinstance(model.content, str):
102-
return {
103-
"content": [_text_content(model.content)],
104-
"role": _ROLE_MAP[model.role],
105-
}
111+
return _message_step(model.role, [_text_content(model.content)])
106112

107113
content = [
108114
cast("TextContentParam", cast(object, item.model_dump(context=info.context)))
109115
for item in model.content
110116
]
111-
return {"content": content, "role": _ROLE_MAP[model.role]}
117+
return _message_step(model.role, content)
112118

113119

114120
@ResponseOutputMessage.register_serializer(_PROVIDER)
115121
def serialize_output_message(
116122
model: ResponseOutputMessage, info: SerializationInfo
117-
) -> "TurnParam":
123+
) -> "StepParam":
118124
if isinstance(model.content, str):
119-
return {
120-
"content": [_text_content(model.content)],
121-
"role": _ROLE_MAP[model.role],
122-
}
125+
return _message_step(model.role, [_text_content(model.content)])
123126

124127
content = [
125128
cast("TextContentParam", cast(object, item.model_dump(context=info.context)))
126129
for item in model.content
127130
]
128-
return {"content": content, "role": _ROLE_MAP[model.role]}
131+
return _message_step(model.role, content)
129132

130133

131134
@ResponseFunctionToolCall.register_serializer(_PROVIDER)
132135
def serialize_output_function_call(
133136
model: ResponseFunctionToolCall, _info: SerializationInfo
134-
) -> "TurnParam":
137+
) -> "StepParam":
135138
return {
136-
"content": [
137-
{
138-
"type": "function_call",
139-
"id": model.call_id,
140-
"name": model.name,
141-
"arguments": deserialize_arguments(model.arguments),
142-
}
143-
],
144-
"role": "model",
139+
"type": "function_call",
140+
"id": model.call_id,
141+
"name": model.name,
142+
"arguments": deserialize_arguments(model.arguments),
145143
}
146144

147145

148146
@ResponseFunctionCallOutput.register_serializer(_PROVIDER)
149147
def serialize_output_function_call_output(
150148
model: ResponseFunctionCallOutput, _info: SerializationInfo
151-
) -> "TurnParam":
149+
) -> "StepParam":
152150
if model.name is None:
153151
# We cannot compute name from call_id alone since function calls are not guaranteed to be in the input.
154152
raise ValueError("name is required for function calls")
155153

156154
return {
157-
"content": [
158-
{
159-
"type": "function_result",
160-
"call_id": model.call_id,
161-
"name": model.name,
162-
"result": model.output,
163-
}
164-
],
165-
"role": "user",
155+
"type": "function_result",
156+
"call_id": model.call_id,
157+
"name": model.name,
158+
"result": model.output,
166159
}
167160

168161

@@ -279,13 +272,16 @@ def to_google(
279272
@staticmethod
280273
def from_google(raw: "Interaction", model: str) -> ResponseResult:
281274
outputs: list[ResponseOutputItem] = []
282-
for item in raw.outputs or []:
283-
if item.type == "text":
284-
outputs.append(
285-
ResponseOutputMessage(
286-
content=[ResponseOutputText(text=item.text)], role="assistant"
287-
)
288-
)
275+
for item in raw.steps or []:
276+
if item.type == "model_output":
277+
for content in item.content or []:
278+
if content.type == "text":
279+
outputs.append(
280+
ResponseOutputMessage(
281+
content=[ResponseOutputText(text=content.text)],
282+
role="assistant",
283+
)
284+
)
289285
elif item.type == "function_call":
290286
outputs.append(
291287
ResponseFunctionToolCall(

libs/giskard-llm/tests/test_providers.py

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -146,16 +146,21 @@ def _make_openai_response_api_response(
146146

147147
def _make_google_interaction_response(
148148
id: str = "int_001",
149-
output_items: list[Any] | None = None,
149+
steps: list[Any] | None = None,
150150
input_tokens: int = 8,
151151
output_tokens: int = 4,
152152
):
153153
"""Mock the Gemini Interactions API shape."""
154-
if output_items is None:
155-
output_items = [SimpleNamespace(type="text", text="Bonjour")]
154+
if steps is None:
155+
steps = [
156+
SimpleNamespace(
157+
type="model_output",
158+
content=[SimpleNamespace(type="text", text="Bonjour")],
159+
)
160+
]
156161
return SimpleNamespace(
157162
id=id,
158-
outputs=output_items,
163+
steps=steps,
159164
usage=SimpleNamespace(
160165
total_input_tokens=input_tokens,
161166
total_output_tokens=output_tokens,
@@ -544,7 +549,7 @@ async def test_google_respond_function_call(mock_errors):
544549
arguments={"city": "Tokyo"},
545550
)
546551
provider._client.aio.interactions.create = AsyncMock(
547-
return_value=_make_google_interaction_response(output_items=[fc_item])
552+
return_value=_make_google_interaction_response(steps=[fc_item])
548553
)
549554

550555
resp = await provider.respond("gemini-2.0-flash", "Weather?")

libs/giskard-llm/tests/translators/test_google_response.py

Lines changed: 32 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,15 @@
22
33
Request shape mirrors :meth:`giskard.llm.translators.google_response.GoogleResponseTranslator.to_google`.
44
Each :class:`~giskard.llm.types.ResponseEasyInputMessage` and message item serializes to one
5-
``TurnParam`` (``content`` + ``role``); :class:`~giskard.llm.types.ResponseFunctionToolCall`
6-
matches assistant replay as a model turn whose ``content`` contains ``function_call`` (``id``, not top-level ``call_id``);
7-
:class:`~giskard.llm.types.ResponseFunctionCallOutput` becomes a **user** turn whose ``content`` contains ``function_result``.
5+
``StepParam`` (``content`` + ``type``); :class:`~giskard.llm.types.ResponseFunctionToolCall`
6+
matches assistant replay as a ``function_call`` step (``id``, not ``call_id``);
7+
:class:`~giskard.llm.types.ResponseFunctionCallOutput` becomes a ``function_result`` step.
88
``None`` from system/developer turns is dropped.
99
10-
**Role mapping** (Gemini Interactions ``TurnParam.role``): ``user`` stays ``user``;
11-
``assistant`` becomes ``model`` (same for :class:`~giskard.llm.types.ResponseOutputMessage`).
12-
:class:`~giskard.llm.types.ResponseOutputFunctionCall` is always ``model``.
10+
**Step mapping** (Gemini Interactions ``StepParam.type``): ``user`` becomes
11+
``user_input``; ``assistant`` becomes ``model_output`` (same for
12+
:class:`~giskard.llm.types.ResponseOutputMessage`).
13+
:class:`~giskard.llm.types.ResponseOutputFunctionCall` is ``function_call``.
1314
System and developer text is not emitted as turns: it is merged into ``system_instruction``.
1415
1516
For **return** mapping -> :class:`~giskard.llm.types.ResponseResult`, see ``test_google_response_return.py``.
@@ -93,42 +94,34 @@ def _text_part(text: str) -> dict[str, str]:
9394
return {**_TEXT, "text": text}
9495

9596

96-
def _msg_turn(role: Literal["user", "model"], text: str) -> dict[str, object]:
97-
"""One ``ResponseEasyInputMessage`` serializes to a single Interactions turn dict."""
98-
return {"content": [_text_part(text)], "role": role}
97+
def _msg_step(
98+
step_type: Literal["user_input", "model_output"], text: str
99+
) -> dict[str, object]:
100+
"""One ``ResponseEasyInputMessage`` serializes to a single Interactions step dict."""
101+
return {"content": [_text_part(text)], "type": step_type}
99102

100103

101104
def _model_function_call_turn(
102105
call_id: str, name: str, arguments: dict[str, object]
103106
) -> dict[str, object]:
104107
"""Input ``ResponseFunctionToolCall`` matches assistant replay shape (Interactions API)."""
105108
return {
106-
"content": [
107-
{
108-
"type": "function_call",
109-
"id": call_id,
110-
"name": name,
111-
"arguments": arguments,
112-
}
113-
],
114-
"role": "model",
109+
"type": "function_call",
110+
"id": call_id,
111+
"name": name,
112+
"arguments": arguments,
115113
}
116114

117115

118116
def _user_function_result_turn(
119117
call_id: str, name: str, result: str
120118
) -> dict[str, object]:
121-
"""``ResponseFunctionCallOutput`` → user turn with ``function_result`` content (Gemini API)."""
119+
"""``ResponseFunctionCallOutput`` -> ``function_result`` step (Gemini API)."""
122120
return {
123-
"content": [
124-
{
125-
"type": "function_result",
126-
"call_id": call_id,
127-
"name": name,
128-
"result": result,
129-
}
130-
],
131-
"role": "user",
121+
"type": "function_result",
122+
"call_id": call_id,
123+
"name": name,
124+
"result": result,
132125
}
133126

134127

@@ -146,7 +139,7 @@ def test_message_instruction_then_user(
146139
]
147140
payload = GoogleResponseTranslator.to_google(_MODEL, items)
148141

149-
assert payload["input"] == [_msg_turn("user", "Hello.")]
142+
assert payload["input"] == [_msg_step("user_input", "Hello.")]
150143
assert payload.get("system_instruction") == "You are helpful."
151144
validate_google_interaction_params(payload)
152145

@@ -160,7 +153,7 @@ def test_message_system_then_developer_then_user():
160153
]
161154
payload = GoogleResponseTranslator.to_google(_MODEL, items)
162155

163-
assert payload["input"] == [_msg_turn("user", "Hello.")]
156+
assert payload["input"] == [_msg_step("user_input", "Hello.")]
164157
assert payload.get("system_instruction") == "You are helpful.\nApp version 2.0"
165158
validate_google_interaction_params(payload)
166159

@@ -188,7 +181,7 @@ def test_message_two_instructions_then_user(
188181
]
189182
payload = GoogleResponseTranslator.to_google(_MODEL, items)
190183

191-
assert payload["input"] == [_msg_turn("user", "Hello.")]
184+
assert payload["input"] == [_msg_step("user_input", "Hello.")]
192185
assert (
193186
payload.get("system_instruction")
194187
== "First system instruction.\nSecond system instruction."
@@ -206,9 +199,9 @@ def test_message_user_assistant_user():
206199
payload = GoogleResponseTranslator.to_google(_MODEL, items)
207200

208201
assert payload["input"] == [
209-
_msg_turn("user", "First user."),
210-
_msg_turn("model", "Assistant reply."),
211-
_msg_turn("user", "Second user."),
202+
_msg_step("user_input", "First user."),
203+
_msg_step("model_output", "Assistant reply."),
204+
_msg_step("user_input", "Second user."),
212205
]
213206
assert "system_instruction" not in payload
214207
validate_google_interaction_params(payload)
@@ -223,7 +216,7 @@ def test_user_tool_call_and_result_with_tools():
223216
{"type": "function", **WEATHER_TOOL.function.model_dump()},
224217
]
225218
assert payload.get("input") == [
226-
_msg_turn("user", "What's the weather in Paris?"),
219+
_msg_step("user_input", "What's the weather in Paris?"),
227220
_model_function_call_turn(TOOL_CALL_ID, "get_weather", {"city": "Paris"}),
228221
_user_function_result_turn(TOOL_CALL_ID, "get_weather", TOOL_RESULT_CONTENT),
229222
]
@@ -240,7 +233,7 @@ def test_user_two_parallel_tool_calls_and_results_with_tools():
240233
{"type": "function", **GET_TIME_TOOL.function.model_dump()},
241234
]
242235
assert payload.get("input") == [
243-
_msg_turn("user", PARALLEL_USER_PROMPT),
236+
_msg_step("user_input", PARALLEL_USER_PROMPT),
244237
_model_function_call_turn(
245238
TOOL_CALL_ID_WEATHER_PARALLEL,
246239
"get_weather",
@@ -275,8 +268,8 @@ def test_user_assistant_text_two_parallel_tool_calls_and_results_with_tools():
275268
{"type": "function", **GET_TIME_TOOL.function.model_dump()},
276269
]
277270
assert payload.get("input") == [
278-
_msg_turn("user", PARALLEL_USER_PROMPT),
279-
_msg_turn("model", ASSISTANT_TEXT_WITH_PARALLEL_TOOLS),
271+
_msg_step("user_input", PARALLEL_USER_PROMPT),
272+
_msg_step("model_output", ASSISTANT_TEXT_WITH_PARALLEL_TOOLS),
280273
_model_function_call_turn(
281274
TOOL_CALL_ID_WEATHER_PARALLEL,
282275
"get_weather",
@@ -319,7 +312,7 @@ def test_assistant_message_mixed_output_text_and_refusal_maps_to_text_parts():
319312
{"type": "text", "text": "Partial."},
320313
{"type": "text", "text": "Stopped."},
321314
],
322-
"role": "model",
315+
"type": "model_output",
323316
}
324317
]
325318
validate_google_interaction_params(payload)

0 commit comments

Comments
 (0)