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

# OpenTelemetry Traces

> Instrument your voice AI agents with OpenTelemetry to capture detailed span-level traces in Cekura

export const CopyLlmPromptButton = ({prompt}) => {
  if (typeof window === 'undefined') return null;
  var copied = false;
  function handleClick() {
    if (copied) return;
    navigator.clipboard.writeText(prompt).then(function () {
      var btn = document.getElementById('ck-llm-btn');
      if (btn) {
        btn.textContent = 'Copied!';
        copied = true;
        setTimeout(function () {
          btn.textContent = 'Copy LLM Prompt';
          copied = false;
        }, 2000);
      }
    });
  }
  setTimeout(function () {
    var btn = document.getElementById('ck-llm-btn');
    if (btn) btn.onclick = handleClick;
  }, 50);
  return <button id="ck-llm-btn" style={{
    display: 'inline-flex',
    alignItems: 'center',
    gap: '6px',
    padding: '7px 14px',
    border: '1px solid rgba(0,0,0,0.15)',
    borderRadius: '8px',
    background: '#fff',
    cursor: 'pointer',
    fontSize: '13px',
    fontWeight: '500',
    fontFamily: 'inherit',
    color: '#374151'
  }}>
      Copy LLM Prompt
    </button>;
};

export const CopyPageButton = () => {
  if (typeof window !== 'undefined') {
    setTimeout(function () {
      if (document.getElementById('ck-tools')) return;
      var anchor = document.getElementById('content-area') || document.querySelector('.mdx-content');
      if (!anchor) return;
      if (!document.getElementById('ck-style')) {
        var s = document.createElement('style');
        s.id = 'ck-style';
        s.textContent = '#ck-tools{position:absolute;top:6px;right:0;z-index:100;font-family:inherit;}' + '.ck-row{display:inline-flex;align-items:stretch;border:1px solid rgba(0,0,0,0.15);border-radius:8px;overflow:hidden;background:#fff;}' + ':root.dark .ck-row{background:rgba(255,255,255,0.06);border-color:rgba(255,255,255,0.12);}' + '.ck-btn{padding:5px 12px;border:none;background:none;cursor:pointer;font-size:13px;font-weight:500;font-family:inherit;color:#374151;}' + ':root.dark .ck-btn{color:#d1d5db;}' + '.ck-btn:hover{background:rgba(0,0,0,0.04);}' + ':root.dark .ck-btn:hover{background:rgba(255,255,255,0.06);}' + '.ck-chevron{padding:5px 8px;border:none;background:none;cursor:pointer;font-size:14px;font-family:inherit;color:#374151;}' + ':root.dark .ck-chevron{color:#d1d5db;}' + '.ck-chevron:hover{background:rgba(0,0,0,0.04);}' + ':root.dark .ck-chevron:hover{background:rgba(255,255,255,0.06);}' + '.ck-divider{width:1px;background:rgba(0,0,0,0.12);flex-shrink:0;}' + ':root.dark .ck-divider{background:rgba(255,255,255,0.12);}' + '.ck-dd{position:absolute;top:calc(100% + 4px);right:0;min-width:180px;background:#fff;border:1px solid rgba(0,0,0,0.12);border-radius:8px;box-shadow:0 4px 12px rgba(0,0,0,0.1);padding:4px;display:none;z-index:200;}' + ':root.dark .ck-dd{background:#1f2937;border-color:rgba(255,255,255,0.1);box-shadow:0 4px 16px rgba(0,0,0,0.35);}' + '.ck-item{display:block;width:100%;padding:7px 12px;border:none;background:none;border-radius:6px;cursor:pointer;font-size:13px;font-family:inherit;text-align:left;color:#374151;}' + ':root.dark .ck-item{color:#d1d5db;}' + '.ck-item:hover{background:rgba(0,0,0,0.05);}' + ':root.dark .ck-item:hover{background:rgba(255,255,255,0.07);}';
        document.head.appendChild(s);
      }
      var wrap = document.createElement('div');
      wrap.id = 'ck-tools';
      var row = document.createElement('div');
      row.className = 'ck-row';
      var mainBtn = document.createElement('button');
      mainBtn.className = 'ck-btn';
      mainBtn.textContent = 'Copy page';
      var divider = document.createElement('span');
      divider.className = 'ck-divider';
      var chevron = document.createElement('button');
      chevron.className = 'ck-chevron';
      chevron.textContent = '▾';
      var dd = document.createElement('div');
      dd.className = 'ck-dd';
      function closeDD() {
        dd.style.display = 'none';
      }
      function openDD() {
        dd.style.display = 'block';
      }
      chevron.onclick = function (e) {
        e.stopPropagation();
        if (dd.style.display === 'block') {
          closeDD();
        } else {
          openDD();
        }
      };
      document.addEventListener('click', function (e) {
        if (!e.target.closest('#ck-tools')) {
          closeDD();
        }
      });
      document.addEventListener('keydown', function (e) {
        if (e.key === 'Escape') {
          closeDD();
        }
      });
      function makeItem(label, fn) {
        var b = document.createElement('button');
        b.className = 'ck-item';
        b.textContent = label;
        b.onclick = function () {
          fn();
          closeDD();
        };
        return b;
      }
      function getMarkdown() {
        var walk = function (node) {
          if (!node) return '';
          if (node.nodeType === 3) return node.textContent || '';
          if (node.nodeType !== 1) return '';
          var tag = node.tagName.toLowerCase();
          var skip = ['script', 'style', 'svg', 'noscript', 'button', 'iframe'];
          if (skip.indexOf(tag) !== -1) return '';
          if (node.id === 'ck-tools') return '';
          var ch = Array.from(node.childNodes).map(walk).join('');
          if (tag === 'h1') return '\n# ' + ch.trim() + '\n\n';
          if (tag === 'h2') return '\n## ' + ch.trim() + '\n\n';
          if (tag === 'h3') return '\n### ' + ch.trim() + '\n\n';
          if (tag === 'p') return '\n' + ch.trim() + '\n\n';
          if (tag === 'pre') return '\n```\n' + node.textContent.trim() + '\n```\n\n';
          if (tag === 'li') return '- ' + ch.trim() + '\n';
          if (tag === 'code') return '`' + ch.trim() + '`';
          return ch;
        };
        var content = document.querySelector('.mdx-content') || document.getElementById('content-area') || document.body;
        return walk(content).replace(/\n\n\n+/g, '\n\n').trim();
      }
      function copyMd() {
        var md = getMarkdown();
        navigator.clipboard.writeText(md).then(function () {
          mainBtn.textContent = 'Copied!';
          setTimeout(function () {
            mainBtn.textContent = 'Copy page';
          }, 2000);
        });
      }
      function viewMd() {
        var md = getMarkdown();
        var safe = md.split('&').join('&amp;').split('<').join('&lt;').split('>').join('&gt;');
        var html = '<!DOCTYPE html><html><head><meta charset="utf-8"><style>body{font-family:monospace;max-width:860px;margin:40px auto;padding:0 24px;line-height:1.7;white-space:pre-wrap;word-wrap:break-word}</style></head><body>' + safe + '</body></html>';
        window.open(URL.createObjectURL(new Blob([html], {
          type: 'text/html'
        })), '_blank');
      }
      function openClaude() {
        var prompt = 'Can you read this Cekura docs page ' + window.location.href + ' so I can ask you questions?';
        window.open('https://claude.ai/new?q=' + encodeURIComponent(prompt), '_blank');
      }
      mainBtn.onclick = copyMd;
      dd.appendChild(makeItem('Copy page', copyMd));
      dd.appendChild(makeItem('View as Markdown', viewMd));
      dd.appendChild(makeItem('Open in Claude', openClaude));
      row.appendChild(mainBtn);
      row.appendChild(divider);
      row.appendChild(chevron);
      wrap.appendChild(row);
      wrap.appendChild(dd);
      anchor.style.position = 'relative';
      anchor.insertBefore(wrap, anchor.firstChild);
    }, 50);
  }
  return null;
};

