Core session coordination and API client for Tuner observability SDKs.
Project description
tuner-core
Session coordination, configuration, and API client for the Tuner
observability SDKs. Zero provider dependencies — every role package
(tuner-stt-observer, tuner-tts-observer, tuner-llm-observer,
tuner-langchain) depends on this; this package depends on nothing else in
the SDK.
Installation
pip install tuner-core
tuner-core alone is enough if you're calling session._record_user_turn()
/ _record_agent_turn() directly (e.g. bridging a framework like LiveKit or
Pipecat that owns its own provider connections). Add a role package to get
automatic recording for a specific provider.
TunerConfig
Single configuration object, built from environment variables or explicit kwargs — explicit kwargs always win.
from tuner_core import TunerConfig
config = TunerConfig.from_env(
asr_model="nova-2",
llm_model="gpt-4o-mini",
tts_model="sonic-2",
cost_calculator=my_cost_fn, # optional — receives a CallUsage, returns cents
)
| Env var | Required | Purpose |
|---|---|---|
TUNER_API_KEY |
Yes | Bearer token (tr_api_...) |
TUNER_WORKSPACE_ID |
Yes | Integer workspace ID |
TUNER_AGENT_ID |
Yes | Agent identifier from Agent Settings |
TUNER_BASE_URL |
No | Defaults to https://api.usetuner.ai |
TUNER_DEBUG |
No | "true" to log the full payload on flush |
AGENT_VERSION |
No | Integer agent version tag (note: no TUNER_ prefix) |
TunerSession
One session per call. Attach handlers as you create provider connections;
call flush() when the call ends.
from tuner_core import TunerSession
session = TunerSession(config=config, call_id=call_id)
handler = session.attach(SomeHandler()) # returns the handler for chaining
await session.flush() # POSTs the assembled call payload; never raises
attach() supports two kinds of handlers — see below. It returns whatever
you pass in, so adapter = session.attach(CartesiaAdapter()) works in one
line.
Two handler contracts
1. Transcript collectors — extend BaseTunerHandler
For handlers that record turns as they happen: STT and TTS adapters.
TunerSession.attach() detects the BaseTunerHandler subclass and injects
itself via _bind(), so the handler can call _record_user_turn() /
_record_agent_turn() / _record_stt_usage() / _record_tts_usage()
directly.
# tuner_stt_observer.DeepgramAdapter does this
class DeepgramAdapter(BaseSTTAdapter): # BaseSTTAdapter extends BaseTunerHandler
def _on_transcript(self, event):
self._record_user_turn(text=..., timestamp_ms=...)
2. LLM trace providers — implement get_segments() (coming)
For LLM framework handlers that accumulate execution data. No inheritance
from tuner-core needed — TunerSession finds them via duck typing.
Useful TunerSession state
session.last_user_end_ms— relative ms when the last user utterance ended; TTS adapters read this to computee2e_latency.session.last_llm_duration_ms— most recent LLM turn's processing time; TTS adapters attach it asllm_node_ttft.session.disconnection_reason— settable; pass aDisconnectReasonvalue beforeflush().
Full example — custom FastAPI + LangGraph + Deepgram + Cartesia
import os
import uuid
from fastapi import FastAPI, WebSocket
from cartesia import AsyncCartesia
from tuner_core import TunerConfig, TunerSession
from tuner_stt_observer import DeepgramAdapter
from tuner_tts_observer import CartesiaAdapter
from tuner_langchain import wrap_graph
app = FastAPI()
_agent = build_agent() # your LangGraph graph
@app.websocket("/call")
async def handle_call(websocket: WebSocket):
await websocket.accept()
call_id = str(uuid.uuid4())
config = TunerConfig.from_env(asr_model="nova-2", llm_model="gpt-4o-mini", tts_model="sonic-2")
session = TunerSession(config=config, call_id=call_id)
# STT — attach before connection.start()
dg_connection = dg_client.listen.asyncwebsocket.v("1")
dg_adapter = DeepgramAdapter(connection=dg_connection)
session.attach(dg_adapter)
await dg_connection.start(options)
# TTS — attach once per call, used per utterance
cartesia_client = AsyncCartesia(api_key=os.environ["CARTESIA_API_KEY"])
cartesia_adapter = session.attach(CartesiaAdapter())
# LLM — wraps the graph, exposes get_segments() for duck typing
instrumented_graph = wrap_graph(_agent)
session.attach(instrumented_graph)
# In your agent turn loop:
# result = await instrumented_graph.ainvoke({"messages": history})
# agent_text = result["messages"][-1].content
#
# async with cartesia_adapter.track_ws(agent_text) as tracked:
# async for chunk in tracked(ctx.receive()):
# if chunk.audio:
# await websocket.send_bytes(chunk.audio)
await session.flush() # at call end
What gets captured automatically
| Signal | Source | Handler |
|---|---|---|
| User transcript | Deepgram Transcript/UtteranceEnd events |
DeepgramAdapter |
| Turn timestamps | Provider event timing + stream open time | DeepgramAdapter / SpeechmaticsAdapter |
| STT latency | User speech end → transcript delta | SpeechmaticsAdapter / DeepgramAdapter |
| Agent transcript | Text passed to CartesiaAdapter.track() / track_ws() |
CartesiaAdapter |
| TTS TTFB | Synthesis request → first audio chunk | CartesiaAdapter |
| E2e latency | User speech end → agent first audio byte | CartesiaAdapter (reads session.last_user_end_ms) |
| LLM latency | Graph/chain invocation duration | wrap_graph / wrap_chain (tuner-langchain) |
| Tool calls + results | Graph/chain callbacks | wrap_graph / wrap_chain (tuner-langchain) |
Submission
submit_call() (in client.py) never raises — failures are logged and
swallowed so a Tuner outage can't crash the voice agent it's observing.
- Retries on
429/5xxand network/timeout errors; abandons immediately on other4xx. - Backoff:
1s, 2s, 4s+ up to 500ms jitter,max_retriesattempts (default 3). 409(duplicate call) is treated as success and logged, not retried.
Package install matrix
| Stack | Install | Status |
|---|---|---|
| Custom stack + LangGraph + Deepgram/Speechmatics + Cartesia | tuner-core tuner-stt-observer tuner-tts-observer tuner-langchain |
✅ Supported |
| Custom stack, OpenAI/Anthropic LLM only | tuner-core tuner-llm-observer |
✅ Supported (OpenAI adapter only — see tuner-llm-observer) |
| LiveKit — dedicated integration package | tuner-livekit-sdk |
✅ Supported |
| Pipecat — dedicated integration package | tuner-pipecat-sdk |
✅ Supported |
Development
uv sync
uv run pytest
uv run ruff check .
uv run mypy
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file tuner_core-0.1.0.tar.gz.
File metadata
- Download URL: tuner_core-0.1.0.tar.gz
- Upload date:
- Size: 17.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
34d751890b640eb13d19139f9c66145b56392966ab58149d4847c13415c3868c
|
|
| MD5 |
8004dc2aac1a2747da69d0bc54fd339b
|
|
| BLAKE2b-256 |
5d5ec4125fb7431af2a6d9524a68f1e145b10caf2454eb5f1b74b0751a66e289
|
File details
Details for the file tuner_core-0.1.0-py3-none-any.whl.
File metadata
- Download URL: tuner_core-0.1.0-py3-none-any.whl
- Upload date:
- Size: 16.3 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
541ffe8b10c3b3e261c20a95c35823a55962dd32256d414c6e56c77908f0dfb0
|
|
| MD5 |
5036b16e390acacb45ae9826c7491615
|
|
| BLAKE2b-256 |
1c3baf4ca1207ec8f6e82a3a703f23738f55b1b59e43f888ff6fef79aa4e7198
|