> ## Documentation Index
> Fetch the complete documentation index at: https://docs.usetuner.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Quickstart

> Add Tuner observability to a custom Python voice stack — a session, a few attached observers, one flush at call end.

<Note>
  This guide uses Tuner's observability libraries for custom voice stacks: [`tuner-core`](https://pypi.org/project/tuner-core/), [`tuner-stt-observer`](https://pypi.org/project/tuner-stt-observer/), [`tuner-tts-observer`](https://pypi.org/project/tuner-tts-observer/), and [`tuner-langchain`](https://pypi.org/project/tuner-langchain/).
</Note>

This guide shows the Tuner lines you add to a voice agent you already have. The examples assume Deepgram for STT, LangGraph for the LLM layer, and Cartesia for TTS — swap any component you don't use. Your audio loops, agent logic, and synthesis code stay exactly as they are; Tuner observes them through each provider's native event system.

## Prerequisites

* Tuner Active Account
* Configured Agent with provider "Custom API" in Tuner
* Python ≥ 3.10 and a custom voice stack you control

## The mental model

Every Tuner library follows the same three-step lifecycle:

<Steps>
  <Step title="Create one TunerSession per call">
    The session coordinates everything and holds your call's identity — `call_id`, workspace, and agent mapping.
  </Step>

  <Step title="Attach observers">
    Each observer hooks into one component of your stack — STT, TTS, or your LLM framework — using that component's native event system.
  </Step>

  <Step title="flush() at call end">
    The session assembles everything the observers captured into a single structured timeline and sends it to Tuner via the [Create Call API](/docs/api-reference/create-call). One network call, at the end.
  </Step>
</Steps>

## Step 1: Install the libraries

Full stack from this guide:

```shell theme={null} theme={null}
pip install tuner-core "tuner-stt-observer[deepgram]" "tuner-tts-observer[cartesia]" tuner-langchain
```

`tuner-core` provides `TunerSession` and `TunerConfig`. Provider SDKs are behind extras so you only pull in what you use.

## Step 2: Set Your Credentials

`TunerConfig.from_env()` reads your credentials from environment variables:

```shell theme={null} theme={null}
export TUNER_API_KEY="tr_api_..."
export TUNER_WORKSPACE_ID="123"
export TUNER_AGENT_ID="fa4da74c-..."
```

