What it does
Every call has two layers:- Voice layer: STT hears the user and TTS speaks the reply (your transport). You already track this.
- Brain layer: LangGraph decides which step runs next, and LangChain runs the LLM and tools inside each step. This is invisible unless you instrument it.
tuner-langchain makes the brain layer visible. It records node transitions, timing, and tool calls automatically, with no hand-wrapping of individual nodes or tools.
How you attach it depends on your stack. On LiveKit and Pipecat, the host SDK wraps your graph and flushes the events for you. On a custom stack with tuner-core, the TunerSession does the same. On a custom stack without tuner-core, you attach a callback handler and include the resulting segments in your own Create Call payload.
How it works
Three pieces do the work. A simple way to picture them:- Handler (
TunerLangGraphHandlerorTunerLangChainHandler): a LangChain callback. It’s passed toinvoke(); you never call its methods yourself. - Accumulator (
TunerAccumulator): one per call. It stores every event the brain layer produced. - Segment builder (
segments_from_invocation()): converts one turn’s events into Tuner transcript rows, with timestamps as milliseconds since call start.
tuner-core, wrap_graph() wires all three for you.
One handler sees both frameworks. LangGraph is built on LangChain, so a single handler on
graph.invoke() captures the graph’s nodes and the LLM/tool calls nested inside them, with no extra wiring per LLM.Attach to your stack
Pick your host. Each tab covers the install and the full wiring for that stack.- Custom stack
- LiveKit
- Pipecat
See Custom stack for the surrounding integration. How you attach depends on whether you use Requires Python ≥ 3.10 and Use
tuner-core.Install
langchain-core 1.x. Pin a version in production. See the package on PyPI.With tuner-core
If you use aTunerSession from tuner-core (see the SDK quickstart), wrap the graph and attach it to the session. The session adds the graph’s segments to the call when you run flush().wrap_chain() for a plain LangChain runnable. The wrapper adds its callback to ainvoke() and astream() calls only, so call the graph asynchronously.Without tuner-core
You attach the handler yourself and include the segments in your own Create Call payload. You touch the library at three moments in a call: set up once, attach the handler on every turn, then format and send when the call ends.1
Set up once per call
Create one accumulator (the notebook) and one handler (the ears).
2
Attach the handler on every turn
Pass the handler in
config each time you run the graph or chain. This is the only wiring you add.3
Format and send when the call ends
Turn the captured events into transcript rows, merge them with your own voice turns, and POST.Then include
transcript in your Create Call request.Data privacy
By default the library forwards node inputs, outputs, system prompts, and tool payloads. Pass aCaptureConfig to suppress what you don’t need:
Public API
The symbols below are what you use on a custom stack withouttuner-core. With tuner-core, LiveKit or Pipecat, you only need wrap_graph() or wrap_chain() and CaptureConfig.