Skip to content

Commit 6a7557b

Browse files
Add tool/function call structure extraction
Automatically extracts and captures full tool call structure from LLM responses: **What's Captured:** OpenAI Format: - gen_ai.tool_calls.count - Number of tool calls - gen_ai.tool.name - First tool name (for filtering) - Events for each tool call with full structure: - tool_call.id (e.g., "call_abc123") - tool_call.type ("function") - tool_call.function.name ("get_weather") - tool_call.function.arguments (JSON string) Anthropic Format: - gen_ai.tool_calls.count - Number of tool uses - gen_ai.tool.name - First tool name - Events for each tool use: - tool_call.id - tool_call.name - tool_call.input **Usage:** ```python @observe() def chat_with_tools(prompt: str): response = client.chat.completions.create( model="gpt-4o", messages=[{"role": "user", "content": prompt}], tools=[{ "type": "function", "function": { "name": "get_weather", "parameters": {...} } }] ) return response # Tool calls automatically extracted! ``` **Result in Last9:** - Attribute: gen_ai.tool_calls.count = 2 - Attribute: gen_ai.tool.name = "get_weather" - Event: gen_ai.tool.call with full tool call structure - Queryable: Filter by tool name, see all tool call details This completes tool call observability matching Langfuse capabilities! Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
1 parent 71ed949 commit 6a7557b

1 file changed

Lines changed: 60 additions & 0 deletions

File tree

last9_genai/decorators.py

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -456,6 +456,9 @@ def _add_llm_attributes(
456456
if finish_reason:
457457
span.set_attribute(GenAIAttributes.RESPONSE_FINISH_REASONS, finish_reason)
458458

459+
# Extract tool calls (if present)
460+
_extract_tool_calls(span, result)
461+
459462
except Exception:
460463
# If attribute extraction fails, just skip
461464
pass
@@ -569,3 +572,60 @@ def _extract_request_parameters(span, kwargs: dict) -> None:
569572
span.set_attribute(
570573
GenAIAttributes.REQUEST_PRESENCE_PENALTY, float(kwargs["presence_penalty"])
571574
)
575+
576+
577+
def _extract_tool_calls(span, result: Any) -> None:
578+
"""Extract and add tool call information from LLM response."""
579+
try:
580+
# OpenAI format - response.choices[0].message.tool_calls
581+
if hasattr(result, "choices") and len(result.choices) > 0:
582+
message = result.choices[0].message
583+
if hasattr(message, "tool_calls") and message.tool_calls:
584+
# Add tool call count
585+
span.set_attribute("gen_ai.tool_calls.count", len(message.tool_calls))
586+
587+
# Add each tool call as an event with structure
588+
for i, tool_call in enumerate(message.tool_calls):
589+
tool_call_data = {
590+
"tool_call.index": i,
591+
"tool_call.id": tool_call.id,
592+
"tool_call.type": tool_call.type,
593+
"tool_call.function.name": tool_call.function.name,
594+
"tool_call.function.arguments": tool_call.function.arguments,
595+
}
596+
span.add_event(f"gen_ai.tool.call", tool_call_data)
597+
598+
# Also add as attributes for easy filtering
599+
# First tool call name (most common use case)
600+
span.set_attribute(GenAIAttributes.TOOL_NAME, message.tool_calls[0].function.name)
601+
602+
return
603+
604+
# Anthropic format - response.content with tool_use blocks
605+
if hasattr(result, "content") and isinstance(result.content, list):
606+
tool_uses = [
607+
block
608+
for block in result.content
609+
if hasattr(block, "type") and block.type == "tool_use"
610+
]
611+
612+
if tool_uses:
613+
# Add tool call count
614+
span.set_attribute("gen_ai.tool_calls.count", len(tool_uses))
615+
616+
# Add each tool call as an event
617+
for i, tool_use in enumerate(tool_uses):
618+
tool_call_data = {
619+
"tool_call.index": i,
620+
"tool_call.id": tool_use.id,
621+
"tool_call.name": tool_use.name,
622+
"tool_call.input": str(tool_use.input),
623+
}
624+
span.add_event(f"gen_ai.tool.call", tool_call_data)
625+
626+
# First tool name
627+
span.set_attribute(GenAIAttributes.TOOL_NAME, tool_uses[0].name)
628+
629+
except Exception:
630+
# If extraction fails, just skip
631+
pass

0 commit comments

Comments
 (0)