-
Notifications
You must be signed in to change notification settings - Fork 535
Feat/preserve thought signature for gemini3 #1227
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
fd3eda8
9730a99
73f9bc3
944b0e1
5424372
a20b7ea
a999c77
5928342
a80c602
3e23297
82f8701
fa9c74c
c42c0fc
6f71ec2
8f0251a
eff6015
4184ab4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -65,6 +65,10 @@ def __init__( | |
|
|
||
| self.client_args = client_args or {} | ||
|
|
||
| # Store the last thought_signature from Gemini responses for multi-turn conversations | ||
| # See: https://ai.google.dev/gemini-api/docs/thought-signatures | ||
| self.last_thought_signature: Optional[bytes] = None | ||
|
|
||
| @override | ||
| def update_config(self, **model_config: Unpack[GeminiConfig]) -> None: # type: ignore[override] | ||
| """Update the Gemini model configuration with the provided arguments. | ||
|
|
@@ -141,12 +145,15 @@ def _format_request_content_part(self, content: ContentBlock) -> genai.types.Par | |
| ) | ||
|
|
||
| if "toolUse" in content: | ||
| # Use the last thought_signature stored from previous Gemini responses | ||
| # This is required for Gemini models that use thought signatures in multi-turn conversations | ||
| return genai.types.Part( | ||
| function_call=genai.types.FunctionCall( | ||
| args=content["toolUse"]["input"], | ||
| id=content["toolUse"]["toolUseId"], | ||
| name=content["toolUse"]["name"], | ||
| ), | ||
| thought_signature=self.last_thought_signature, | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This would apply it to all tool uses right? Is there a way we can make it more specific?
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is required per Gemini docs: "As a general rule, if you receive a thought signature in a model response, you should pass it back exactly as received when sending the conversation history in the next turn. When using Gemini 3 Pro, you must pass back thought signatures during function calling, otherwise you will get a validation error (4xx status code)."
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I meant more of this line, where we don't actually need to set this for all tool calls, just the first one. That said, this is more of a nit comment |
||
| ) | ||
|
|
||
| raise TypeError(f"content_type=<{next(iter(content))}> | unsupported type") | ||
|
|
@@ -170,7 +177,7 @@ def _format_request_content(self, messages: Messages) -> list[genai.types.Conten | |
| for message in messages | ||
| ] | ||
|
|
||
| def _format_request_tools(self, tool_specs: Optional[list[ToolSpec]]) -> list[genai.types.Tool | Any]: | ||
| def _format_request_tools(self, tool_specs: Optional[list[ToolSpec]]) -> Optional[list[genai.types.Tool | Any]]: | ||
| """Format tool specs into Gemini tools. | ||
|
|
||
| - Docs: https://googleapis.github.io/python-genai/genai.html#genai.types.Tool | ||
|
|
@@ -179,8 +186,11 @@ def _format_request_tools(self, tool_specs: Optional[list[ToolSpec]]) -> list[ge | |
| tool_specs: List of tool specifications to make available to the model. | ||
|
|
||
| Return: | ||
| Gemini tool list. | ||
| Gemini tool list, or None if no tools are provided. | ||
| """ | ||
| if not tool_specs: | ||
| return None | ||
|
|
||
| return [ | ||
| genai.types.Tool( | ||
| function_declarations=[ | ||
|
|
@@ -189,7 +199,7 @@ def _format_request_tools(self, tool_specs: Optional[list[ToolSpec]]) -> list[ge | |
| name=tool_spec["name"], | ||
| parameters_json_schema=tool_spec["inputSchema"]["json"], | ||
| ) | ||
| for tool_spec in tool_specs or [] | ||
| for tool_spec in tool_specs | ||
| ], | ||
| ), | ||
| ] | ||
|
|
@@ -268,14 +278,14 @@ def _format_chunk(self, event: dict[str, Any]) -> StreamEvent: | |
| # that name be set in the equivalent FunctionResponse type. Consequently, we assign | ||
| # function name to toolUseId in our tool use block. And another reason, function_call is | ||
| # not guaranteed to have id populated. | ||
| tool_use: dict[str, Any] = { | ||
| "name": event["data"].function_call.name, | ||
| "toolUseId": event["data"].function_call.name, | ||
| } | ||
|
|
||
| return { | ||
| "contentBlockStart": { | ||
| "start": { | ||
| "toolUse": { | ||
| "name": event["data"].function_call.name, | ||
| "toolUseId": event["data"].function_call.name, | ||
| }, | ||
| }, | ||
| "start": {"toolUse": cast(Any, tool_use)}, | ||
| }, | ||
| } | ||
|
|
||
|
|
@@ -373,15 +383,28 @@ async def stream( | |
| yield self._format_chunk({"chunk_type": "content_start", "data_type": "text"}) | ||
|
|
||
| tool_used = False | ||
|
|
||
| async for event in response: | ||
| candidates = event.candidates | ||
| candidate = candidates[0] if candidates else None | ||
| content = candidate.content if candidate else None | ||
| parts = content.parts if content and content.parts else [] | ||
|
|
||
| for part in parts: | ||
| # Capture thought_signature and store it for use in subsequent requests | ||
| # According to Gemini docs, thought_signature can be on any part | ||
| # See: https://ai.google.dev/gemini-api/docs/thought-signatures | ||
| if hasattr(part, "thought_signature") and part.thought_signature: | ||
| self.last_thought_signature = part.thought_signature | ||
|
|
||
| if part.function_call: | ||
| yield self._format_chunk({"chunk_type": "content_start", "data_type": "tool", "data": part}) | ||
| yield self._format_chunk( | ||
| { | ||
| "chunk_type": "content_start", | ||
| "data_type": "tool", | ||
| "data": part, | ||
| } | ||
| ) | ||
| yield self._format_chunk({"chunk_type": "content_delta", "data_type": "tool", "data": part}) | ||
| yield self._format_chunk({"chunk_type": "content_stop", "data_type": "tool", "data": part}) | ||
| tool_used = True | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can we default to skip? This way, if developers change model provider (e.g. anthropic to gemini), the agent would still work
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is contained within the Gemini model implementation so it doesn't affect other providers. Could you please ellaborate? (the absence of thought signatures is also the root cause of the current incompatibility with Gemini 3 Pro)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually 2 use cases:
First, customers can switch model providers. So imagine they start with Bedrock, and then they want to change the agent's model provider to gemini 3
Second, session managers can be problematic. If agent's context is loaded from session managers, the thought signature will not exist. We need to ensure for these cases, we do not throw exception.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
From what I understood reading Google's docs, the requirement for the preservation of tought signatures needs to happen within a turn/cycle of the agent loop. So, if a session/memory was created using another model, during the next turn execution with Gemini 3, a first/new thought signature would be introduced and be used. I can try to run both scenarios to try and reproduce.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here's a quick script I've used:
Worked fine :) Here's the output: