mirror of
https://github.com/hwchase17/langchain.git
synced 2026-07-13 12:14:06 +00:00
fix(core): preserve structured inputs on tool runs in tracers (#37108)
Tool runs in `_TracerCore._create_tool_run` were discarding the
structured `inputs` dict that `BaseTool.run` passes to `on_tool_start`,
replacing it with `{"input": str(filtered_tool_input)}`. Consequently,
every multi-arg tool (e.g. ones in `deepagents` like `execute`,
`edit_file`, `write_file`, `grep`, ...) appeared in LangSmith with a
stringified, escaped dump of its arguments — multi-line bash commands
rendered with `\n` and were effectively unreadable. Chain runs already
preserved dicts via `_get_chain_inputs`; tool runs are now symmetric.
## Changes
- Preserve `inputs` when it is already a `dict` in the `original` /
`original+chat` branch of `_TracerCore._create_tool_run`, falling back
to `{"input": input_str}` only when no structured payload was provided
- Add regression tests in the sync and async base-tracer suites that
pass a structured `inputs` to `on_tool_start` and assert the dict
survives onto the resulting `Run`
## Breaking change
Custom `BaseTracer` subclasses that parsed `Run.inputs["input"]` as a
stringified dict for tool runs will need to read the structured fields
directly. The shape now matches what `on_tool_start(inputs=...)` has
always received — introduced alongside `_schema_format` in the
`astream_events` work — and what `streaming_events` consumers already
see.
This commit is contained in:
@@ -449,7 +449,7 @@ class _TracerCore(ABC):
|
||||
kwargs.update({"metadata": metadata})
|
||||
|
||||
if self._schema_format in {"original", "original+chat"}:
|
||||
inputs = {"input": input_str}
|
||||
inputs = inputs if isinstance(inputs, dict) else {"input": input_str}
|
||||
elif self._schema_format == "streaming_events":
|
||||
inputs = {"input": inputs}
|
||||
else:
|
||||
|
||||
@@ -214,6 +214,40 @@ async def test_tracer_tool_run() -> None:
|
||||
assert tracer.runs == [compare_run]
|
||||
|
||||
|
||||
@freeze_time("2023-01-01")
|
||||
async def test_tracer_tool_run_preserves_structured_inputs() -> None:
|
||||
"""Structured `inputs` from `BaseTool.run` should not be flattened to `str`."""
|
||||
uuid = uuid4()
|
||||
structured_inputs = {"command": "echo 'hello\nworld'", "timeout": None}
|
||||
compare_run = Run(
|
||||
id=str(uuid),
|
||||
name="tool",
|
||||
start_time=datetime.now(timezone.utc),
|
||||
end_time=datetime.now(timezone.utc),
|
||||
events=[
|
||||
{"name": "start", "time": datetime.now(timezone.utc)},
|
||||
{"name": "end", "time": datetime.now(timezone.utc)},
|
||||
],
|
||||
extra={},
|
||||
serialized={"name": "tool"},
|
||||
inputs=structured_inputs,
|
||||
outputs={"output": "ok"},
|
||||
error=None,
|
||||
run_type="tool",
|
||||
trace_id=uuid,
|
||||
dotted_order=f"20230101T000000000000Z{uuid}",
|
||||
)
|
||||
tracer = FakeAsyncTracer()
|
||||
await tracer.on_tool_start(
|
||||
serialized={"name": "tool"},
|
||||
input_str=str(structured_inputs),
|
||||
run_id=uuid,
|
||||
inputs=structured_inputs,
|
||||
)
|
||||
await tracer.on_tool_end("ok", run_id=uuid)
|
||||
assert tracer.runs == [compare_run]
|
||||
|
||||
|
||||
@freeze_time("2023-01-01")
|
||||
async def test_tracer_nested_run() -> None:
|
||||
"""Test tracer on a nested run."""
|
||||
|
||||
@@ -217,6 +217,40 @@ def test_tracer_tool_run() -> None:
|
||||
assert tracer.runs == [compare_run]
|
||||
|
||||
|
||||
@freeze_time("2023-01-01")
|
||||
def test_tracer_tool_run_preserves_structured_inputs() -> None:
|
||||
"""Structured `inputs` from `BaseTool.run` should not be flattened to `str`."""
|
||||
uuid = uuid4()
|
||||
structured_inputs = {"command": "echo 'hello\nworld'", "timeout": None}
|
||||
compare_run = Run(
|
||||
id=str(uuid),
|
||||
name="tool",
|
||||
start_time=datetime.now(timezone.utc),
|
||||
end_time=datetime.now(timezone.utc),
|
||||
events=[
|
||||
{"name": "start", "time": datetime.now(timezone.utc)},
|
||||
{"name": "end", "time": datetime.now(timezone.utc)},
|
||||
],
|
||||
extra={},
|
||||
serialized={"name": "tool"},
|
||||
inputs=structured_inputs,
|
||||
outputs={"output": "ok"},
|
||||
error=None,
|
||||
run_type="tool",
|
||||
trace_id=uuid,
|
||||
dotted_order=f"20230101T000000000000Z{uuid}",
|
||||
)
|
||||
tracer = FakeTracer()
|
||||
tracer.on_tool_start(
|
||||
serialized={"name": "tool"},
|
||||
input_str=str(structured_inputs),
|
||||
run_id=uuid,
|
||||
inputs=structured_inputs,
|
||||
)
|
||||
tracer.on_tool_end("ok", run_id=uuid)
|
||||
assert tracer.runs == [compare_run]
|
||||
|
||||
|
||||
@freeze_time("2023-01-01")
|
||||
def test_tracer_nested_run() -> None:
|
||||
"""Test tracer on a nested run."""
|
||||
|
||||
Reference in New Issue
Block a user