export const otelPrompt = ["You are integrating OpenTelemetry tracing into an existing voice AI agent codebase to export traces to Cekura.", "", "OBJECTIVE", "Instrument the agent with OpenTelemetry so that every call produces a trace with span-level timing for STT, LLM, TTS (or a single S2S span for realtime speech-to-speech models), and tool calls. Export traces to Cekura and link them to call logs via trace_id.", "", "BEFORE YOU START", '1. Ask the user: "What is your Cekura agent ID (or project ID)? You can find it in the Cekura dashboard. If you don\'t know it, I can look it up for you."', '2. Ask the user: "Do you prefer gRPC or HTTP for the OTel exporter? gRPC is recommended for most cases."', "3. Find the main conversation loop or call handler in the codebase (where STT, LLM, and TTS calls happen, or where a single realtime S2S model handles the turn).", "4. Check if OpenTelemetry is already configured in the codebase. If so, reuse the existing TracerProvider and just add the Cekura exporter.", "5. Find existing error handling and service patterns in the codebase. Follow them.", "", "IMPLEMENTATION REQUIREMENTS", "", "1. Install dependencies:", "   - gRPC: pip install opentelemetry-sdk opentelemetry-exporter-otlp-proto-grpc", "   - HTTP: pip install opentelemetry-sdk opentelemetry-exporter-otlp-proto-http", "", "2. Configure the OTel exporter with Cekura auth headers:", "   - gRPC endpoint: otel.cekura.ai:443", "   - HTTP endpoint: https://otel-http.cekura.ai/v1/traces", "   - Required headers: x-cekura-api-key (your API key) and x-cekura-agent-id (your agent ID)", "   - Alternative: use x-cekura-project-id instead of x-cekura-agent-id for project-level scoping.", "", "3. Set up TracerProvider with a BatchSpanProcessor and a Resource with service.name.", "", "4. Instrument agent code with spans using these naming conventions (Cekura renders specialized UI for these):", '   - "stt" for speech-to-text calls — set stt.provider, stt.transcript', '   - "llm" for LLM calls — set gen_ai.system, gen_ai.request.model, gen_ai.usage.input_tokens, gen_ai.usage.output_tokens', '   - "tts" for text-to-speech calls — set tts.provider, tts.characters', '   - "s2s" for speech-to-speech / realtime model calls (e.g. OpenAI Realtime, Gemini Live) — set gen_ai.provider.name, gen_ai.request.model, gen_ai.usage.input_tokens, gen_ai.usage.output_tokens', '   - "tool_call" for tool/function calls — set function.name, function.input, function.output', '   - Wrap the entire call in a root "conversation" span.', "", "5. Extract the trace_id from the root span using format_trace_id().", "", "6. When sending call logs to Cekura via the observe API (POST https://api.cekura.ai/observability/v1/observe/), include the trace_id field to link the trace to the call.", "", "7. Call provider.force_flush() before sending the call log to ensure all spans are exported.", "", "8. Store CEKURA_API_KEY and CEKURA_AGENT_ID in environment variables — never hardcode them.", "", "INTEGRATION POINTS", "- The OTel setup should be initialized once at module/application startup, not per-call.", "- Spans wrap existing service calls (STT, LLM, TTS, or a single S2S realtime model) — insert them around the existing code, do not restructure.", "- The trace_id links OTel traces to call logs in the Cekura dashboard — include it in the observe API payload.", "- If the codebase already has an OTel TracerProvider, add the Cekura exporter as an additional span processor rather than replacing the existing setup.", "", "SUCCESS CRITERIA", "- Every call produces an OTel trace with a root conversation span and child spans for STT, LLM, TTS (or S2S), and tool calls.", "- Traces appear in the Cekura dashboard linked to their corresponding call logs.", "- Span attributes include provider names, token usage, and timing information.", "- Credentials are read from environment variables.", "- Existing agent logic and call flow are unchanged.", "", "CONSTRAINTS", "- DO NOT restructure the existing agent or call handling logic.", "- DO NOT replace an existing OTel setup — add to it.", "- DO NOT hardcode API keys or agent IDs.", "- DO NOT add spans for code that is not a service call (no spans for internal logic or data transformations).", "- Custom span names work but won't have specialized styling in the Cekura UI — prefer the standard names above.", "", "REFERENCE IMPLEMENTATION", "", "```python", "import os", "from opentelemetry.sdk.trace import TracerProvider", "from opentelemetry.sdk.trace.export import BatchSpanProcessor", "from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter", "from opentelemetry.sdk.resources import Resource", "from opentelemetry.trace import format_trace_id", "", "CEKURA_API_KEY = os.getenv('CEKURA_API_KEY')", "AGENT_ID = os.getenv('CEKURA_AGENT_ID')", "", "exporter = OTLPSpanExporter(", "    endpoint='otel.cekura.ai:443',", "    headers=(", "        ('x-cekura-api-key', CEKURA_API_KEY),", "        ('x-cekura-agent-id', AGENT_ID),", "    ),", ")", "", "provider = TracerProvider(", "    resource=Resource.create({'service.name': 'my-voice-agent'})", ")", "provider.add_span_processor(BatchSpanProcessor(exporter))", "tracer = provider.get_tracer('my-voice-agent')", "", "def run_call(call_id, user_audio_stream):", "    with tracer.start_as_current_span('conversation') as root_span:", "        trace_id = format_trace_id(root_span.get_span_context().trace_id)", "        transcript = []", "", "        for audio_chunk in user_audio_stream:", "            with tracer.start_as_current_span('stt') as span:", "                text = stt.transcribe(audio_chunk)", "                span.set_attribute('stt.provider', 'deepgram')", "                span.set_attribute('stt.transcript', text)", "", "            with tracer.start_as_current_span('llm') as span:", "                response = llm.chat(transcript)", "                span.set_attribute('gen_ai.request.model', 'gpt-4o')", "                span.set_attribute('gen_ai.usage.input_tokens', response.usage.prompt_tokens)", "                span.set_attribute('gen_ai.usage.output_tokens', response.usage.completion_tokens)", "", "            with tracer.start_as_current_span('tts') as span:", "                audio = tts.synthesize(response.text)", "                span.set_attribute('tts.provider', 'elevenlabs')", "", "    provider.force_flush()", "    # Then POST call log to https://api.cekura.ai/observability/v1/observe/ with trace_id", "```", "", "NEXT STEPS", "Once tracing is integrated, use the Cekura MCP tools to:", "- Monitor production calls and evaluate agent performance from the Cekura dashboard.", "- Set up custom metrics to track latency, token usage, and quality across calls.", "If the Cekura MCP server is available in your environment, use its tools/skills to review call logs, set up metrics, and configure alerts — all without leaving your coding agent.", "", "ADDITIONAL REFERENCE", "For full setup details, span naming conventions, HTTP exporter config, and a complete example see: https://docs.cekura.ai/documentation/guides/tracing"].join("\n");

