Skip to main content

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 compute e2e_latency.
  • session.last_llm_duration_ms — most recent LLM turn's processing time; TTS adapters attach it as llm_node_ttft.
  • session.disconnection_reason — settable; pass a DisconnectReason value before flush().

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/5xx and network/timeout errors; abandons immediately on other 4xx.
  • Backoff: 1s, 2s, 4s + up to 500ms jitter, max_retries attempts (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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

tuner_core-0.1.0.tar.gz (17.9 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

tuner_core-0.1.0-py3-none-any.whl (16.3 kB view details)

Uploaded Python 3

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

Hashes for tuner_core-0.1.0.tar.gz
Algorithm Hash digest
SHA256 34d751890b640eb13d19139f9c66145b56392966ab58149d4847c13415c3868c
MD5 8004dc2aac1a2747da69d0bc54fd339b
BLAKE2b-256 5d5ec4125fb7431af2a6d9524a68f1e145b10caf2454eb5f1b74b0751a66e289

See more details on using hashes here.

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

Hashes for tuner_core-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 541ffe8b10c3b3e261c20a95c35823a55962dd32256d414c6e56c77908f0dfb0
MD5 5036b16e390acacb45ae9826c7491615
BLAKE2b-256 1c3baf4ca1207ec8f6e82a3a703f23738f55b1b59e43f888ff6fef79aa4e7198

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page