| Variable             | Required | Description                                                                                           |
| -------------------- | -------- | ----------------------------------------------------------------------------------------------------- |
| `TUNER_API_KEY`      | ✅        | Bearer token (starts with `tr_api_`)                                                                  |
| `TUNER_WORKSPACE_ID` | ✅        | Your Tuner workspace ID                                                                               |
| `TUNER_AGENT_ID`     | ✅        | **Agent Remote ID** from **Agent Settings → Agent Connection** (UUID — not your agent's display name) |

## Step 3: Create a session per call

At the start of each call, create one `TunerSession`. The model names you pass are used for cost calculation. `call_id` is your unique identifier for the call — keep it stable across retries so ingestion stays idempotent.

```python theme={null} theme={null}
from tuner_core import TunerConfig, TunerSession

session = TunerSession(
    config=TunerConfig.from_env(
        asr_model="nova-2",
        llm_model="gpt-4o-mini",
        tts_model="sonic-2",
    ),
    call_id=call_id,  # your unique, retry-stable identifier for this call
)
```

## Step 4: Attach observers

Attach only the ones that match your stack — each is independent.

### STT — Deepgram

Attach the adapter to the Deepgram connection you already have. It's a pure observer — it captures complete user utterances, turn timing, STT latency, and STT usage; it never touches your audio.

```python theme={null} theme={null}
from tuner_stt_observer import DeepgramAdapter

stt = DeepgramAdapter(connection=dg_connection)   # your existing Deepgram connection
stt.on_utterance = handle_user_utterance          # your callback: complete user turn text
session.attach(stt)
```

<Warning>
  Your `LiveOptions` **must** set `utterance_end_ms` (e.g. `"1000"`) and `vad_events=True`. Without them, `UtteranceEnd` never fires and the utterance buffer never flushes — you'll see calls with no user turns.
</Warning>

Using Speechmatics instead? See [Speech-to-Text](/docs/api-and-integrations/libraries/tuner-stt).

### LLM — LangGraph

Wrap your graph and attach it. Node transitions, per-node LLM latency, and tool calls and results all land in the call timeline. Invoke the wrapped graph exactly as you would the original.

```python theme={null} theme={null}
from tuner_langchain import wrap_graph

graph = session.attach(wrap_graph(your_graph))

# then, in your agent logic, invoke as usual:
result = await graph.ainvoke({"messages": history})
```

Privacy controls and plain-chain support are covered in [LangChain / LangGraph](/docs/api-and-integrations/libraries/tuner-langchain).

### TTS — Cartesia

Wrap your synthesis stream with `track_ws()`. Tuner observes synthesis — it never owns or drives it. When the user barges in and you stop streaming, Tuner records only the words actually spoken, not the full intended response.

```python theme={null} theme={null}
from tuner_tts_observer import CartesiaAdapter

tts = session.attach(CartesiaAdapter())

# around your existing Cartesia WebSocket receive loop:
async with tts.track_ws(agent_text) as tracked:
    async for chunk in tracked(ctx.receive()):    # ctx: your Cartesia context
        ...                                        # your playback / send code
        # break out on barge-in — Tuner records only what was spoken
```

<Note>
  Word-accurate interruption capture requires `add_timestamps=True` on your Cartesia context. An SSE pattern is available for HTTP-only stacks — see [Text-to-Speech](/docs/api-and-integrations/libraries/tuner-tts).
</Note>

`track_ws()` only records an interruption once your app reports one — building working barge-in also needs a detection signal, `mark_interrupted()`/`wait_for_interruption()`, and client-side playback stopping. See [Text-to-Speech](/docs/api-and-integrations/libraries/tuner-tts#what-your-app-owns).

## Step 5: Flush at call end

`flush()` is the only network call the libraries make. Put it in a `finally` block so the call reaches Tuner even when the connection drops or your agent loop raises:

```python theme={null} theme={null}
try:
    ...  # your call loop
finally:
    await session.flush()
```

Open **Tuner → Calls** to see the processed result.

## Mix and match

| Your stack                      | Install                                                                                                                                        |
| ------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
| Deepgram + LangGraph + Cartesia | `tuner-core "tuner-stt-observer[deepgram]" "tuner-tts-observer[cartesia]" tuner-langchain`                                                     |
| Speechmatics + Cartesia         | `tuner-core "tuner-stt-observer[speechmatics]" "tuner-tts-observer[cartesia]"`                                                                 |
| STT observability only          | `tuner-core "tuner-stt-observer[deepgram]"`                                                                                                    |
| LLM tracing only                | `tuner-core tuner-langchain`                                                                                                                   |
| LiveKit or Pipecat              | Use the [LiveKit](/docs/api-and-integrations/connecting-to-livekit) / [Pipecat](/docs/api-and-integrations/connecting-to-pipecat) SDKs instead |

Missing an adapter for one of your providers? Extend the base adapter for that role — see *Custom providers* on the [Speech-to-Text](/docs/api-and-integrations/libraries/tuner-stt) and [Text-to-Speech](/docs/api-and-integrations/libraries/tuner-tts) pages. The [Create Call API](/docs/api-reference/create-call) is the alternative to the libraries, not a supplement: each call is submitted exactly once, either by `session.flush()` or by your own request.

## Troubleshooting

<AccordionGroup>
  <Accordion title="Calls don't appear in Tuner">
    * Verify that `TUNER_AGENT_ID` is the **Agent Remote ID** from **Agent Settings → Agent Connection** (not your agent's display name).
    * Confirm `TUNER_WORKSPACE_ID` is correct.
    * Confirm `session.flush()` actually ran — put it in a `finally` block so exceptions don't skip it.
  </Accordion>

  <Accordion title="Unauthorized (401)">
    * Ensure `TUNER_API_KEY` starts with `tr_api_` and is valid.
    * Confirm the API key belongs to the correct workspace.
  </Accordion>

  <Accordion title="Calls have no user turns">
    * Your `LiveOptions` must set `utterance_end_ms` (e.g. `"1000"`) and `vad_events=True` — without them Deepgram never emits `UtteranceEnd` and the utterance buffer never flushes.
    * Confirm caller audio is routed through `stt.send_audio()`, not sent to the Deepgram connection directly.
  </Accordion>
</AccordionGroup>

## What's Next?

<CardGroup cols={2}>
  <Card title="Speech-to-Text" icon="microphone" href="/docs/api-and-integrations/libraries/tuner-stt">
    Deepgram and Speechmatics adapters and custom providers.
  </Card>

  <Card title="Text-to-Speech" icon="volume-high" href="/docs/api-and-integrations/libraries/tuner-tts">
    Cartesia over WebSocket or SSE, word-level interruption capture.
  </Card>

  <Card title="LangChain / LangGraph" icon="link" href="/docs/api-and-integrations/libraries/tuner-langchain">
    Node transitions, tool calls, and LLM latency in your call timeline.
  </Card>

  <Card title="Review your first call" icon="magnifying-glass" href="/user-guide/quick-start/how-to-review-your-first-call">
    See what Tuner does with the data once it lands.
  </Card>
</CardGroup>
