fix(langchain): normalize tool definitions and tool_calls for Langfuse UI#1568
Draft
udayshnk wants to merge 1 commit intolangfuse:mainfrom
Draft
fix(langchain): normalize tool definitions and tool_calls for Langfuse UI#1568udayshnk wants to merge 1 commit intolangfuse:mainfrom
udayshnk wants to merge 1 commit intolangfuse:mainfrom
Conversation
2f1c5de to
f1f1776
Compare
…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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
_to_langfuse_tool()to convert Anthropic (input_schema) and OpenAI (functionwrapper) formats to the flat{name, description, parameters}shape validated byLLMToolDefinitionSchema{messages, tools}when tools are present soextractToolsFromObservationfinds definitions at the top-leveltoolskeyAIMessage.tool_callsfrom{name, args, id}to{id, type, name, arguments}with args serialized as a JSON string so Langfuse's backend can extract call info correctlyFixes: langfuse/langfuse#11850
Test plan
_to_langfuse_tool()covering OpenAI format, Anthropic format, unknown dict passthrough, and non-dict passthroughtest_callback_openai_functions_with_toolsupdated to assert{messages, tools}input structure andLLMToolDefinitionSchemacomplianceDisclaimer: 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 byLLMToolDefinitionSchema, restructure LLM input as{messages, tools}so the backend'sextractToolsFromObservationcan find definitions at the top-leveltoolskey, and convertAIMessage.tool_callsfrom LangChain's{name, args, id}format to Langfuse's{id, type, name, arguments}with args serialized as a JSON string._to_langfuse_tool()helper is clean, well-documented, and properly placed at module level.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 jsonis correctly added at the top of the module, satisfying the project import convention.invalid_tool_callsis left unconverted whiletool_callsis now converted; if the backend applies the sameToolCallSchemato both fields,invalid_tool_callsentries will remain in the wrong format.message.tool_callsfails theisinstance(tc, dict)guard,message_dict["tool_calls"]is set to[]rather than being omitted entirely, which may surprise downstream consumers.Confidence Score: 4/5
invalid_tool_callsnot being converted to the same schema astool_calls, and the possibility of assigning an emptytool_callslist when all entries are skipped—are style-level concerns that don't affect the happy path.langfuse/langchain/CallbackHandler.py— specifically theinvalid_tool_callshandling block (lines 1127–1132) and the empty-list edge case in thetool_callsconversion loop.Important Files Changed
_to_langfuse_tool()for normalizing OpenAI/Anthropic tool definitions to Langfuse'sLLMToolDefinitionSchema, restructures LLM input as{messages, tools}when tools are present, and convertsAIMessage.tool_callsto{id, type, name, arguments}. Minor inconsistency:invalid_tool_callsis left in the original LangChain format whiletool_callsis converted; edge case where all skipped tool calls produce an empty list that is still assigned._to_langfuse_tool()covering OpenAI format, Anthropic format, unknown dict passthrough, and non-dict passthrough. Updates the integration testtest_callback_openai_functions_with_toolsto assert the new{messages, tools}input structure andLLMToolDefinitionSchemacompliance.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)Last reviewed commit: "fix(langchain): norm..."
(2/5) Greptile learns from your feedback when you react with thumbs up/down!