fix(langchain): prefer structured tool inputs#1719
Merged
hassiebp merged 1 commit intoJun 23, 2026
Conversation
hassiebp
approved these changes
Jun 23, 2026
hassiebp
left a comment
Collaborator
There was a problem hiding this comment.
Thanks for your contribution!
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.
What does this PR do?
Fixes LangChain tool observations storing the stringified tool input when LangChain also provides structured tool inputs.
For structured tools, LangChain passes both
input_strand the original structured payload inkwargs["inputs"]toon_tool_start. The callback handler previously always usedinput_str, which can be a Pythonstr(dict)representation. For complex tool inputs, such as multiline content with quotes or JSON-like text, that representation is not reliable JSON and can show up poorly in Langfuse.This updates
on_tool_startto preferkwargs["inputs"]when it is available, while keeping the existinginput_strfallback for callbacks that do not provide structured inputs.Fixes langfuse/langfuse#14026
Type of change
Verification
List the main commands you ran:
uv run --frozen pytest tests/unit/test_langchain.py::test_tool_start_prefers_structured_inputs_when_available uv run --frozen pytest tests/unit/test_langchain.py uv run --frozen ruff check langfuse/langchain/CallbackHandler.py tests/unit/test_langchain.py uv run --frozen ruff format --check langfuse/langchain/CallbackHandler.py tests/unit/test_langchain.py uv run --frozen mypy langfuse --no-error-summary uv run --frozen ruff check . uv run --frozen pytest -n auto --dist worksteal tests/unitI also reproduced the issue locally before the fix with an in-memory exporter. Before this change, the exported tool observation input was a Python dict string and failed JSON parsing:
After the change, the same repro exports structured JSON and parses back to the original input payload:
Full unit result:
Checklist
code_review.md..env.templateif needed — N/A, this is covered by the LangChain callback unit test and does not change setup or public examples.Greptile Summary
This PR fixes
on_tool_startin the LangChain callback handler to prefer the structuredkwargs["inputs"]dict over the Python-stringifiedinput_strwhen recording tool observation inputs, with a fallback toinput_strwhen no structured input is provided.CallbackHandler.pypicks upkwargs["inputs"]first and only falls back toinput_str, preventing unreliablestr(dict)representations (with single quotes, newlines, or embedded JSON) from reaching Langfuse.test_langchain.pycovers the prefer-structured-inputs path end-to-end using a payload that would have failed JSON parsing under the old behavior.Confidence Score: 4/5
The change is a safe, minimal addition with a correct fallback; the only rough edge is that structured inputs end up stored redundantly in metadata, which doesn't break anything but creates noise in observations.
The fix correctly resolves a real serialisation problem and the new test covers the targeted scenario well. The one gap is that
kwargs["inputs"]continues to be swept intometadataby the unconditionalmeta.update(kwargs)loop that already existed, so structured inputs will appear twice in every affected observation — once asinputand once asmetadata["inputs"]. This is a minor quality issue rather than a correctness bug, and it does not affect the main goal of the PR.The
meta.updateblock inlangfuse/langchain/CallbackHandler.pyis worth revisiting to exclude theinputskey when it is promoted to the primaryinputfield.Sequence Diagram
%%{init: {'theme': 'neutral'}}%% sequenceDiagram participant LC as LangChain Runtime participant CB as CallbackHandler participant LF as Langfuse Observation LC->>CB: "on_tool_start(serialized, input_str, inputs=structured_dict)" CB->>CB: "tool_input = kwargs["inputs"] (structured dict)" note over CB: fallback to input_str if inputs absent CB->>CB: "meta = build_metadata(kwargs excl. inputs)" CB->>LF: "start_observation(input=tool_input, metadata=meta)" LC->>CB: on_tool_end(output) CB->>LF: "update_observation(output=output)"%%{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"}}}%% sequenceDiagram participant LC as LangChain Runtime participant CB as CallbackHandler participant LF as Langfuse Observation LC->>CB: "on_tool_start(serialized, input_str, inputs=structured_dict)" CB->>CB: "tool_input = kwargs["inputs"] (structured dict)" note over CB: fallback to input_str if inputs absent CB->>CB: "meta = build_metadata(kwargs excl. inputs)" CB->>LF: "start_observation(input=tool_input, metadata=meta)" LC->>CB: on_tool_end(output) CB->>LF: "update_observation(output=output)"Prompt To Fix All With AI
Reviews (1): Last reviewed commit: "fix(langchain): prefer structured tool i..." | Re-trigger Greptile