fix(langchain): normalize tool definitions to OpenAI function format#1758
Open
milanagm wants to merge 2 commits into
Open
Conversation
Tool definitions from invocation_params["tools"] were forwarded into the generation input without format normalization. Setups that bypass bind_tools() (model.bind(tools=...), tools passed as invoke kwargs, or custom model integrations) deliver raw BaseTool instances, which were serialized as their attribute dict (args_schema, return_direct, ...) and not recognized as tool definitions by the Langfuse UI. The CallbackHandler now normalizes each tool definition before appending it: OpenAI-format dicts pass through unchanged, everything else is converted via langchain_core's convert_to_openai_tool with a fallback to the original value. Fixes #11850
b100d31 to
4587d8d
Compare
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.
Problem
Tool definitions from
invocation_params["tools"]are forwarded into the generation input without format normalization. Setups that bypassbind_tools()—model.bind(tools=...), tools passed as invoke kwargs, or custom model integrations — deliver rawBaseToolinstances to the callback. These get serialized as their attribute dict (args_schema,return_direct,verbose, ...) and are not recognized as tool definitions by the Langfuse UI, which expects the OpenAI function format ({"type": "function", "function": {...}}).The standard path (
bind_tools()/create_agent) is unaffected: LangChain converts tools at bind time there. Verified against both current package versions and the versions available when the issue was filed.Fix
CallbackHandler.__on_llm_actionnow normalizes each tool definition before appending it to the generation input:langchain_core.utils.function_calling.convert_to_openai_toolTests
_normalize_tool_definition(pass-through identity,BaseToolconversion, bare function schema wrapping, unconvertible fallback).bind(tools=...)) is normalized, the standard path (bind_tools()) stays untouchedFixes langfuse/langfuse#11850
Greptile Summary
This PR adds best-effort normalization of tool definitions captured in
invocation_params[\"tools\"], converting rawBaseToolinstances and bare function-schema dicts into the OpenAI function format ({\"type\": \"function\", \"function\": {...}}) before they are stored in the Langfuse generation input. Definitions that are already in the expected format pass through unchanged, so the standardbind_tools()path is unaffected._normalize_tool_definitionhelper inutils.pyhandles three cases: identity pass-through for already-valid OpenAI dicts, conversion vialangchain_core.utils.function_calling.convert_to_openai_tool, and silent fallback to the original value if conversion fails.CallbackHandler.__on_llm_actionis updated to call this helper for each tool before appending it to thepromptslist.Confidence Score: 4/5
The change is narrowly scoped and additive — tools already in the right format pass through unchanged, so existing integrations are not disturbed.
The normalization logic and its two-path design are sound. The deferred import inside the function body violates the team's module-level import convention, and the bare
except Exceptionmasks both import failures and unexpected conversion errors without any diagnostic output, making future regressions harder to debug.langfuse/langchain/utils.py — the deferred import and broad exception handler in
_normalize_tool_definitionFlowchart
%%{init: {'theme': 'neutral'}}%% flowchart TD A["__on_llm_action called"] --> B["Extract invocation_params['tools']"] B --> C{tools is a non-empty list?} C -- No --> D["Skip tool normalization"] C -- Yes --> E["For each tool: _normalize_tool_definition(tool)"] E --> F{Is dict with type='function' and function key?} F -- Yes --> G["Return tool unchanged (identity pass-through)"] F -- No --> H["try: convert_to_openai_tool(tool)"] H --> I{Conversion successful?} I -- Yes --> J["Return normalized OpenAI-format dict"] I -- No --> K["except Exception: Return original tool unchanged"] G --> L["Append {role: tool, content: normalized} to prompts"] J --> L K --> L D --> M["Continue with model_name extraction and span creation"] L --> M%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%% flowchart TD A["__on_llm_action called"] --> B["Extract invocation_params['tools']"] B --> C{tools is a non-empty list?} C -- No --> D["Skip tool normalization"] C -- Yes --> E["For each tool: _normalize_tool_definition(tool)"] E --> F{Is dict with type='function' and function key?} F -- Yes --> G["Return tool unchanged (identity pass-through)"] F -- No --> H["try: convert_to_openai_tool(tool)"] H --> I{Conversion successful?} I -- Yes --> J["Return normalized OpenAI-format dict"] I -- No --> K["except Exception: Return original tool unchanged"] G --> L["Append {role: tool, content: normalized} to prompts"] J --> L K --> L D --> M["Continue with model_name extraction and span creation"] L --> MPrompt To Fix All With AI
Reviews (1): Last reviewed commit: "fix(langchain): normalize tool definitio..." | Re-trigger Greptile
Context used:
Learned From
langfuse/langfuse-python#1387