diff --git a/.sampo/changesets/gentle-toolsmith-vellamo.md b/.sampo/changesets/gentle-toolsmith-vellamo.md new file mode 100644 index 00000000..be8069cd --- /dev/null +++ b/.sampo/changesets/gentle-toolsmith-vellamo.md @@ -0,0 +1,5 @@ +--- +pypi/posthog: patch +--- + +Normalize Gemini tool calls and tool responses in captured input so they render in traces and reach evaluations diff --git a/posthog/ai/gemini/gemini_converter.py b/posthog/ai/gemini/gemini_converter.py index 9ad711d7..b746290e 100644 --- a/posthog/ai/gemini/gemini_converter.py +++ b/posthog/ai/gemini/gemini_converter.py @@ -55,15 +55,24 @@ def _format_part(part: Any) -> Optional[FormattedContentItem]: if "file_data" in plain: media = _format_media_payload(plain["file_data"]) return {"type": _kind_from_mime(media.get("mime_type")), "file_data": media} + # Gemini-native function_call/function_response parts are normalized to the + # provider-agnostic tool-call/tool-result blocks. Emitting the raw Gemini + # shape leaves them unrenderable and invisible to evals. if "function_call" in plain: + call = to_plain(plain["function_call"]) or {} return { - "type": "function_call", - "function_call": to_plain(plain["function_call"]), + "type": "function", + "function": { + "name": call.get("name"), + "arguments": call.get("args"), + }, } if "function_response" in plain: + response = to_plain(plain["function_response"]) or {} return { - "type": "function_response", - "function_response": to_plain(plain["function_response"]), + "type": "function", + "tool_name": response.get("name") or "", + "content": response.get("response"), } if not plain: return None diff --git a/posthog/test/ai/gemini/test_gemini_converter.py b/posthog/test/ai/gemini/test_gemini_converter.py index 3991a8ef..d1388551 100644 --- a/posthog/test/ai/gemini/test_gemini_converter.py +++ b/posthog/test/ai/gemini/test_gemini_converter.py @@ -83,9 +83,14 @@ def test_function_call_and_response_turns_preserved(self): ), ] out = format_gemini_input(contents) - assert out[0]["content"][0]["type"] == "function_call" - assert out[0]["content"][0]["function_call"]["name"] == "get_weather" - assert out[1]["content"][0]["type"] == "function_response" + call = out[0]["content"][0] + assert call["type"] == "function" + assert call["function"] == {"name": "get_weather", "arguments": {"city": "SF"}} + result = out[1]["content"][0] + assert result["type"] == "function" + assert result["tool_name"] == "get_weather" + assert result["content"] == {"temp": "18C"} + assert "function" not in result def test_camel_case_dict_part_preserved(self): out = format_gemini_input(