Skip to content

Commit 16d2b70

Browse files
committed
fix(llmo): properly set tool calls in $ai_output_choices
1 parent 1e7f53c commit 16d2b70

File tree

5 files changed

+752
-53
lines changed

5 files changed

+752
-53
lines changed

posthog/ai/langchain/callbacks.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -584,7 +584,7 @@ def _capture_generation(
584584
]
585585
else:
586586
completions = [
587-
_extract_raw_esponse(generation) for generation in generation_result
587+
_extract_raw_response(generation) for generation in generation_result
588588
]
589589
event_properties["$ai_output_choices"] = with_privacy_mode(
590590
self._ph_client, self._privacy_mode, completions
@@ -615,7 +615,7 @@ def _log_debug_event(
615615
)
616616

617617

618-
def _extract_raw_esponse(last_response):
618+
def _extract_raw_response(last_response):
619619
"""Extract the response from the last response of the LLM call."""
620620
# We return the text of the response if not empty
621621
if last_response.text is not None and last_response.text.strip() != "":

posthog/ai/utils.py

Lines changed: 128 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -117,100 +117,178 @@ def format_response(response, provider: str):
117117

118118
def format_response_anthropic(response):
119119
output = []
120+
121+
content_text = ""
122+
tool_calls = []
123+
120124
for choice in response.content:
121125
if (
122126
hasattr(choice, "type")
123127
and choice.type == "text"
124128
and hasattr(choice, "text")
125129
and choice.text
126130
):
127-
output.append(
128-
{
129-
"role": "assistant",
130-
"content": choice.text,
131-
}
132-
)
131+
content_text += choice.text
132+
elif (
133+
hasattr(choice, "type")
134+
and choice.type == "tool_use"
135+
and hasattr(choice, "name")
136+
and hasattr(choice, "id")
137+
):
138+
tool_call = {
139+
"type": "function",
140+
"id": choice.id,
141+
"function": {
142+
"name": choice.name,
143+
"arguments": getattr(choice, "input", {}),
144+
},
145+
}
146+
147+
tool_calls.append(tool_call)
148+
149+
if content_text or tool_calls:
150+
message = {
151+
"role": "assistant",
152+
"content": content_text if content_text else None,
153+
}
154+
155+
if tool_calls:
156+
message["tool_calls"] = tool_calls
157+
158+
output.append(message)
159+
133160
return output
134161

135162

136163
def format_response_openai(response):
137164
output = []
165+
138166
if hasattr(response, "choices"):
139167
for choice in response.choices:
140168
# Handle Chat Completions response format
141-
if hasattr(choice, "message") and choice.message and choice.message.content:
142-
output.append(
143-
{
144-
"content": choice.message.content,
145-
"role": choice.message.role,
146-
}
147-
)
169+
if hasattr(choice, "message") and choice.message:
170+
message = {
171+
"role": choice.message.role,
172+
"content": choice.message.content,
173+
}
174+
175+
if hasattr(choice.message, "tool_calls") and choice.message.tool_calls:
176+
tool_calls = []
177+
for tool_call in choice.message.tool_calls:
178+
tool_calls.append({
179+
"type": "function",
180+
"id": tool_call.id,
181+
"function": {
182+
"name": tool_call.function.name,
183+
"arguments": tool_call.function.arguments,
184+
},
185+
})
186+
message["tool_calls"] = tool_calls
187+
188+
output.append(message)
189+
148190
# Handle Responses API format
149191
if hasattr(response, "output"):
192+
content_text = ""
193+
tool_calls = []
194+
images = []
195+
role = "assistant"
196+
150197
for item in response.output:
151198
if item.type == "message":
152-
# Extract text content from the content list
199+
role = item.role
200+
153201
if hasattr(item, "content") and isinstance(item.content, list):
154202
for content_item in item.content:
155203
if (
156204
hasattr(content_item, "type")
157205
and content_item.type == "output_text"
158206
and hasattr(content_item, "text")
159207
):
160-
output.append(
161-
{
162-
"content": content_item.text,
163-
"role": item.role,
164-
}
165-
)
208+
content_text += content_item.text
166209
elif hasattr(content_item, "text"):
167-
output.append(
168-
{
169-
"content": content_item.text,
170-
"role": item.role,
171-
}
172-
)
210+
content_text += content_item.text
173211
elif (
174212
hasattr(content_item, "type")
175213
and content_item.type == "input_image"
176214
and hasattr(content_item, "image_url")
177215
):
178-
output.append(
179-
{
180-
"content": {
181-
"type": "image",
182-
"image": content_item.image_url,
183-
},
184-
"role": item.role,
185-
}
186-
)
187-
else:
188-
output.append(
189-
{
190-
"content": item.content,
191-
"role": item.role,
192-
}
193-
)
216+
images.append({
217+
"type": "image",
218+
"image": content_item.image_url,
219+
})
220+
elif hasattr(item, "content"):
221+
content_text += str(item.content)
222+
223+
elif hasattr(item, "type") and item.type == "function_call":
224+
tool_call = {
225+
"type": "function",
226+
"id": getattr(item, "call_id", getattr(item, "id", "")),
227+
"function": {
228+
"name": item.name,
229+
"arguments": getattr(item, "arguments", {}),
230+
},
231+
}
232+
233+
tool_calls.append(tool_call)
234+
235+
if content_text or tool_calls:
236+
message = {
237+
"role": role,
238+
"content": content_text if content_text else None,
239+
}
240+
241+
if tool_calls:
242+
message["tool_calls"] = tool_calls
243+
244+
output.append(message)
245+
246+
for image in images:
247+
output.append({
248+
"content": image,
249+
"role": role,
250+
})
251+
194252
return output
195253

196254

197255
def format_response_gemini(response):
198256
output = []
257+
199258
if hasattr(response, "candidates") and response.candidates:
200259
for candidate in response.candidates:
201260
if hasattr(candidate, "content") and candidate.content:
202261
content_text = ""
262+
tool_calls = []
263+
203264
if hasattr(candidate.content, "parts") and candidate.content.parts:
204265
for part in candidate.content.parts:
205266
if hasattr(part, "text") and part.text:
206267
content_text += part.text
207-
if content_text:
208-
output.append(
209-
{
210-
"role": "assistant",
211-
"content": content_text,
212-
}
213-
)
268+
elif hasattr(part, "function_call") and part.function_call:
269+
function_call = part.function_call
270+
271+
tool_call = {
272+
"type": "function",
273+
"function": {
274+
"name": function_call.name,
275+
"arguments": function_call.args,
276+
},
277+
}
278+
279+
tool_calls.append(tool_call)
280+
281+
if content_text or tool_calls:
282+
message = {
283+
"role": "assistant",
284+
"content": content_text if content_text else None,
285+
}
286+
287+
if tool_calls:
288+
message["tool_calls"] = tool_calls
289+
290+
output.append(message)
291+
214292
elif hasattr(candidate, "text") and candidate.text:
215293
output.append(
216294
{
@@ -225,6 +303,7 @@ def format_response_gemini(response):
225303
"content": response.text,
226304
}
227305
)
306+
228307
return output
229308

230309

0 commit comments

Comments
 (0)