### Install tuner-stt-observer Source: https://docs.usetuner.ai/docs/api-and-integrations/libraries/tuner-stt Install the core tuner library and the STT observer. This is the base installation required for using the STT observer. ```shell pip install tuner-core tuner-stt-observer ``` -------------------------------- ### Install Tuner Libraries Source: https://docs.usetuner.ai/docs/api-and-integrations/custom-integration/quickstart Install the core Tuner library and optional provider SDKs using pip. Install provider SDKs with extras like '[deepgram]' or '[cartesia]' to include only the necessary dependencies. ```shell pip install tuner-core "tuner-stt-observer[deepgram]" "tuner-tts-observer[cartesia]" tuner-langchain ``` -------------------------------- ### Minimal Tuner Langchain Starter Source: https://docs.usetuner.ai/docs/api-and-integrations/libraries/tuner-langchain This snippet demonstrates a basic setup for using TunerAccumulator and TunerLangGraphHandler with Langchain. It includes functions to handle individual turns and build a complete transcript at the end of a call. Ensure you have the necessary Tuner and Langchain libraries installed. ```python import time from tuner_langchain import TunerAccumulator, CaptureConfig from tuner_langchain.handlers import TunerLangGraphHandler from tuner_langchain.segment_builder import segments_from_invocation # Per call call_start_ms = int(time.time() * 1000) accumulator = TunerAccumulator(capture=CaptureConfig(tool_inputs=False)) handler = TunerLangGraphHandler(accumulator) voice_turns = [] # your own STT/TTS turns # Per turn def handle_turn(user_text: str, state: dict) -> str: voice_turns.append({"role": "user", "text": user_text, "start_ms": ...}) result = compiled_graph.invoke(state, config={"callbacks": [handler]}) response = result["response"] voice_turns.append({"role": "agent", "text": response, "start_ms": ...}) return response # At hang-up def build_transcript() -> list[dict]: call_start_ns = call_start_ms * 1_000_000 segments = [] for inv in accumulator.get_invocations(): segments.extend(segments_from_invocation(inv, call_start_ns)) return sorted(voice_turns + segments, key=lambda e: e.get("start_ms", 0)) # POST build_transcript() inside your Tuner Create Call payload ``` -------------------------------- ### Install Tuner for LLM Tracing Source: https://docs.usetuner.ai/docs/api-and-integrations/custom-integration/quickstart Install Tuner with the Langchain integration for LLM tracing capabilities. ```bash tuner-core tuner-langchain ``` -------------------------------- ### Install Tuner Core and TTS Observer Source: https://docs.usetuner.ai/docs/api-and-integrations/libraries/tuner-tts Install the necessary Tuner libraries for core functionality and TTS observation. ```shell pip install tuner-core tuner-tts-observer ``` -------------------------------- ### Install Speechmatics Adapter Source: https://docs.usetuner.ai/docs/api-and-integrations/libraries/tuner-stt Install specific STT provider adapters as optional extras to minimize dependencies. Use this command to install the Speechmatics adapter. ```shell pip install "tuner-stt-observer[speechmatics]" # SpeechmaticsAdapter ``` -------------------------------- ### Full Pipecat Bot Example Source: https://docs.usetuner.ai/docs/api-and-integrations/connecting-to-pipecat This Python code demonstrates a full example of setting up a Pipecat bot. It includes initializing observers, defining a cost calculator, and configuring a pipeline for call processing. Ensure you have the TUNER_API_KEY and TUNER_WORKSPACE_ID environment variables set. ```python import os import uuid from tuner_pipecat_sdk import Observer from tuner_pipecat_sdk.models import DisconnectReason from pipecat.observers.turn_tracking_observer import TurnTrackingObserver from pipecat.pipeline.pipeline import Pipeline from pipecat.pipeline.task import PipelineTask, PipelineParams async def run_bot(call_data, context, transport, stt, llm, tts, context_aggregator): # call_data is the payload from your telephony WebSocket handler # e.g. _, call_data = await parse_telephony_websocket(websocket) # Read the SIP Call-ID from the provider payload — the field name varies # Twilio example: forward it as a customParameter (see SIP section above) sip_call_id = call_data.get("customParameters", {}).get("SipCallId") turn_tracker = TurnTrackingObserver() _reason = None # update the rates to match your models def calculate_cost(usage) -> float: # OpenAI gpt-4o-mini pricing (per token / per character / per second) llm_cost = (usage.llm_prompt_tokens or 0) * 0.000_000_150 llm_cost += (usage.llm_completion_tokens or 0) * 0.000_000_600 # OpenAI TTS-1: $15 per 1M characters tts_cost = (usage.tts_characters or 0) * 0.000_015 # OpenAI gpt-4o-transcribe: $6 per 1M audio seconds stt_cost = usage.stt_audio_seconds * 0.000_006 return (llm_cost + tts_cost + stt_cost) * 100 observer = Observer( api_key=os.environ["TUNER_API_KEY"], workspace_id=int(os.environ["TUNER_WORKSPACE_ID"]), agent_id="fa4da74c-...", call_id=str(uuid.uuid4()), base_url="https://api.usetuner.ai", call_type="phone_call", asr_model="deepgram/nova-3", llm_model="gpt-4o-mini", tts_model="cartesia/sonic", sip_call_id=sip_call_id, disconnection_reason_resolver=lambda: _reason, cost_calculator=calculate_cost, ) observer.attach_turn_tracking_observer(turn_tracker) pipeline = Pipeline([ transport.input(), stt, context_aggregator.user(), llm, tts, transport.output(), context_aggregator.assistant(), ]) task = PipelineTask( pipeline, params=PipelineParams( enable_metrics=True, enable_usage_metrics=True, ), observers=[observer, observer.latency_observer, turn_tracker], ) _reason = DisconnectReason.USER_HANGUP ``` -------------------------------- ### Full TunerPlugin Configuration Example Source: https://docs.usetuner.ai/docs/api-and-integrations/connecting-to-livekit A comprehensive example demonstrating how to configure the TunerPlugin with API keys, workspace ID, agent ID, call type, recording URL resolver, cost calculator, metadata, agent version, timeouts, retries, and enablement. ```typescript import { TunerPlugin } from '@usetuner.ai/livekit-sdk'; import type { metrics } from '@livekit.ai/agents'; function calculateCost(usage: metrics.UsageSummary): number { const usd = usage.llmPromptTokens * 0.000_003 + usage.llmCompletionTokens * 0.000_015 + usage.ttsCharactersCount * 0.000_030; return usd * 100; // Tuner expects USD cents } async function getRecordingUrl(roomName: string, jobId: string): Promise { return (await myStorage.getUrl(jobId)) ?? 'pending'; } new TunerPlugin(session, ctx, { apiKey: process.env.TUNER_API_KEY, workspaceId: Number.parseInt(process.env.TUNER_WORKSPACE_ID!, 10), agentId: process.env.TUNER_AGENT_ID, callType: 'phone_call', recordingUrlResolver: getRecordingUrl, costCalculator: calculateCost, extraMetadata: { env: 'prod', region: 'us-east-1' }, agentVersion: 42, timeoutSeconds: 20, maxRetries: 3, enabled: process.env.NODE_ENV === 'production', }); ``` -------------------------------- ### Install Tuner LiveKit SDK for Python Source: https://docs.usetuner.ai/docs/api-and-integrations/connecting-to-livekit Install the Python SDK using pip. Ensure you have Python 3.10+, livekit-agents 1.4+, and aiohttp 3.9+. ```shell pip install tuner-livekit-sdk ``` -------------------------------- ### Install Tuner TTS Observer with WebSocket Support Source: https://docs.usetuner.ai/docs/api-and-integrations/libraries/tuner-tts Install the tuner-tts-observer library with the 'cartesia' extra for WebSocket support, which is recommended. ```shell pip install "tuner-tts-observer[cartesia]" ``` -------------------------------- ### Install LiveKit SDK for Node.js Source: https://docs.usetuner.ai/docs/api-and-integrations/connecting-to-livekit Install the Tuner AI SDK for Node.js using npm. This requires Node.js 18 or later, and specific versions of the LiveKit agents and RTC SDK. ```shell npm install @usetuner.ai/livekit-sdk ``` -------------------------------- ### Install tuner-langchain Source: https://docs.usetuner.ai/docs/api-and-integrations/libraries/tuner-langchain Install the tuner-langchain library using pip. Ensure you have Python 3.10+ and compatible versions of langchain-core and langgraph. ```bash pip install tuner-langchain ``` -------------------------------- ### Install STT Provider Adapter Source: https://docs.usetuner.ai/docs/api-and-integrations/libraries/tuner-stt Install specific STT provider adapters as optional extras to minimize dependencies. Use this command to install the Deepgram adapter. ```shell pip install "tuner-stt-observer[deepgram]" # DeepgramAdapter ``` -------------------------------- ### Install LangChain Extra Source: https://docs.usetuner.ai/docs/api-and-integrations/connecting-to-pipecat Install the optional LangChain extra for the tuner-pipecat-sdk. This is required to use LangChain or LangGraph integration features. ```shell pip install tuner-pipecat-sdk[langchain] ``` -------------------------------- ### Initiate Tuner Agent Setup in Chat Source: https://docs.usetuner.ai/docs/mcp/set-up-your-agent Paste this command into your chat with the AI to begin configuring your agent's observability using the Tuner MCP server. Follow the 'tuner_setup_agent' prompt provided by the server. ```text Set up Tuner observability for this agent using the Tuner MCP. Fetch and follow the 'tuner_setup_agent' prompt from the Tuner MCP server. ``` -------------------------------- ### Install Tuner for STT Observability with Deepgram Source: https://docs.usetuner.ai/docs/api-and-integrations/custom-integration/quickstart Install Tuner with the STT observer for Deepgram to enable speech-to-text observability. ```bash tuner-core "tuner-stt-observer[deepgram]" ``` -------------------------------- ### Install Tuner with Speechmatics and Cartesia Source: https://docs.usetuner.ai/docs/api-and-integrations/custom-integration/quickstart Install the Tuner packages required for a stack using Speechmatics for STT and Cartesia for TTS. ```bash tuner-core "tuner-stt-observer[speechmatics]" "tuner-tts-observer[cartesia]" ``` -------------------------------- ### Example E-commerce Support Bot System Prompt Source: https://docs.usetuner.ai/user-guide/quick-start/how-to-review-your-first-call This is an example system prompt for an e-commerce support bot. It defines the bot's persona and primary function, and sets a constraint on the types of questions it should answer. ```text You are a friendly and helpful e-commerce support bot. Your primary goal is to answer customer questions about their orders and provide tracking information. Do not answer questions that are not related to e-commerce support. ``` -------------------------------- ### Install Tuner Pipecat SDK Source: https://docs.usetuner.ai/docs/api-and-integrations/connecting-to-pipecat Install the Tuner Pipecat SDK using pip. Ensure you have Python 3.11-3.13 and pipecat-ai version 1.0.0 or higher. ```shell pip install tuner-pipecat-sdk ``` -------------------------------- ### Python LiveKit Agent with Tuner Plugin Source: https://docs.usetuner.ai/docs/api-and-integrations/connecting-to-livekit Example of setting up a LiveKit agent in Python with the Tuner plugin. It includes extracting SIP correlation IDs, calculating call costs, and resolving recording URLs. Ensure environment variables for API key, workspace ID, and agent ID are set. ```python import os from livekit import rtc from livekit.agents import JobContext, AgentSession from tuner import TunerPlugin def _extract_sip_correlation_id(ctx: JobContext) -> str | None: for participant in ctx.room.remote_participants.values(): if participant.kind != rtc.ParticipantKind.PARTICIPANT_KIND_SIP: continue attributes = dict(getattr(participant, "attributes", {}) or {}) sip_call_id_full = attributes.get("sip.callIDFull") if isinstance(sip_call_id_full, str) and sip_call_id_full: return sip_call_id_full return None def calculate_cost(usage) -> float: usd = ( usage.llm_prompt_tokens * 0.000_003 + usage.llm_completion_tokens * 0.000_015 + usage.tts_characters_count * 0.000_030 ) return usd * 100 # Tuner expects USD cents async def get_recording_url(room_name: str, job_id: str) -> str: return await my_storage.get_url(job_id) or "pending" async def entrypoint(ctx: JobContext): session = AgentSession(...) await ctx.connect() sip_correlation_id = _extract_sip_correlation_id(ctx) TunerPlugin( session, ctx, api_key=os.environ["TUNER_API_KEY"], workspace_id=int(os.environ["TUNER_WORKSPACE_ID"]), agent_id=os.environ["TUNER_AGENT_ID"], call_type="phone_call", recording_url_resolver=get_recording_url, cost_calculator=calculate_cost, sip_correlation_id=sip_correlation_id, extra_metadata={"env": "prod", "region": "us-east-1"}, timeout_seconds=20.0, max_retries=3, enabled=True, ) await session.start(...) ``` -------------------------------- ### Install Tuner with Deepgram, LangGraph, and Cartesia Source: https://docs.usetuner.ai/docs/api-and-integrations/custom-integration/quickstart Install the necessary Tuner packages for a stack including Deepgram for STT, LangGraph for orchestration, and Cartesia for TTS. ```bash tuner-core "tuner-stt-observer[deepgram]" "tuner-tts-observer[cartesia]" tuner-langchain ``` -------------------------------- ### Add Tuner Plugin to Python Agent Entrypoint Source: https://docs.usetuner.ai/docs/api-and-integrations/connecting-to-livekit Import TunerPlugin and instantiate it with the AgentSession and context before starting the session. This automatically enables call data capture. ```python from tuner import TunerPlugin async def entrypoint(ctx: JobContext): session = AgentSession(...) TunerPlugin(session, ctx) # wires itself automatically await session.start(...) ``` -------------------------------- ### Wire SIP Correlation ID into Entrypoint Source: https://docs.usetuner.ai/docs/api-and-integrations/connecting-to-livekit This example shows how to integrate the SIP correlation ID extraction into your agent's entrypoint. Ensure you connect to the room first, then read the ID, and finally pass it to TunerPlugin. ```python async def entrypoint(ctx: JobContext): await ctx.connect() # 1. connect first sip_correlation_id = _extract_sip_correlation_id(ctx) # 2. then read session = AgentSession(...) TunerPlugin( session, ctx, sip_correlation_id=sip_correlation_id, # 3. pass it in # ... your other TunerPlugin options ) await session.start(...) ``` -------------------------------- ### Node.js Webhook Setup Source: https://docs.usetuner.ai/docs/api-and-integrations/vapi-transient-assistants Set up a Node.js Express webhook to receive VAPI end-of-call reports and send them to Tuner. This handler should be fire-and-forget. ```javascript app.post("/vapi/webhook", (req, res) => { const message = req.body?.message; if (message?.type === "end-of-call-report") { // fire-and-forget, never blocks, never throws sendCallToTuner(message); } res.sendStatus(200); }); ``` -------------------------------- ### Example SIP INVITE with Call-ID Header Source: https://docs.usetuner.ai/docs/simulation/other-platforms This is a standard SIP INVITE request containing the essential Call-ID header that Tuner uses for linking. ```sip INVITE sip:your-agent@provider.example SIP/2.0 Call-ID: 2jf9wCCXfwgxKkTOfLcEsA5LD6f ... ``` -------------------------------- ### End Call with Agent Hangup Reason (Tool Example) Source: https://docs.usetuner.ai/docs/api-and-integrations/connecting-to-livekit Example of using `context.session.shutdown()` and `get_job_context().shutdown(reason="agent_hangup")` within a function tool to properly end a call and record the reason. ```python @function_tool async def end_call(self, context: RunContext): """End the call once the user has been helped.""" await context.session.shutdown(drain=True) get_job_context().shutdown(reason="agent_hangup") ``` -------------------------------- ### Python FastAPI Webhook Setup Source: https://docs.usetuner.ai/docs/api-and-integrations/vapi-transient-assistants Set up a Python FastAPI webhook to receive VAPI end-of-call reports and send them to Tuner. This handler should never block or raise exceptions. ```python from send_to_tuner import send_call_to_tuner # your existing VAPI webhook endpoint (FastAPI shown) @app.post("/vapi/webhook") async def vapi_webhook(request: Request): message = (await request.json()).get("message") or {} if message.get("type") == "end-of-call-report": # never blocks your handler, never raises send_call_to_tuner(message) return {} ``` -------------------------------- ### Example Diagnosis Output Source: https://docs.usetuner.ai/docs/mcp/diagnose-your-agent This markdown snippet shows a typical structured output from an agent diagnostic report, detailing agent health, identified issues, working components, and data gaps. ```markdown ## Agent health Broadly healthy: 82% success across 1,240 calls in the last 30 days, but one intent is dragging the average down. ## Issues found ### Booking fails when the requested time is unavailable **Signal:** The "Offer alternative times" eval fails on 71% of *Make a Reservation* calls that end in the *No Availability* outcome, a red-flag rate ~3x the agent average. **Pattern:** When the first-choice slot is taken, the agent apologizes and ends the call instead of proposing nearby times. **Root cause:** The system prompt describes booking a requested slot but has no branch for the unavailable case. (High confidence.) **Fix:** Add an explicit step to the booking workflow: when a slot is unavailable, offer the two closest available times before closing. **Verify:** Watch the "Offer alternative times" eval pass-rate and the share of *No Availability* outcomes over the next week. ## What's working Reservation confirmations and party-size capture pass on 96%+ of calls. Leave them as-is. ## Data gaps No data-extraction field for the requested time, so how far off the alternatives are can't be measured. Adding it would sharpen this analysis. ``` -------------------------------- ### Create SIP Participant Request Source: https://docs.usetuner.ai/docs/simulation/outbound/individual-platforms Example of creating a SIP participant request, specifying the destination SIP URI. This is a core part of initiating an outbound call in simulation. ```python await ctx.api.sip.create_sip_participant( api.CreateSIPParticipantRequest( sip_call_to=destination, # the phone number, or Tuner's SIP URI in simulation ... ) ) ``` -------------------------------- ### Building Data Lookups for Turns and User Rows Source: https://docs.usetuner.ai/docs/api-and-integrations/vapi-transient-assistants This Python code constructs dictionaries to map turns to agent rows and to track when customer rows started speaking. It's essential for organizing conversational data. ```python # Build lookups: which agent rows belong to each turn, and when each # customer row started speaking. rows_of_turn: dict[int, list[Json]] = {} user_rows: list[dict] = [] row_spoken: dict[int, float] = {} for s in segments: own = frags_of.get(id(s)) if own: row_spoken[id(s)] = own[0]["spokenTime"] turns = turns_of.get(id(s)) if not turns: continue if s.get("role") == "agent": for t in turns: rows_of_turn.setdefault(t, []).append(s) elif s.get("role") == "user": user_rows.append( {"row": s, "minTurn": min(turns), "spoken": row_spoken.get(id(s), 0)} ) ``` -------------------------------- ### Calculate Turn First Output Time Source: https://docs.usetuner.ai/docs/api-and-integrations/vapi-transient-assistants This function calculates the earliest time a turn produced output, considering events like `pipeline.botSpeechStarted` or `assistant.tool.started`. This is crucial for determining when a customer's speech must have started to trigger the turn. ```typescript const turnFirstOut = new Map(); for (const tid of completedTurnIds) { for (const e of byTurn.get(tid)!) { if (e.event === "pipeline.botSpeechStarted" || e.event === "assistant.tool.started") { turnFirstOut.set(tid, Math.min(turnFirstOut.get(tid) ?? Infinity, e.time)); } } } ``` -------------------------------- ### Build Turn and User Row Lookups Source: https://docs.usetuner.ai/docs/api-and-integrations/vapi-transient-assistants This code builds lookup maps for agent rows belonging to each turn and for customer rows, including their start times. It organizes data to facilitate the attachment of VAPI's latency numbers. ```typescript const rowsOfTurn = new Map(); const userRows: { row: Json; minTurn: number; spoken: number }[] = []; const rowSpoken = new Map(); for (const s of segments) { const own = fragsOf.get(s); if (own?.length) rowSpoken.set(s, own[0].spokenTime); const turns = turnsOf.get(s); if (!turns || !turns.size) continue; if (s.role === "agent") { for (const t of turns) { const list = rowsOfTurn.get(t) ?? []; list.push(s); rowsOfTurn.set(t, list); } } else if (s.role === "user") { userRows.push({ row: s, minTurn: Math.min(...turns), spoken: rowSpoken.get(s) ?? 0 }); } } ``` -------------------------------- ### Calculate Segment Timing Information Source: https://docs.usetuner.ai/docs/api-and-integrations/vapi-transient-assistants Calculates the start, end, and duration in milliseconds for a given segment relative to the call's start time. Handles cases where call start time or segment timing data might be null. ```typescript /** Work out a row's start/end/duration relative to the start of the call. */ function segmentTiming( m: Json, callStartMs: number | null, ): { startMs: number | null; endMs: number | null; durationMs: number | null } { let startMs: number | null = null; let endMs: number | null = null; let durationMs: number | null = null; ``` -------------------------------- ### Initialize Tuner Observer and Turn Tracker Source: https://docs.usetuner.ai/docs/api-and-integrations/connecting-to-pipecat Create an Observer instance with your API key, workspace, agent, and call IDs. Attach a TurnTrackingObserver for call turn tracking. ```python import uuid from tuner_pipecat_sdk import Observer from pipecat.observers.turn_tracking_observer import TurnTrackingObserver turn_tracker = TurnTrackingObserver() observer = Observer( api_key="YOUR_TUNER_API_KEY", workspace_id=42, agent_id="fa4da74c-...", call_id=str(uuid.uuid4()), base_url="https://api.usetuner.ai", asr_model="deepgram/nova-3", llm_model="gpt-4o-mini", tts_model="cartesia/sonic", ) observer.attach_turn_tracking_observer(turn_tracker) ``` -------------------------------- ### Agent Initialization Source: https://docs.usetuner.ai/docs/api-and-integrations/connecting-to-livekit Initializes an Agent with a Langchain LLMAdapter. Configuration for thread ID is passed within the adapter. ```python agent = Agent( instructions="", llm=langchain.LLMAdapter( wrapped_chain, stream_mode="messages", config={"configurable": {"thread_id": thread_id}}, ), ) ``` -------------------------------- ### Initialize Observer with Inline Credentials Source: https://docs.usetuner.ai/docs/api-and-integrations/connecting-to-pipecat Instantiate the Observer class by passing your Tuner API key, workspace ID, agent ID, and optionally the base URL directly in the code. A unique call ID is also generated. ```python Observer( api_key="tr_api_...", workspace_id=123, agent_id="fa4da74c-...", call_id=str(uuid.uuid4()), base_url="https://api.usetuner.ai", ) ``` -------------------------------- ### Calculate Segment Timing Relative to Call Start Source: https://docs.usetuner.ai/docs/api-and-integrations/vapi-transient-assistants This helper function calculates the start, end, and duration of a speech segment in milliseconds, relative to the beginning of the call. It supports different input formats for timing information. ```python def _segment_timing(m: Json, call_start_ms: Optional[int]): """Work out a row's start/end/duration relative to the start of the call.""" start_ms: Optional[int] = None end_ms: Optional[int] = None duration_ms: Optional[int] = None seconds = m.get("secondsFromStart") if seconds is not None: start_ms = math.ceil(seconds * 1000) if m.get("duration") is not None: duration_ms = math.ceil(m["duration"]) if start_ms is not None and duration_ms is not None: end_ms = start_ms + duration_ms elif call_start_ms is not None: if m.get("time") is not None and start_ms is None: start_ms = math.ceil(m["time"] - call_start_ms) if m.get("endTime") is not None: end_ms = math.ceil(m["endTime"] - call_start_ms) if start_ms is not None and end_ms is not None: duration_ms = end_ms - start_ms return start_ms, end_ms, duration_ms ``` -------------------------------- ### Configure Python SDK Credentials Inline Source: https://docs.usetuner.ai/docs/api-and-integrations/connecting-to-livekit Pass API key, workspace ID, and agent ID directly in the TunerPlugin constructor. ```python TunerPlugin( session, ctx, api_key="tr_api_...", workspace_id=123, agent_id="fa4da74c-...", ) ``` -------------------------------- ### Create Call with Tenant-Specific Metadata Source: https://docs.usetuner.ai/docs/agent-configurations/evaluating-dynamic-agents This Python snippet demonstrates how to log a call with dynamic tenant instructions embedded in the metadata. Ensure the `tuner` library is imported and necessary variables like `WORKSPACE_ID`, `AGENT_ID`, `session_id`, `start_ts`, `end_ts`, `recording_url`, and `transcript` are defined. ```python tuner.create_call( workspace_id=WORKSPACE_ID, agent_remote_identifier=AGENT_ID, body={ "call_id": f"call-{session_id}", "call_type": "web_call", "call_status": "call_ended", "start_timestamp": start_ts, "end_timestamp": end_ts, "recording_url": recording_url, "transcript_with_tool_calls": transcript, "metadata": { "instructions": tenant.system_prompt, "tenant": tenant.id, } } ) ``` -------------------------------- ### Check Problem Timeline Source: https://docs.usetuner.ai/user-guide/quick-start/diagnose-your-agent Inquire about the historical occurrence of a problem to determine if it started with a specific agent version or has been present historically. ```text Did this problem start at a specific agent version, or has it always been there? ``` -------------------------------- ### Create a Tuner Session Source: https://docs.usetuner.ai/docs/api-and-integrations/custom-integration/quickstart Initialize a TunerSession at the beginning of each call. Configure the session with desired ASR, LLM, and TTS models, and provide a unique, retry-stable call_id for idempotent ingestion. ```python 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 ) ``` -------------------------------- ### Get Agent Source: https://docs.usetuner.ai/llms.txt Retrieves details for a specific agent using its unique Tuner ID. This is useful for fetching agent configurations or status. ```APIDOC ## GET /agents/{agent_id} ### Description Get a single agent by its Tuner ID. ### Method GET ### Endpoint /agents/{agent_id} ### Parameters #### Path Parameters - **agent_id** (string) - Required - The unique Tuner ID of the agent to retrieve. ``` -------------------------------- ### Set Call Type Label Source: https://docs.usetuner.ai/docs/api-and-integrations/connecting-to-pipecat Override the default call type label for an Observer. Use this to categorize calls, for example, as 'web_call' or 'phone_call'. ```python Observer(..., call_type="web_call") Observer(..., call_type="phone_call") ``` -------------------------------- ### Building Tuner Transcript Segments Source: https://docs.usetuner.ai/docs/api-and-integrations/libraries/tuner-langchain Convert recorded brain layer events into Tuner transcript rows. Timestamps are converted to milliseconds since call start. ```python from tuner_langchain import segments_from_invocation # Assuming 'invocations' is the output from handler.get_invocations() segments = segments_from_invocation(invocations) ``` -------------------------------- ### Format and Send Call Transcript Source: https://docs.usetuner.ai/docs/api-and-integrations/libraries/tuner-langchain Convert captured invocations into transcript segments using segments_from_invocation. Merge these segments with voice turns and sort them by start time before sending. ```python from tuner_langchain.segment_builder import segments_from_invocation # segments_from_invocation expects nanoseconds, so convert your call start call_start_ns = call_start_ms * 1_000_000 graph_segments = [] for invocation in accumulator.get_invocations(): # one entry per user turn graph_segments.extend(segments_from_invocation(invocation, call_start_ns)) # merge with your STT/TTS turns and sort the timeline transcript = sorted(voice_turns + graph_segments, key=lambda e: e.get("start_ms", 0)) ``` -------------------------------- ### Configure Tuner Credentials Inline in Code (Node.js) Source: https://docs.usetuner.ai/docs/api-and-integrations/connecting-to-livekit Instantiate the TunerPlugin with credentials passed directly as an options object. This is an alternative to using environment variables. ```typescript new TunerPlugin(session, ctx, { apiKey: 'tr_api_...', workspaceId: 123, agentId: 'fa4da74c-...', }); ``` -------------------------------- ### Initialize Tuner Accumulator and Handler Source: https://docs.usetuner.ai/docs/api-and-integrations/libraries/tuner-langchain Set up the TunerAccumulator to collect events and the TunerLangGraphHandler to listen to graph execution. This is done once per call. ```python from tuner_langchain import TunerAccumulator from tuner_langchain.handlers import TunerLangGraphHandler accumulator = TunerAccumulator() handler = TunerLangGraphHandler(accumulator) ``` -------------------------------- ### Initialize and Attach SpeechmaticsAdapter Source: https://docs.usetuner.ai/docs/api-and-integrations/libraries/tuner-stt Initialize the SpeechmaticsAdapter with your Speechmatics client and attach it to the session. This adapter safely stacks event handlers, allowing your custom handlers to coexist. ```python from tuner_stt_observer import SpeechmaticsAdapter stt = SpeechmaticsAdapter(client=stt_client) # your existing Speechmatics client session.attach(stt) # Your own handlers still work — Speechmatics-rt stacks, not replaces. # For barge-in, register your own partial-transcript handler: stt_client.on(SMEvent.ADD_PARTIAL_TRANSCRIPT)(handle_barge_in) ``` -------------------------------- ### Attach SIP Information Directly Source: https://docs.usetuner.ai/docs/api-and-integrations/connecting-to-pipecat Manually attach SIP call ID and headers if you have them readily available, for example, from custom SIP trunks or PBX systems like FreeSWITCH or Asterisk. ```python observer.attach_sip_info(sip_call_id="...", sip_headers={...}) ``` -------------------------------- ### POST /api/v1/workspaces/{workspace_id}/agents/ Source: https://docs.usetuner.ai/docs/api-reference/create-agent Creates a new agent in a specified workspace and links it to a voice provider integration. If an integration with the same provider name and API key already exists, it will be reused; otherwise, a new one will be created. The response includes the newly created agent's ID, which can be used for subsequent requests. ```APIDOC ## POST /api/v1/workspaces/{workspace_id}/agents/ ### Description Create a new agent in a workspace. Creates an agent and links it to a voice provider integration identified by `provider_name` and `api_key`. If a matching integration already exists in the workspace it will be reused; otherwise a new one is created. Returns the newly created agent including its Tuner ID (`id`), which you can use in subsequent `GET` or `PUT` requests. ### Method POST ### Endpoint /api/v1/workspaces/{workspace_id}/agents/ ### Parameters #### Path Parameters - **workspace_id** (integer) - Required - Get this from Workspace > General Settings. #### Request Body - **agent_name** (string) - Required - Display name for the agent (e.g., 'Support Bot', 'Sales Agent'). Max length 200, min length 1. - **summary** (string | null) - Optional - Brief description of what this agent does (e.g., 'Handles billing inquiries'). - **provider_name** (string) - Required - Voice provider this agent is connected to (e.g., 'retell', 'vapi'). Must match a provider name supported by Tuner. Max length 200, min length 1. - **api_key** (string) - Required - API key for authenticating with your voice provider. Find this in your provider's account settings. Max length 500, min length 1. - **agent_id** (string) - Required - Your voice provider's unique identifier for this agent. Find this in your provider's agent configuration. Max length 36, min length 1. - **agent_lang** (string | null) - Optional - BCP 47 language tag for the agent's primary language (e.g., 'en', 'es', 'fr'). Defaults to workspace language if omitted. Max length 10. - **call_direction** (string) - Optional - Whether this agent handles 'inbound' (customer-initiated) or 'outbound' (agent-initiated) calls. Defaults to 'inbound'. Pattern: ^(inbound|outbound)$. - **prompt** (string | null) - Optional - System prompt or instructions given to the agent. - **workflow** (object | null) - Optional - Agent workflow configuration as a JSON object (must be JSON-serializable). - **webhook_url** (string | null) - Optional - Webhook URL for the agent. Max length 2048. ### Response #### Success Response (201) - **id** (string) - The Tuner ID of the newly created agent. #### Error Response - **422** - Validation Error ``` -------------------------------- ### Get Agent OpenAPI Specification Source: https://docs.usetuner.ai/docs/api-reference/get-agent This OpenAPI specification defines the endpoint for retrieving a single agent by its workspace and agent IDs. It includes request parameters, response schemas, and authentication requirements. ```yaml GET /api/v1/workspaces/{workspace_id}/agents/{agent_id} openapi: 3.1.0 info: title: My Public API version: 1.0.0 servers: - url: https://api.usetuner.ai security: [] tags: - name: public description: Public endpoints paths: /api/v1/workspaces/{workspace_id}/agents/{agent_id}: get: tags: - public summary: Get Workspace Agent description: >- Get a single agent by its Tuner ID. Returns the agent with its nested integration and provider details. The authenticated user must have access to the workspace the agent belongs to. - **workspace_id**: Get this from Workspace > General Settings. - **agent_id**: Tuner's internal numeric ID for the agent (returned by `GET /workspaces/{workspace_id}/agents` or `POST /workspaces/{workspace_id}/agents`). operationId: >- get_workspace_agent_api_v1_workspaces__workspace_id__agents__agent_id__get parameters: - name: agent_id in: path required: true schema: type: integer title: Agent Id - name: workspace_id in: path required: true schema: type: integer title: Workspace Id responses: '200': description: Successful Response content: application/json: schema: $ref: '#/components/schemas/Agent' '422': description: Validation Error content: application/json: schema: $ref: '#/components/schemas/HTTPValidationError' security: - Bearer: [] components: schemas: Agent: properties: id: type: integer title: Id description: Tuner's internal numeric ID for this agent. integration_id: type: integer title: Integration Id description: ID of the voice provider integration this agent uses. name: type: string title: Name description: Display name of the agent. description: anyOf: - type: string - type: 'null' title: Description description: Brief description of what this agent does. lang: anyOf: - type: string - type: 'null' title: Lang description: >- BCP 47 language tag for the agent's primary language (e.g., 'en', 'es'). persona: anyOf: - $ref: '#/components/schemas/AgentPersona' - type: 'null' description: >- Nested language and accent for the agent. Null when the agent has no language configured. call_direction: type: string title: Call Direction description: >- 'inbound' for customer-initiated calls, 'outbound' for agent-initiated calls. remote_identifier: type: string title: Remote Identifier description: Your voice provider's unique identifier for this agent. prompt: anyOf: - type: string - type: 'null' title: Prompt description: System prompt or instructions given to the agent. workflow: anyOf: - additionalProperties: true type: object - type: 'null' title: Workflow description: Agent workflow configuration. Must be valid JSON. webhook_url: anyOf: - type: string - type: 'null' title: Webhook Url description: >- Destination URL for outbound webhook notifications. Null if not configured. integration_name: anyOf: - type: string - type: 'null' title: Integration Name description: Name of the voice provider for this agent's integration. source_template_id: anyOf: - type: integer - type: 'null' title: Source Template Id description: ID of the template this agent was created from. Null if none. setup_method: type: string title: Setup Method description: >- ``` -------------------------------- ### Basic TunerPlugin Integration Source: https://docs.usetuner.ai/docs/api-and-integrations/connecting-to-livekit This snippet shows the most basic integration of the TunerPlugin. It automatically wires itself to the agent session and submits call data upon session end. ```typescript import { TunerPlugin } from '@usetuner.ai/livekit-sdk'; import { voice, type JobContext } from '@livekit/agents'; export default async function entry(ctx: JobContext) { const session = new voice.AgentSession({ /* ... */ }); new TunerPlugin(session, ctx); // wires itself automatically await session.start({ agent, room: ctx.room }); } ``` -------------------------------- ### PublicTranscriptSegment Source: https://docs.usetuner.ai/docs/api-reference/create-call Defines a single transcript segment, providing a unified shape for all providers. It includes utterance text, word-level timing, start and end times, duration, and details for workflow transitions or tool invocations. ```APIDOC ## PublicTranscriptSegment ### Description Represents a single segment of a transcript, unifying data from various providers. It includes the role, text content, word-level timing, timestamps (start, end, duration), and optional node transition or tool invocation details. ### Fields - **role** (PublicTranscriptRole) - Required - The type of segment (e.g., 'user', 'agent', 'node_transition'). - **text** (string | null) - Optional - The utterance text for user or agent messages. - **words** (array[PublicTranscriptWord] | null) - Optional - Word-level timing data. - **start_ms** (integer | null) - Optional - Segment start time in milliseconds relative to call start. - **end_ms** (integer | null) - Optional - Segment end time in milliseconds relative to call start. - **duration_ms** (integer | null) - Optional - Utterance duration in milliseconds. - **node** (PublicTranscriptNode | null) - Optional - Workflow transition details. - **tool** (PublicTranscriptTool | null) - Optional - Tool invocation or result details. ### Notes For roles 'user' and 'agent', at least one timing source is required: `words` (non-empty with `start_ms`/`end_ms`), or `start_ms` + `end_ms`, or `start_ms` + `duration_ms`. ``` -------------------------------- ### Wire SIP Correlation ID into Entrypoint Source: https://docs.usetuner.ai/docs/api-and-integrations/connecting-to-livekit This snippet demonstrates how to integrate the SIP correlation ID extraction into your agent's entrypoint. It ensures you connect to the room first, then extract the ID, and finally pass it to the TunerPlugin. ```typescript export default async function entry(ctx: JobContext) { const session = new voice.AgentSession({ /* ... */ }); await ctx.connect(); // 1. connect first const sipCorrelationId = extractSipCorrelationId(ctx); // 2. then read new TunerPlugin(session, ctx, { sipCorrelationId, // 3. pass it in // ...other options }); await session.start({ agent, room: ctx.room }); } ``` -------------------------------- ### Parse Word-Level Timing from VAPI Metadata Source: https://docs.usetuner.ai/docs/api-and-integrations/vapi-transient-assistants This function extracts and converts word-level timing information from VAPI metadata into milliseconds relative to the call start. It handles different time units that VAPI has used historically. ```python def _words(metadata: Any, call_start_ms: Optional[int]) -> Optional[list[Json]]: """ Word-level timing from VAPI. VAPI has used different time units for word timestamps over time, so the unit is detected from the value itself. """ if not isinstance(metadata, dict): return None raw = metadata.get("wordLevelConfidence") if not isinstance(raw, list) or len(raw) == 0: return None def to_rel_ms(v: float) -> int: if v > 1e11: return math.ceil(v - (call_start_ms or 0)) # epoch ms if v < 1e5: return math.ceil(v * 1000) # seconds from call start return math.ceil(v) # already ms from call start ``` -------------------------------- ### Set Tuner Credentials Source: https://docs.usetuner.ai/docs/api-and-integrations/custom-integration/quickstart Configure your Tuner API key, workspace ID, and agent ID by exporting them as environment variables. These are required for authenticating with the Tuner API. ```shell export TUNER_API_KEY="tr_api_..." export TUNER_WORKSPACE_ID="123" export TUNER_AGENT_ID="fa4da74c-..." ``` -------------------------------- ### Discover Unknown Problems Source: https://docs.usetuner.ai/user-guide/quick-start/diagnose-your-agent Use this prompt to identify calls that do not match any of your configured intents. The assistant will cluster these calls and explain their themes, helping you discover unhandled user needs. ```text Are there calls that don't match any of my configured intents? Cluster them and tell me what they're about. ``` -------------------------------- ### Mark Customer Interruptions in Agent Segments Source: https://docs.usetuner.ai/docs/api-and-integrations/vapi-transient-assistants This function flags agent speech segments that were interrupted by a customer. It calculates interruptions based on the start time of customer speech relative to the end time of the preceding agent speech, using a defined threshold. ```python _INTERRUPTION_THRESHOLD_MS = 200 def _mark_interruptions(segments: list[Json]) -> None: """ Flag agent rows the customer talked over (fallback path, no call log). If a customer message starts clearly before the previous agent message ended, that agent message was interrupted. The log-based path detects interruptions more completely; this is the best the payload alone allows. """ last_agent_seg: Optional[Json] = None for seg in segments: role = seg.get("role") if role == "agent": last_agent_seg = seg elif role == "user": if ( last_agent_seg and last_agent_seg.get("end_ms") is not None and seg.get("start_ms") is not None and seg["start_ms"] < last_agent_seg["end_ms"] - _INTERRUPTION_THRESHOLD_MS ): last_agent_seg["metadata"] = { **(last_agent_seg.get("metadata") or {}), "interrupted": True, } last_agent_seg = None # each customer turn compares against a fresh agent turn # tool segments: ignore, keep the current agent turn as the candidate ``` -------------------------------- ### Preparing Tuner API Request URL and Headers Source: https://docs.usetuner.ai/docs/api-and-integrations/vapi-transient-assistants This snippet shows how to construct the URL and headers for making a POST request to the Tuner API. It includes URL encoding for workspace and agent identifiers, and sets up authorization and content type headers. ```python from urllib.parse import urlencode params = urlencode( { "workspace_id": str(TUNER_WORKSPACE), "agent_remote_identifier": TUNER_AGENT_ID, } ) url = f"{TUNER_BASE_URL}/api/v1/public/call?{params}" headers = { "Authorization": f"Bearer {TUNER_API_KEY}", "Content-Type": "application/json", } ``` -------------------------------- ### Pushing Calls with Metadata Source: https://docs.usetuner.ai/docs/agent-configurations/evaluating-dynamic-agents When logging a call to Tuner, include session instructions or relevant runtime context in the 'metadata' field of the request body. This ensures the evaluator receives the specific instructions the agent operated under. ```json { "call_id": "call-acme-001", "call_type": "web_call", "call_status": "call_ended", "start_timestamp": 1751790000000, "end_timestamp": 1751790007000, "recording_url": "https://cdn.example.com/recordings/call-acme-001.ogg", "transcript_with_tool_calls": [ { "role": "agent", "text": "Hi, how can I help you today?", "start_ms": 0, "end_ms": 2000 }, { "role": "user", "text": "What's your pricing?", "start_ms": 2500, "end_ms": 4000 }, { "role": "agent", "text": "I'm not able to discuss pricing, but I can connect you with our sales team.", "start_ms": 4500, "end_ms": 7000 } ], "metadata": { "instructions": "You are a support agent for Acme Corp. Never discuss pricing.", "tenant": "acme-corp" } } ``` -------------------------------- ### Determine Turn for Agent Speech Source: https://docs.usetuner.ai/docs/api-and-integrations/vapi-transient-assistants This code determines which turn produced each piece of agent speech by comparing the spoken time with the turn start time. It sorts speech events by time to accurately attribute them to the correct turn, especially when speech is interrupted. ```typescript const botStarts: { tid: number; time: number }[] = []; for (const tid of completedTurnIds) { for (const e of byTurn.get(tid)!) { if (e.event === "pipeline.botSpeechStarted") botStarts.push({ tid, time: e.time }); } } botStarts.sort((a, b) => a.time - b.time); const producingTurn = (t: number): number | null => { let res: number | null = null; for (const b of botStarts) { if (b.time <= t) res = b.tid; else break; } return res; }; ```