<CopyPageButton />

<Info>
  **Using a coding agent?** Paste this prompt directly in your voice agent codebase to jumpstart your Cekura OTel integration.

  <CopyLlmPromptButton prompt={otelPrompt} />
</Info>

## Overview

OpenTelemetry (OTel) tracing gives you deep visibility into your voice agent's execution - every LLM call, TTS request, STT transcription, and tool invocation captured as spans with timing, token usage, and metadata.

Once traces are flowing into Cekura, you can:

* View span timelines and waterfall diagrams for each call
* Identify latency bottlenecks (slow LLM responses, TTS delays)
* Track token usage and model performance across calls
* Debug tool call failures with full input/output context

<Info>
  **Using Pipecat?** The Cekura Python SDK instruments your pipeline automatically - no manual setup needed. See the [Pipecat Tracing](/documentation/integrations/pipecat/tracing) guide.
</Info>

## Prerequisites

* A Cekura account with an API key
* An Agent ID or Project ID from the Cekura dashboard
* Python 3.8+ (examples use Python, but any OTel-compatible language works)

## Install Dependencies

<Tabs>
  <Tab title="gRPC (Recommended)">
    ```bash theme={null}
    pip install opentelemetry-sdk opentelemetry-exporter-otlp-proto-grpc
    ```
  </Tab>

  <Tab title="HTTP">
    ```bash theme={null}
    pip install opentelemetry-sdk opentelemetry-exporter-otlp-proto-http
    ```
  </Tab>
