tuner-langchain
LangChain and LangGraph observability for Tuner SDKs.
Captures node transitions, tool calls, and tool results from LangChain and LangGraph agents and feeds them into the Tuner transcript pipeline — giving you full visibility into what your orchestration layer did on every call.
Overview
When users run LangChain or LangGraph as the orchestration layer inside a LiveKit or Pipecat voice agent, tool calls and node transitions happen inside the graph — invisible to the voice framework. This package bridges that gap.
It works by attaching a LangChain callback handler to the graph invocation. The handler captures every node transition and tool call with wall-clock timestamps, and the Tuner SDK mappers inject them into the transcript at flush time — in the correct position between the user message and the bot response.
Installation
Not yet on PyPI. Install locally from the repo:
pip install -e /path/to/tuner-langchain
Once published:
pip install tuner-langchain
Requirements: Python ≥ 3.10, langchain-core >= 1.0, < 2.0
Usage
With LiveKit
from livekit.agents import AgentSession, JobContext, Agent
from livekit.plugins import langchain
from tuner import TunerPlugin
async def entrypoint(ctx: JobContext):
session = AgentSession(...)
plugin = TunerPlugin(session, ctx)
wrapped_graph = plugin.wrap_graph(my_compiled_graph)
agent = Agent(
instructions="",
llm=langchain.LLMAdapter(
wrapped_graph,
stream_mode="custom",
config={"configurable": {"thread_id": thread_id}},
),
)
await session.start(agent=agent, room=ctx.room)
For plain LangChain (non-graph):
wrapped_chain = plugin.wrap_chain(my_chain)
agent = Agent(
instructions="",
llm=langchain.LLMAdapter(
wrapped_chain,
stream_mode="messages",
config={"configurable": {"thread_id": thread_id}},
),
)
What gets captured
Node transitions (node_transition)
Every named LangGraph node or LangChain chain step:
| Field | Description |
|---|---|
node_name |
The node/step name as defined in the graph |
start_ms |
Start time relative to call start |
end_ms |
End time relative to call start |
duration_ms |
Execution time in milliseconds |
inputs |
Node inputs (omitted when empty) |
outputs |
Node outputs (omitted when empty) |
error |
Error message if the node failed |
node_instructions |
System prompt active during this node's LLM call |
LangGraph internal nodes (__start__, __end__, compiled graph root) are filtered out automatically.
Tool calls (agent_function + agent_result)
Every tool invocation inside the graph:
| Field | Description |
|---|---|
tool_name |
Tool name |
inputs |
Raw input string passed to the tool |
output |
Tool result or error message |
is_error |
Whether the tool raised an error |
duration_ms |
Tool execution time in milliseconds |
start_ms |
Invocation time relative to call start |
Data Privacy
By default, tuner-langchain forwards the following data to the Tuner ingestion API:
| Field | Captured by default | How to disable |
|---|---|---|
| Node instructions | ✅ | node_instructions=False |
| Tool inputs | ✅ | tool_inputs=False |
| Tool outputs | ✅ | tool_outputs=False |
| Node inputs | ✅ | node_inputs=False |
| Node outputs | ✅ | node_outputs=False |
Node instructions are capped at 300 characters. Tool error output is always
captured regardless of tool_outputs — errors are not considered sensitive
and are required for debugging.
To disable specific fields, pass a CaptureConfig to wrap_graph() or wrap_chain():
from tuner_langchain import CaptureConfig
wrapped_graph = plugin.wrap_graph(
my_graph,
capture=CaptureConfig(
node_instructions=False,
tool_inputs=False,
tool_outputs=False,
node_inputs=False,
node_outputs=False,
)
)
How it fits in the transcript
All segments are sorted chronologically by start_ms — the correct execution order:
user message
node_transition "intent_classifier" (start_ms, end_ms, duration_ms)
node_transition "booking_node" (start_ms, end_ms, inputs, outputs)
agent_function "get_patient_info" (start_ms, inputs)
agent_result "get_patient_info" (start_ms, output, duration_ms)
agent text response
user message
...
The segment shape is identical to what Tuner produces for non-LangGraph tool calls — same role, same tool object structure, same timing fields. The Tuner API and frontend handle both paths transparently.
Architecture
src/tuner_langchain/
├── __init__.py public API
├── models.py NodeTransition, ToolCallEvent, GraphInvocation
├── accumulator.py TunerAccumulator — stores events per session
├── segment_builder.py segments_from_invocation() — used by SDK mappers at flush
└── handlers/
├── __init__.py
├── base.py TunerBaseHandler — shared tool + chain-end logic
├── langgraph.py TunerLangGraphHandler — filters __start__/__end__
└── langchain.py TunerLangChainHandler — filters anonymous wrappers
TunerBaseHandler holds all shared logic. TunerLangGraphHandler and TunerLangChainHandler only override on_chain_start — the one place where the two frameworks differ in how chain names are interpreted.
Public API
from tuner_langchain import (
# Handlers — pass to graph/chain config
TunerLangGraphHandler,
TunerLangChainHandler,
# Accumulator — one per session, created by wrap_graph() / wrap_chain()
TunerAccumulator,
# Segment builder — used internally by SDK mappers at flush time
segments_from_invocation,
# Models
GraphInvocation,
NodeTransition,
ToolCallEvent,
)
Development
uv sync
uv run pytest
uv run ruff check .
uv run mypy
Metadata
Release files for tuner-langchain 0.1.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| tuner_langchain-0.1.1.tar.gz | 20.5 kB | Without provenance | Details for tuner_langchain-0.1.1.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| tuner_langchain-0.1.1-py3-none-any.whl | Python 3 | none | any | Details for tuner_langchain-0.1.1-py3-none-any.whl |
Total release size: 38.2 kB
Release files / tuner_langchain-0.1.1.tar.gz
| Download URL | tuner_langchain-0.1.1.tar.gz |
|---|---|
| Size | 20.5 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
279121ed509d84b19667fb190441254b6a85966b87fd974b7be6c261a1665d73
|
|
BLAKE2b-256 checksum How to use checksums |
df5f0c53ebea11254d4794fa0286a53c3f6151db5ca9809ab0fd81f369bf3084
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.12.7
|
Release files / tuner_langchain-0.1.1-py3-none-any.whl
| Download URL | tuner_langchain-0.1.1-py3-none-any.whl |
|---|---|
| Size | 17.7 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
c57df2e5c3760599bd2ec4aa5ac68b9782b050824567280fc122e3fa90d9a636
|
|
BLAKE2b-256 checksum How to use checksums |
6e7cece66e96b298991dca42fba1e7f01a7e0c9ba6705b47c4e9da6f50cc4efd
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.12.7
|
Security for tuner-langchain 0.1.1
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.