"""Typed events flowing on ``text_output_queue``. These are internal pipeline events produced by VAD, TranscriptionNotifier, and LMOutputProcessor, consumed by the realtime ``WebSocketRouter`` send-loop and ``RealtimeService.dispatch_pipeline_event``. They replace the raw ``dict`` literals that were previously put on the queue. """ from __future__ import annotations from typing import Literal, Optional from openai.types.responses.response_function_tool_call import ResponseFunctionToolCall from pydantic import BaseModel, Field class PipelineEvent(BaseModel): """Base for all text_output_queue events. The ``type`` field mirrors the former dict ``"type"`` key and acts as a Pydantic discriminator. """ type: str # ── VAD events ──────────────────────────────────────────────────────── class SpeechStartedEvent(PipelineEvent): type: Literal["speech_started"] = "speech_started" audio_start_ms: int = 0 turn_id: str | None = None turn_revision: int | None = None reopened: bool = False interrupt_response: bool = Field(default=True, exclude=True) class SpeechStoppedEvent(PipelineEvent): type: Literal["speech_stopped"] = "speech_stopped" duration_s: float = 0.0 audio_end_ms: int = 0 turn_id: str | None = None turn_revision: int | None = None # ── Transcription events (TranscriptionNotifier) ───────────────────── class PartialTranscriptionEvent(PipelineEvent): type: Literal["partial_transcription"] = "partial_transcription" delta: str turn_id: str | None = None turn_revision: int | None = None class TranscriptionCompletedEvent(PipelineEvent): type: Literal["transcription_completed"] = "transcription_completed" transcript: str language_code: Optional[str] = None turn_id: str | None = None turn_revision: int | None = None speech_stopped_at_s: float | None = Field(default=None, exclude=True) # ── LLM output events (LMOutputProcessor) ──────────────────────────── class AssistantTextEvent(PipelineEvent): type: Literal["assistant_text"] = "assistant_text" text: str tools: list[ResponseFunctionToolCall] = Field(default_factory=list) turn_id: str | None = None turn_revision: int | None = None # Response generation that produced this text, mirroring AudioOutput. Lets the # send loop discard stale assistant text by the same generation-aware rule as # audio, instead of blanket-dropping while cancel_scope.discarding is set. cancel_generation: int | None = None class TokenUsageEvent(PipelineEvent): type: Literal["token_usage"] = "token_usage" input_tokens: int = 0 output_tokens: int = 0 turn_id: str | None = None turn_revision: int | None = None class ResponseFailedEvent(PipelineEvent): """Signals that a response could not be generated (e.g. invalid out-of-band input). Dispatched to the service so it can close the in-progress response with ``status="failed"`` instead of the usual ``completed``. """ type: Literal["response_failed"] = "response_failed" message: str = "" turn_id: str | None = None turn_revision: int | None = None