</Tabs>

## Endpoints

| Protocol | Endpoint                  | When to use                                                                     |
| -------- | ------------------------- | ------------------------------------------------------------------------------- |
| gRPC     | `otel.cekura.ai:443`      | Recommended for most use cases. Lower overhead, better for high-volume tracing. |
| HTTP     | `otel-http.cekura.ai:443` | Use when gRPC is not available (e.g. environments that don't support HTTP/2).   |

## Authentication

Every request to the OTel endpoint requires two headers:

| Header                | Required | Description                                         |
| --------------------- | -------- | --------------------------------------------------- |
| `x-cekura-api-key`    | Yes      | Your Cekura API key                                 |
| `x-cekura-agent-id`   | Yes\*    | The agent ID to associate traces with               |
| `x-cekura-project-id` | Yes\*    | Alternative to agent ID - use project-level scoping |

\* Provide either `x-cekura-agent-id` or `x-cekura-project-id`.

These are passed as exporter headers (HTTP) or gRPC metadata, not as span attributes.

## Setup

<Steps>
  <Step title="Configure the OTel exporter">
    Set up the exporter with your Cekura credentials:

    <Tabs>
      <Tab title="gRPC">
        ```python theme={null}
        from opentelemetry.sdk.trace import TracerProvider
        from opentelemetry.sdk.trace.export import BatchSpanProcessor
        from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
        from opentelemetry.sdk.resources import Resource

        # Create the exporter with Cekura auth headers
        exporter = OTLPSpanExporter(
            endpoint="otel.cekura.ai:443",
            headers=(
                ("x-cekura-api-key", "your-api-key"),
                ("x-cekura-agent-id", "your-agent-id"),
            ),
        )

        # Set up the tracer provider
        resource = Resource.create({"service.name": "my-voice-agent"})
        provider = TracerProvider(resource=resource)
        provider.add_span_processor(BatchSpanProcessor(exporter))

        # Get a tracer
        tracer = provider.get_tracer("my-voice-agent")
        ```
      </Tab>

      <Tab title="HTTP">
        ```python theme={null}
        from opentelemetry.sdk.trace import TracerProvider
        from opentelemetry.sdk.trace.export import BatchSpanProcessor
        from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
        from opentelemetry.sdk.resources import Resource

        # Create the exporter with Cekura auth headers
        exporter = OTLPSpanExporter(
            endpoint="https://otel-http.cekura.ai/v1/traces",
            headers={
                "x-cekura-api-key": "your-api-key",
                "x-cekura-agent-id": "your-agent-id",
            },
        )

        # Set up the tracer provider
        resource = Resource.create({"service.name": "my-voice-agent"})
        provider = TracerProvider(resource=resource)
        provider.add_span_processor(BatchSpanProcessor(exporter))

        # Get a tracer
        tracer = provider.get_tracer("my-voice-agent")
        ```
      </Tab>
    </Tabs>
  </Step>

  <Step title="Instrument your agent code">
    Create spans around each service call in your agent. Use the span names from the [naming conventions](#span-naming-conventions) for best results in the Cekura UI.

    ```python theme={null}
    def handle_conversation_turn(user_audio):
        # Speech-to-text
        with tracer.start_as_current_span("stt") as stt_span:
            transcript = stt_service.transcribe(user_audio)
            stt_span.set_attribute("stt.provider", "deepgram")
            stt_span.set_attribute("stt.transcript", transcript)

        # LLM call
        with tracer.start_as_current_span("llm") as llm_span:
            response = llm_service.chat(transcript)
            llm_span.set_attribute("gen_ai.system", "openai")
            llm_span.set_attribute("gen_ai.request.model", "gpt-4o")
            llm_span.set_attribute("gen_ai.usage.input_tokens", response.usage.prompt_tokens)
            llm_span.set_attribute("gen_ai.usage.output_tokens", response.usage.completion_tokens)

        # Text-to-speech
        with tracer.start_as_current_span("tts") as tts_span:
            audio = tts_service.synthesize(response.text)
            tts_span.set_attribute("tts.provider", "elevenlabs")
            tts_span.set_attribute("tts.characters", len(response.text))

        return audio
    ```
  </Step>

  <Step title="Capture the trace ID">
    Extract the trace ID from your root span - you'll need this to correlate the trace with the call log in Cekura.

    ```python theme={null}
    from opentelemetry.trace import format_trace_id

    # Start a root span for the entire call
    with tracer.start_as_current_span("conversation") as root_span:
        # Get the trace ID (32-character hex string)
        trace_id = format_trace_id(root_span.get_span_context().trace_id)

        # Run your agent conversation loop
        while call_active:
            handle_conversation_turn(user_audio)

    # trace_id is now something like "4bf92f3577b34da6a3ce929d0e0e4736"
    ```
  </Step>

  <Step title="Send the trace ID with your call log">
    When sending the call log to Cekura via the [Send Calls](/api-reference/observability/send-calls) API, include the `trace_id` field. This links the OTel trace to the call in the dashboard.

    ```python theme={null}
    import requests

    requests.post(
        "https://api.cekura.ai/observability/v1/observe/",
        headers={
            "x-cekura-api-key": "your-api-key",
        },
        json={
            "agent": your_agent_id,
            "call_id": "unique-call-id",
            "trace_id": trace_id,  # 32-char hex from step 3
            "transcript_json": transcript_data,
            # ... other fields
        },
    )
    ```

    Once the call log is submitted with a `trace_id`, the trace will be visible in the call details page in the Cekura dashboard.
  </Step>
</Steps>

## Span Naming Conventions

Cekura recognizes these span names and renders them with specialized UI:

| Span Name   | Purpose                           | Recommended Attributes                                                                                    |
| ----------- | --------------------------------- | --------------------------------------------------------------------------------------------------------- |
| `stt`       | Speech-to-text                    | `stt.provider`, `stt.transcript`, `stt.confidence`                                                        |
| `llm`       | LLM / model call                  | `gen_ai.system`, `gen_ai.request.model`, `gen_ai.usage.input_tokens`, `gen_ai.usage.output_tokens`        |
| `tts`       | Text-to-speech                    | `tts.provider`, `tts.characters`                                                                          |
| `s2s`       | Speech-to-speech (realtime model) | `gen_ai.provider.name`, `gen_ai.request.model`, `gen_ai.usage.input_tokens`, `gen_ai.usage.output_tokens` |
| `tool_call` | Tool / function call              | `function.name`, `function.input`, `function.output`                                                      |

Custom span names work too - they just won't have specialized styling in the UI.

<Tip>
  Use `s2s` when a single realtime model handles the full audio-in/audio-out turn (e.g. OpenAI Realtime, Gemini Live), instead of a cascading `stt` → `llm` → `tts` pipeline:

  ```python theme={null}
  with tracer.start_as_current_span("s2s") as s2s_span:
      audio_out = realtime_model.respond(user_audio)
      s2s_span.set_attribute("gen_ai.provider.name", "openai")
      s2s_span.set_attribute("gen_ai.request.model", "gpt-4o-realtime-preview")
      s2s_span.set_attribute("gen_ai.usage.input_tokens", usage.input_tokens)
      s2s_span.set_attribute("gen_ai.usage.output_tokens", usage.output_tokens)
  ```
</Tip>

<Note>
  For workflows that require audit-grade explainability — capturing the reasoning behind individual agent decisions, not just pass/fail outcomes — see [Decision-Level Reasoning and Explainability](/documentation/FAQ#how-does-cekura-handle-decision-level-reasoning-and-explainability-and-what-data-do-i-need-to-provide-for-audit-purposes) in the FAQ.
</Note>

## Example: Full Voice Agent

Here's a complete example showing a voice agent with OTel tracing and Cekura integration:

```python theme={null}
import os
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
from opentelemetry.sdk.resources import Resource
from opentelemetry.trace import format_trace_id

CEKURA_API_KEY = os.getenv("CEKURA_API_KEY")
AGENT_ID = os.getenv("CEKURA_AGENT_ID")

# 1. Set up OTel tracing
exporter = OTLPSpanExporter(
    endpoint="otel.cekura.ai:443",
    headers=(
        ("x-cekura-api-key", CEKURA_API_KEY),
        ("x-cekura-agent-id", AGENT_ID),
    ),
)

provider = TracerProvider(
    resource=Resource.create({"service.name": "my-voice-agent"})
)
provider.add_span_processor(BatchSpanProcessor(exporter))
tracer = provider.get_tracer("my-voice-agent")


# 2. Agent conversation with tracing
def run_call(call_id, user_audio_stream):
    with tracer.start_as_current_span("conversation") as root_span:
        trace_id = format_trace_id(root_span.get_span_context().trace_id)
        transcript = []

        for audio_chunk in user_audio_stream:
            # STT
            with tracer.start_as_current_span("stt") as span:
                text = stt.transcribe(audio_chunk)
                span.set_attribute("stt.provider", "deepgram")
                span.set_attribute("stt.transcript", text)
                transcript.append({"role": "user", "content": text})

            # LLM
            with tracer.start_as_current_span("llm") as span:
                response = llm.chat(transcript)
                span.set_attribute("gen_ai.request.model", "gpt-4o")
                span.set_attribute("gen_ai.usage.input_tokens", response.usage.prompt_tokens)
                span.set_attribute("gen_ai.usage.output_tokens", response.usage.completion_tokens)
                transcript.append({"role": "assistant", "content": response.text})

            # TTS
            with tracer.start_as_current_span("tts") as span:
                audio = tts.synthesize(response.text)
                span.set_attribute("tts.provider", "elevenlabs")

            yield audio

    # 3. Flush traces and send call log to Cekura
    provider.force_flush()

    requests.post(
        "https://api.cekura.ai/observability/v1/observe/",
        headers={"x-cekura-api-key": CEKURA_API_KEY},
        json={
            "agent": int(AGENT_ID),
            "call_id": call_id,
            "trace_id": trace_id,
            "transcript_json": transcript,
        },
    )
```

## Next Steps

* [Pipecat Tracing](/documentation/integrations/pipecat/tracing) - auto-instrumentation for Pipecat agents
* [Send Calls API](/api-reference/observability/send-calls) - full API reference for the observe endpoint
* [Custom Metrics](/documentation/key-concepts/metrics/custom-metrics) - evaluate agent performance from your call data
* [Cekura MCP Server](/mcp/introduction) - use MCP tools to create tests, run simulations, and monitor production calls directly from your IDE or AI assistant
