|
3 | 3 | - Docs: https://ai.google.dev/api |
4 | 4 | """ |
5 | 5 |
|
| 6 | +import base64 |
6 | 7 | import json |
7 | 8 | import logging |
8 | 9 | import mimetypes |
@@ -141,12 +142,26 @@ def _format_request_content_part(self, content: ContentBlock) -> genai.types.Par |
141 | 142 | ) |
142 | 143 |
|
143 | 144 | if "toolUse" in content: |
| 145 | + thought_signature_b64 = cast(Optional[str], content["toolUse"].get("thoughtSignature")) |
| 146 | + |
| 147 | + thought_signature = None |
| 148 | + if thought_signature_b64: |
| 149 | + try: |
| 150 | + thought_signature = base64.b64decode(thought_signature_b64) |
| 151 | + except Exception as e: |
| 152 | + logger.error("toolUseId=<%s> | failed to decode thoughtSignature: %s", content["toolUse"].get("toolUseId"), e) |
| 153 | + else: |
| 154 | + # thoughtSignature is now preserved by the Strands framework (as of v1.18+) |
| 155 | + # If missing, it means the model didn't provide one (e.g., older Gemini versions) |
| 156 | + logger.debug("toolUseId=<%s> | no thoughtSignature in toolUse (model may not require it)", content["toolUse"].get("toolUseId")) |
| 157 | + |
144 | 158 | return genai.types.Part( |
145 | 159 | function_call=genai.types.FunctionCall( |
146 | 160 | args=content["toolUse"]["input"], |
147 | 161 | id=content["toolUse"]["toolUseId"], |
148 | 162 | name=content["toolUse"]["name"], |
149 | 163 | ), |
| 164 | + thought_signature=thought_signature, |
150 | 165 | ) |
151 | 166 |
|
152 | 167 | raise TypeError(f"content_type=<{next(iter(content))}> | unsupported type") |
@@ -212,9 +227,19 @@ def _format_request_config( |
212 | 227 | Returns: |
213 | 228 | Gemini request config. |
214 | 229 | """ |
| 230 | + # Disable thinking text output when tools are present |
| 231 | + # Note: Setting include_thoughts=False prevents thinking text in responses but |
| 232 | + # Gemini still returns thought_signature for function calls. As of Strands v1.18+, |
| 233 | + # the framework properly preserves this field through the message history. |
| 234 | + # See: https://ai.google.dev/gemini-api/docs/thought-signatures |
| 235 | + thinking_config = None |
| 236 | + if tool_specs: |
| 237 | + thinking_config = genai.types.ThinkingConfig(include_thoughts=False) |
| 238 | + |
215 | 239 | return genai.types.GenerateContentConfig( |
216 | 240 | system_instruction=system_prompt, |
217 | 241 | tools=self._format_request_tools(tool_specs), |
| 242 | + thinking_config=thinking_config, |
218 | 243 | **(params or {}), |
219 | 244 | ) |
220 | 245 |
|
@@ -268,14 +293,24 @@ def _format_chunk(self, event: dict[str, Any]) -> StreamEvent: |
268 | 293 | # that name be set in the equivalent FunctionResponse type. Consequently, we assign |
269 | 294 | # function name to toolUseId in our tool use block. And another reason, function_call is |
270 | 295 | # not guaranteed to have id populated. |
| 296 | + tool_use: dict[str, Any] = { |
| 297 | + "name": event["data"].function_call.name, |
| 298 | + "toolUseId": event["data"].function_call.name, |
| 299 | + } |
| 300 | + |
| 301 | + # Get thought_signature from the event dict (passed from stream method) |
| 302 | + thought_sig = event.get("thought_signature") |
| 303 | + |
| 304 | + if thought_sig: |
| 305 | + # Ensure it's bytes for encoding |
| 306 | + if isinstance(thought_sig, str): |
| 307 | + thought_sig = thought_sig.encode("utf-8") |
| 308 | + # Use base64 encoding for storage |
| 309 | + tool_use["thoughtSignature"] = base64.b64encode(thought_sig).decode("utf-8") |
| 310 | + |
271 | 311 | return { |
272 | 312 | "contentBlockStart": { |
273 | | - "start": { |
274 | | - "toolUse": { |
275 | | - "name": event["data"].function_call.name, |
276 | | - "toolUseId": event["data"].function_call.name, |
277 | | - }, |
278 | | - }, |
| 313 | + "start": {"toolUse": cast(Any, tool_use)}, |
279 | 314 | }, |
280 | 315 | } |
281 | 316 |
|
@@ -373,15 +408,33 @@ async def stream( |
373 | 408 | yield self._format_chunk({"chunk_type": "content_start", "data_type": "text"}) |
374 | 409 |
|
375 | 410 | tool_used = False |
| 411 | + # Track thought_signature to associate with function calls |
| 412 | + # According to Gemini docs, thought_signature can be on any part |
| 413 | + last_thought_signature: Optional[bytes] = None |
| 414 | + |
376 | 415 | async for event in response: |
377 | 416 | candidates = event.candidates |
378 | 417 | candidate = candidates[0] if candidates else None |
379 | 418 | content = candidate.content if candidate else None |
380 | 419 | parts = content.parts if content and content.parts else [] |
381 | 420 |
|
382 | 421 | for part in parts: |
| 422 | + # Check ALL parts for thought_signature (Gemini may still include it even with thinking disabled) |
| 423 | + if hasattr(part, "thought_signature") and part.thought_signature: |
| 424 | + last_thought_signature = part.thought_signature |
| 425 | + |
383 | 426 | if part.function_call: |
384 | | - yield self._format_chunk({"chunk_type": "content_start", "data_type": "tool", "data": part}) |
| 427 | + # Use the last thought_signature captured |
| 428 | + effective_thought_signature = last_thought_signature |
| 429 | + |
| 430 | + yield self._format_chunk( |
| 431 | + { |
| 432 | + "chunk_type": "content_start", |
| 433 | + "data_type": "tool", |
| 434 | + "data": part, |
| 435 | + "thought_signature": effective_thought_signature, |
| 436 | + } |
| 437 | + ) |
385 | 438 | yield self._format_chunk({"chunk_type": "content_delta", "data_type": "tool", "data": part}) |
386 | 439 | yield self._format_chunk({"chunk_type": "content_stop", "data_type": "tool", "data": part}) |
387 | 440 | tool_used = True |
|
0 commit comments