Skip to content

fix(langchain): normalize tool definitions and tool_calls for Langfuse UI#1568

Draft
udayshnk wants to merge 1 commit intolangfuse:mainfrom
udayshnk:main
Draft

fix(langchain): normalize tool definitions and tool_calls for Langfuse UI#1568
udayshnk wants to merge 1 commit intolangfuse:mainfrom
udayshnk:main

Conversation

@udayshnk
Copy link

@udayshnk udayshnk commented Mar 19, 2026

Summary

  • Add _to_langfuse_tool() to convert Anthropic (input_schema) and OpenAI (function wrapper) formats to the flat {name, description, parameters} shape validated by LLMToolDefinitionSchema
  • Structure LLM input as {messages, tools} when tools are present so extractToolsFromObservation finds definitions at the top-level tools key
  • Convert AIMessage.tool_calls from {name, args, id} to {id, type, name, arguments} with args serialized as a JSON string so Langfuse's backend can extract call info correctly

Fixes: langfuse/langfuse#11850

Test plan

  • Unit tests for _to_langfuse_tool() covering OpenAI format, Anthropic format, unknown dict passthrough, and non-dict passthrough
  • Integration test test_callback_openai_functions_with_tools updated to assert {messages, tools} input structure and LLMToolDefinitionSchema compliance

Disclaimer: Experimental PR review

Greptile Summary

This PR fixes tool definition and tool call serialization in the LangChain callback handler so that the Langfuse UI can correctly extract and display tool information. The changes normalize disparate provider formats (OpenAI's {type: "function", function: {...}} wrapper and Anthropic's {name, description, input_schema} shape) into the flat {name, description, parameters} schema expected by LLMToolDefinitionSchema, restructure LLM input as {messages, tools} so the backend's extractToolsFromObservation can find definitions at the top-level tools key, and convert AIMessage.tool_calls from LangChain's {name, args, id} format to Langfuse's {id, type, name, arguments} with args serialized as a JSON string.

  • The new _to_langfuse_tool() helper is clean, well-documented, and properly placed at module level.
  • The previous approach of prompts.extend([{"role": "tool", "content": tool} ...]) mutated the input list and used a semantically incorrect message role for tool definitions; the new {messages, tools} dict structure is a clear improvement.
  • import json is correctly added at the top of the module, satisfying the project import convention.
  • invalid_tool_calls is left unconverted while tool_calls is now converted; if the backend applies the same ToolCallSchema to both fields, invalid_tool_calls entries will remain in the wrong format.
  • Edge case: if every entry in message.tool_calls fails the isinstance(tc, dict) guard, message_dict["tool_calls"] is set to [] rather than being omitted entirely, which may surprise downstream consumers.

Confidence Score: 4/5

  • This PR is safe to merge; the core normalization logic is correct and well-tested, with only minor consistency and edge-case gaps.
  • The primary goal (normalizing tool definitions and tool call formats for Langfuse UI) is implemented correctly and covered by both unit and integration tests. The two flagged items—invalid_tool_calls not being converted to the same schema as tool_calls, and the possibility of assigning an empty tool_calls list when all entries are skipped—are style-level concerns that don't affect the happy path.
  • langfuse/langchain/CallbackHandler.py — specifically the invalid_tool_calls handling block (lines 1127–1132) and the empty-list edge case in the tool_calls conversion loop.

Important Files Changed

Filename Overview
langfuse/langchain/CallbackHandler.py Adds _to_langfuse_tool() for normalizing OpenAI/Anthropic tool definitions to Langfuse's LLMToolDefinitionSchema, restructures LLM input as {messages, tools} when tools are present, and converts AIMessage.tool_calls to {id, type, name, arguments}. Minor inconsistency: invalid_tool_calls is left in the original LangChain format while tool_calls is converted; edge case where all skipped tool calls produce an empty list that is still assigned.
tests/test_langchain.py Adds unit tests for _to_langfuse_tool() covering OpenAI format, Anthropic format, unknown dict passthrough, and non-dict passthrough. Updates the integration test test_callback_openai_functions_with_tools to assert the new {messages, tools} input structure and LLMToolDefinitionSchema compliance.

Sequence Diagram

sequenceDiagram
    participant LC as LangChain Runtime
    participant CB as CallbackHandler.__on_llm_action
    participant TN as _to_langfuse_tool()
    participant LF as Langfuse Backend

    LC->>CB: on_llm_start(prompts, invocation_params{tools})
    CB->>CB: observation_input = prompts
    alt tools present in invocation_params
        loop each tool
            CB->>TN: _to_langfuse_tool(tool)
            alt OpenAI format {type:function, function:{...}}
                TN-->>CB: {name, description, parameters}
            else Anthropic format {name, input_schema, ...}
                TN-->>CB: {name, description, parameters}
            else Unknown / already normalized
                TN-->>CB: tool (passthrough)
            end
        end
        CB->>CB: observation_input = {messages: prompts, tools: [normalized...]}
    end
    CB->>LF: start_observation(input=observation_input)

    LC->>CB: on_llm_end(AIMessage with tool_calls)
    CB->>CB: _convert_message_to_dict(message)
    loop each tool_call {name, args, id}
        CB->>CB: json.dumps(args) → arguments string
        CB->>CB: append {id, type:"function", name, arguments}
    end
    CB->>LF: update_observation(output with converted tool_calls)
Loading

Last reviewed commit: "fix(langchain): norm..."

(2/5) Greptile learns from your feedback when you react with thumbs up/down!

@CLAassistant
Copy link

CLAassistant commented Mar 19, 2026

CLA assistant check
All committers have signed the CLA.

@udayshnk udayshnk marked this pull request as draft March 19, 2026 07:55
@udayshnk udayshnk force-pushed the main branch 5 times, most recently from 2f1c5de to f1f1776 Compare March 19, 2026 23:40
…llback handler

- Add _to_langfuse_tool helper returning a list to normalize tool definitions
  into OpenAI canonical format, handling:
  - OpenAI / LiteLLM / Ollama: {type: "function", function: {name, description, parameters}}
  - Anthropic: {name, description, input_schema}
  - Google / Vertex AI: {function_declarations: [{name, description, parameters}, ...]}
  - BaseTool / StructuredTool objects not yet converted to dict (fixes #11850)
- Flatten Google's function_declarations container (one object → N tools) at call site
- Add _convert_tool_call helper to unify tool_calls and invalid_tool_calls conversion,
  supporting both LangChain (args) and Anthropic streaming (input) formats
- Structure on_llm_start input as {messages, tools} so the backend's
  extractToolsFromObservation can find tool definitions at the top-level tools key
- Add _normalize_anthropic_content_blocks to strip streaming artifacts (index,
  partial_json) from Anthropic tool_use content blocks and fill empty input
  from message.tool_calls
- Add unit tests for all formats and helpers
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bug: Issue rendering tool calls made with Langchain 1.0

2 participants