from __future__ import annotations from typing import Literal from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator from services.shared.core import Role Channel = Literal["voice", "telegram", "whatsapp", "email", "webchat"] InteractionStatus = Literal["new", "in_progress", "escalated", "closed", "abandoned"] AIThreadState = Literal["queued", "thinking", "active", "handoff_required", "human_owned", "closed", "error"] VoiceAIState = Literal[ "queued", "greeting", "listening", "thinking", "speaking", "active", "handoff_requested", "handoff_required", "human_owned", "completed", "closed", "error", ] RecordingStatus = Literal["ready", "archived", "missing"] IvrSessionStatus = Literal["active", "completed", "abandoned"] EventOutboxStatus = Literal["pending", "published", "failed"] AsteriskForwardStatus = Literal["received", "processing", "forwarded", "failed"] TelephonyStatus = Literal["ringing", "claimed", "connected", "ended", "failed"] CallActionResultStatus = Literal["ok", "failed", "rejected"] VoiceStartNameStatus = Literal["name_obtained", "name_not_obtained", "name_followup_required"] VoiceStartNameSource = Literal["known_customer", "voice_start", "voice_followup", "none"] VoiceNameKnownCustomerBehavior = Literal["trust_and_handoff", "confirm_in_downstream", "ask_on_start"] VoiceNameUnknownCustomerBehavior = Literal["ask_on_start", "skip_to_downstream"] VoiceNameMissingNameBehavior = Literal["ask_inline_once", "do_not_ask"] VoiceNameUncertainNameBehavior = Literal["confirm_then_finalize", "finalize_immediately", "discard_and_collect"] class VoiceNameCollectionLanguageTexts(BaseModel): start_prompt: str = Field(min_length=1) personalized_greeting_template: str = Field(min_length=1) confirmation_greeting_template: str = Field(min_length=1) inline_followup_prompt: str = Field(min_length=1) @field_validator( "start_prompt", "personalized_greeting_template", "confirmation_greeting_template", "inline_followup_prompt", mode="before", ) @classmethod def _trim_required_text(cls, value: str) -> str: normalized = str(value or "").strip() if not normalized: raise ValueError("Text value is required") return normalized @model_validator(mode="after") def _validate_name_templates(self): if "{name}" not in self.personalized_greeting_template: raise ValueError("personalized_greeting_template must contain {name}") if "{name}" not in self.confirmation_greeting_template: raise ValueError("confirmation_greeting_template must contain {name}") return self class VoiceNameCollectionStartConfig(BaseModel): ask_name_on_start: bool = True known_customer_behavior: VoiceNameKnownCustomerBehavior = "trust_and_handoff" unknown_customer_behavior: VoiceNameUnknownCustomerBehavior = "ask_on_start" class VoiceNameCollectionDownstreamConfig(BaseModel): missing_name_behavior: VoiceNameMissingNameBehavior = "ask_inline_once" uncertain_name_behavior: VoiceNameUncertainNameBehavior = "confirm_then_finalize" finalize_on_explicit_name: bool = True finalize_on_confirmation: bool = True class VoiceNameCollectionTextsConfig(BaseModel): ru: VoiceNameCollectionLanguageTexts kz: VoiceNameCollectionLanguageTexts class VoiceNameCollectionConfig(BaseModel): enabled: bool = True start: VoiceNameCollectionStartConfig = Field(default_factory=VoiceNameCollectionStartConfig) downstream: VoiceNameCollectionDownstreamConfig = Field(default_factory=VoiceNameCollectionDownstreamConfig) texts: VoiceNameCollectionTextsConfig class VoiceNameCollectionConfigOut(BaseModel): config: VoiceNameCollectionConfig updated_at: str | None = None source: Literal["defaults", "database"] = "defaults" VoiceTTSProviderName = Literal["yandex", "elevenlabs", "openai"] VoiceTTSLanguage = Literal["ru", "kz"] class VoiceTTSLanguageConfig(BaseModel): model_config = ConfigDict(protected_namespaces=()) voice: str | None = None model_id: str | None = None language_code: str | None = None @field_validator("voice", "model_id", "language_code", mode="before") @classmethod def _trim_optional_text(cls, value: str | None) -> str | None: if value is None: return None normalized = str(value).strip() return normalized or None class VoiceTTSProviderConfig(BaseModel): ru: VoiceTTSLanguageConfig = Field(default_factory=VoiceTTSLanguageConfig) kz: VoiceTTSLanguageConfig = Field(default_factory=VoiceTTSLanguageConfig) class VoiceTTSVoiceOption(BaseModel): value: str = Field(min_length=1) label: str = Field(min_length=1) @field_validator("value", "label", mode="before") @classmethod def _trim_required_option_text(cls, value: str) -> str: normalized = str(value or "").strip() if not normalized: raise ValueError("Option value is required") return normalized class VoiceTTSConfig(BaseModel): provider: VoiceTTSProviderName = "yandex" yandex: VoiceTTSProviderConfig = Field(default_factory=VoiceTTSProviderConfig) elevenlabs: VoiceTTSProviderConfig = Field(default_factory=VoiceTTSProviderConfig) openai: VoiceTTSProviderConfig = Field(default_factory=VoiceTTSProviderConfig) class VoiceTTSConfigOut(BaseModel): config: VoiceTTSConfig updated_at: str | None = None source: Literal["defaults", "database"] = "defaults" provider_options: list[VoiceTTSProviderName] = Field(default_factory=list) voice_options: dict[str, dict[str, list[VoiceTTSVoiceOption]]] = Field(default_factory=dict) class AIOperatorConfig(BaseModel): agent_name: str = Field(default="Айнур", min_length=1) company_name: str = Field(default="DigiOps", min_length=1) base_system_prompt: str = Field(min_length=1) identity_reply_ru: str = Field(min_length=1) identity_reply_kz: str = Field(min_length=1) voice_greeting_ru: str = Field(min_length=1) voice_greeting_kz: str = Field(min_length=1) @field_validator( "agent_name", "company_name", "base_system_prompt", "identity_reply_ru", "identity_reply_kz", "voice_greeting_ru", "voice_greeting_kz", mode="before", ) @classmethod def _trim_required_text(cls, value: str) -> str: normalized = str(value or "").strip() if not normalized: raise ValueError("Text value is required") return normalized class AIOperatorConfigOut(BaseModel): config: AIOperatorConfig updated_at: str | None = None source: Literal["defaults", "database"] = "defaults" class HealthResponse(BaseModel): status: str service: str version: str = "v1" class LoginRequest(BaseModel): username: str password: str class LoginResponse(BaseModel): access_token: str token_type: str = "bearer" role: Role auth_source: str | None = None provider: str | None = None full_name: str | None = None class UserCreate(BaseModel): username: str = Field(min_length=3) password: str = Field(min_length=6) full_name: str = Field(min_length=2) role: Role class UserUpdate(BaseModel): password: str | None = Field(default=None, min_length=6) full_name: str | None = Field(default=None, min_length=2) role: Role | None = None class UserOut(BaseModel): user_id: str username: str full_name: str role: Role class AuditEventIn(BaseModel): actor: str action: str entity: str metadata: dict = Field(default_factory=dict) class AuditEvent(AuditEventIn): event_id: str created_at: str class CustomerCreate(BaseModel): display_name: str = Field(min_length=2) phones: list[str] = Field(default_factory=list) preferred_phone: str | None = None tags: list[str] = Field(default_factory=list) class CustomerNameUpdateIn(BaseModel): display_name: str = Field(min_length=2) source: str | None = None class CustomerOut(CustomerCreate): customer_id: str created_at: str class CustomerHistoryEventOut(BaseModel): timestamp: str kind: str title: str body: str note: str = "" interaction_id: str | None = None thread_id: str | None = None call_id: str | None = None class CustomerHistorySummaryOut(BaseModel): contact_points: int = 0 open_cases: int = 0 active_channels: list[str] = Field(default_factory=list) latest_event_at: str | None = None latest_event_title: str | None = None primary_phone: str | None = None primary_telegram_thread_id: str | None = None class CustomerHistoryOut(BaseModel): customer: CustomerOut summary: CustomerHistorySummaryOut interactions: list["InteractionOut"] = Field(default_factory=list) telegram_threads: list["TelegramThreadOut"] = Field(default_factory=list) live_calls: list["VoiceLiveCallOut"] = Field(default_factory=list) history: list[CustomerHistoryEventOut] = Field(default_factory=list) class InteractionCreate(BaseModel): channel: Channel subject: str = Field(min_length=3) customer_id: str | None = None queue_id: str | None = None priority: int = Field(default=3, ge=1, le=5) class InteractionOut(InteractionCreate): interaction_id: str status: InteractionStatus assigned_to: str | None = None created_at: str updated_at: str class InteractionDrilldownFiltersOut(BaseModel): from_ts: str to_ts: str queue_id: str | None = None channel: str | None = None agent_id: str | None = None status: str | None = None q: str | None = None sort_by: str | None = None sort_dir: str | None = None class InteractionDrilldownOut(BaseModel): items: list[InteractionOut] = Field(default_factory=list) total: int limit: int offset: int filters: InteractionDrilldownFiltersOut class AssignRequest(BaseModel): assignee: str class StatusRequest(BaseModel): status: InteractionStatus resolved_first_contact: bool | None = None class EscalateRequest(BaseModel): target_queue_id: str class InteractionTimelineAppendIn(BaseModel): action: str = Field(min_length=1) metadata: dict = Field(default_factory=dict) class QueueRule(BaseModel): channel: Channel priority: int = Field(default=3, ge=1, le=5) strategy: Literal["round_robin", "least_loaded", "skill_based"] = "round_robin" sla_seconds: int = Field(default=30, ge=5, le=3600) class QueueCreate(BaseModel): name: str description: str = "" rules: list[QueueRule] = Field(default_factory=list) class QueueOut(QueueCreate): queue_id: str created_at: str class VoiceEventIn(BaseModel): event_type: Literal[ "call.started", "ivr.completed", "call.ended", "recording.ready", "call.connected", "call.transferred", ] call_id: str interaction_id: str | None = None source_event_id: str | None = None payload: dict = Field(default_factory=dict) class VoiceEventOut(VoiceEventIn): event_id: str created_at: str class RecordingRegisterIn(BaseModel): call_id: str interaction_id: str | None = None source_path: str file_name: str | None = None mime_type: str | None = None duration_seconds: int | None = Field(default=None, ge=0) recorded_at: str | None = None source_event_id: str | None = None class RecordingOut(BaseModel): recording_id: str channel: Literal["voice"] call_id: str interaction_id: str | None = None source_event_id: str | None = None file_name: str mime_type: str size_bytes: int duration_seconds: int | None = None storage_backend: Literal["local_fs"] status: RecordingStatus recorded_at: str created_at: str updated_at: str archived_at: str | None = None class AsteriskBridgeStatusOut(BaseModel): status: str ami_connected: bool ami_host: str | None = None last_event_at: str | None = None queue_codes_loaded: list[str] = Field(default_factory=list) sftp_enabled: bool = False bridge_auth_mode: str | None = None callcontrol_enabled: bool | None = None webrtc_enabled: bool = False webrtc_ws_url: str | None = None class AsteriskEventOut(BaseModel): bridge_event_id: str ami_event_name: str call_id: str linked_id: str | None = None interaction_id: str | None = None recording_id: str | None = None forward_status: AsteriskForwardStatus payload: dict = Field(default_factory=dict) last_error: str | None = None created_at: str updated_at: str class VoiceLiveCallOut(BaseModel): call_id: str interaction_id: str queue_id: str queue_code: str caller_number: str | None = None caller_name: str | None = None status: str telephony_status: TelephonyStatus claimed_by_user: str | None = None claimed_at: str | None = None operator_extension: str | None = None channel_name: str | None = None started_at: str connected_at: str | None = None ended_at: str | None = None updated_at: str last_transition_at: str | None = None hangup_cause: str | None = None terminal_action: str | None = None terminal_target: str | None = None voice_session_id: str | None = None ai_session_id: str | None = None ai_state: VoiceAIState | None = None ai_handoff_reason: str | None = None ai_last_model_at: str | None = None has_recording: bool = False class VoiceCallClaimIn(BaseModel): operator_extension: str | None = None class VoiceCallBlindTransferIn(BaseModel): target_type: Literal["extension", "queue_code"] = "extension" target_value: str = Field(min_length=1) class VoiceCallActionOut(BaseModel): action_id: str call_id: str interaction_id: str | None = None action_type: str actor_user: str actor_role: str request: dict = Field(default_factory=dict) result_status: CallActionResultStatus ami_action_id: str | None = None error: str | None = None created_at: str class VoiceAISummaryOut(BaseModel): call_id: str session_id: str | None = None voice_session_id: str | None = None status_label: str status_tone: Literal["answered", "handoff"] customer_name_status: VoiceStartNameStatus | None = None customer_name_value: str | None = None customer_name_source: VoiceStartNameSource | None = None voice_start_language: str | None = None customer_request_text: str ai_outcome_text: str handoff_reason: str recommended_next_step: str generated_at: str transcript_segments: list["VoiceAISummaryTranscriptSegmentOut"] = Field(default_factory=list) class VoiceAISummaryTranscriptSegmentOut(BaseModel): speaker: Literal["caller", "assistant"] text: str sequence_no: int source_type: str created_at: str interrupted: bool = False class BrowserSoftphoneConfigOut(BaseModel): enabled: bool = False ws_url: str | None = None sip_uri: str | None = None authorization_username: str | None = None password: str | None = None display_name: str | None = None ice_servers: list[dict] = Field(default_factory=list) operator_extension: str | None = None class IvrFlowCreate(BaseModel): name: str = Field(min_length=2) description: str = "" queue_id: str entry_node_id: str flow_json: dict = Field(default_factory=dict) is_active: bool = True class IvrFlowUpdate(BaseModel): name: str | None = Field(default=None, min_length=2) description: str | None = None entry_node_id: str | None = None flow_json: dict | None = None is_active: bool | None = None class IvrFlowOut(BaseModel): flow_id: str name: str description: str channel: Literal["voice"] queue_id: str version: int is_active: bool entry_node_id: str flow_json: dict created_at: str updated_at: str class IvrSessionStartIn(BaseModel): call_id: str queue_id: str interaction_id: str | None = None class IvrDtmfIn(BaseModel): digit: str = Field(min_length=1, max_length=1) class IvrSessionOut(BaseModel): session_id: str call_id: str interaction_id: str | None = None flow_id: str queue_id: str current_node_id: str entered_digits: list[str] = Field(default_factory=list) status: IvrSessionStatus outcome_code: str | None = None resolved_queue_id: str | None = None resolved_queue_code: str | None = None completed_at: str | None = None created_at: str updated_at: str class EventEnvelope(BaseModel): event_id: str event_type: str event_version: int = 1 occurred_at: str producer: str entity_type: str entity_id: str correlation_id: str | None = None routing_key: str payload: dict = Field(default_factory=dict) class EventOutboxItem(BaseModel): event_id: str event_type: str event_version: int producer_service: str entity_type: str entity_id: str correlation_id: str | None = None routing_key: str payload: dict = Field(default_factory=dict) status: EventOutboxStatus attempt_count: int last_error: str | None = None available_at: str published_at: str | None = None created_at: str updated_at: str class TelegramWebhookIn(BaseModel): chat_id: str text: str customer_external_id: str | None = None payload: dict = Field(default_factory=dict) class TelegramWebhookOut(TelegramWebhookIn): message_id: str thread_id: str | None = None interaction_id: str | None = None direction: Literal["inbound", "outbound", "system"] = "inbound" created_at: str class TelegramThreadOut(BaseModel): thread_id: str chat_id: str interaction_id: str telegram_user_id: str | None = None username: str | None = None display_name: str | None = None queue_id: str | None = None status: InteractionStatus claimed_by_user: str | None = None claimed_at: str | None = None ai_session_id: str | None = None ai_state: AIThreadState | None = None ai_handoff_reason: str | None = None ai_last_model_at: str | None = None last_message_at: str last_message_preview: str created_at: str updated_at: str class TelegramThreadMessageOut(BaseModel): message_id: str thread_id: str interaction_id: str chat_id: str direction: Literal["inbound", "outbound", "system"] text: str telegram_message_id_external: str | None = None operator_user: str | None = None author_type: Literal["customer", "human", "ai", "system"] = "customer" author_id: str | None = None delivery_status: str | None = None payload: dict = Field(default_factory=dict) created_at: str class TelegramThreadAISummaryOut(BaseModel): thread_id: str session_id: str status_label: str status_tone: Literal["answered", "handoff"] customer_request_text: str ai_outcome_text: str handoff_reason: str recommended_next_step: str generated_at: str class TelegramThreadReplyIn(BaseModel): text: str = Field(min_length=1) class TelegramThreadEscalateIn(BaseModel): target_queue_id: str = Field(min_length=1) class AITelegramEnqueueIn(BaseModel): trigger_message_id: str | None = None class AITelegramPauseIn(BaseModel): reason: str = Field(min_length=1) actor_user: str | None = None class VoiceAISessionCreateIn(BaseModel): call_id: str = Field(min_length=1) linked_id: str | None = None interaction_id: str = Field(min_length=1) queue_id: str = Field(min_length=1) caller_number: str | None = None caller_name: str | None = None agent_profile: str = "voice_support" language_hint: str | None = None handoff_queue_id: str | None = None metadata: dict = Field(default_factory=dict) class VoiceAISessionOut(BaseModel): voice_session_id: str ai_session_id: str | None = None status: VoiceAIState class VoiceAIMediaBridgeEventIn(BaseModel): event_type: Literal["requested", "ended"] media_uuid: str = Field(min_length=1) call_id: str = Field(min_length=1) linked_id: str | None = None channel: str | None = None service_address: str | None = None reason: str | None = None class VoiceAITelephonyEventIn(BaseModel): event_type: Literal["call.connected", "call.ended", "recording.ready", "operator.connected"] payload: dict = Field(default_factory=dict) class VoiceAITurnIn(BaseModel): voice_session_id: str = Field(min_length=1) call_id: str = Field(min_length=1) interaction_id: str = Field(min_length=1) transcript_text: str = Field(min_length=1) language: str | None = None sequence_no: int = Field(default=1, ge=1) barge_in: bool = False metadata: dict = Field(default_factory=dict) class VoiceAITurnDecisionOut(BaseModel): language: str intent: str reply_text: str confidence: float needs_handoff: bool handoff_reason: str | None = None case_action: Literal["none", "close", "escalate", "keep_open"] = "keep_open" kb_refs: list[str] = Field(default_factory=list) summary_text: str = "" model: str | None = None latency_ms: int | None = None status: VoiceAIState = "active" metadata: dict = Field(default_factory=dict) class VoiceStartResult(BaseModel): language: str customer_id: str | None = None customer_name_status: VoiceStartNameStatus = "name_not_obtained" customer_name_value: str | None = None customer_name_source: VoiceStartNameSource = "none" downstream_queue_id: str | None = None downstream_queue_code: str | None = None resolved_at: str | None = None class VoiceAIStartIn(BaseModel): voice_session_id: str = Field(min_length=1) call_id: str = Field(min_length=1) interaction_id: str = Field(min_length=1) customer_id: str | None = None language_hint: str | None = None agent_profile: str = "voice_support" metadata: dict = Field(default_factory=dict) class VoiceAIStartOut(BaseModel): session_id: str language: str greeting_text: str disclosure_required: bool = True needs_handoff: bool = False handoff_reason: str | None = None summary_text: str = "" start_result: VoiceStartResult | None = None metadata: dict = Field(default_factory=dict) class VoiceAIHandoffRequestIn(BaseModel): voice_session_id: str = Field(min_length=1) ai_session_id: str | None = None interaction_id: str = Field(min_length=1) target_queue_id: str | None = None reason: str = Field(min_length=1) summary: dict = Field(default_factory=dict) metadata: dict = Field(default_factory=dict) class VoiceAICallStateUpdateIn(BaseModel): voice_session_id: str | None = None ai_session_id: str | None = None ai_state: VoiceAIState handoff_reason: str | None = None metadata: dict = Field(default_factory=dict) class TelegramThreadAIReplyIn(BaseModel): text: str = Field(min_length=1) agent_profile: str = "telegram_support" model: str | None = None trigger_message_id: str | None = None language: str | None = None confidence: float | None = None kb_refs: list[str] = Field(default_factory=list) payload: dict = Field(default_factory=dict) class TelegramThreadAIHandoffIn(BaseModel): reason: str = Field(min_length=1) agent_profile: str = "telegram_support" trigger_message_id: str | None = None confidence: float | None = None payload: dict = Field(default_factory=dict) class WhatsAppWebhookIn(BaseModel): chat_id: str text: str = "" external_message_id: str | None = None whatsapp_user_id: str | None = None phone_number: str | None = None display_name: str | None = None customer_id: str | None = None external_subject: str | None = None queue_id: str | None = None is_group: bool = False payload: dict = Field(default_factory=dict) class WhatsAppWebhookOut(BaseModel): message_id: str chat_id: str text: str external_message_id: str | None = None customer_id: str | None = None payload: dict = Field(default_factory=dict) thread_id: str | None = None interaction_id: str | None = None direction: Literal["inbound", "outbound", "system"] = "inbound" created_at: str class WhatsAppThreadOut(BaseModel): thread_id: str chat_id: str interaction_id: str whatsapp_user_id: str | None = None phone_number: str | None = None display_name: str | None = None queue_id: str | None = None is_group: bool = False status: InteractionStatus claimed_by_user: str | None = None claimed_at: str | None = None ai_session_id: str | None = None ai_state: AIThreadState | None = None ai_handoff_reason: str | None = None ai_last_model_at: str | None = None unread_count: int = 0 last_message_at: str last_message_preview: str created_at: str updated_at: str class WhatsAppThreadMessageOut(BaseModel): message_id: str thread_id: str interaction_id: str chat_id: str direction: Literal["inbound", "outbound", "system"] text: str whatsapp_message_id_external: str | None = None operator_user: str | None = None author_type: Literal["customer", "human", "ai", "system"] = "customer" author_id: str | None = None customer_id: str | None = None delivery_status: str | None = None payload: dict = Field(default_factory=dict) created_at: str class WhatsAppThreadAISummaryOut(BaseModel): thread_id: str session_id: str status_label: str status_tone: Literal["answered", "handoff"] customer_request_text: str ai_outcome_text: str handoff_reason: str recommended_next_step: str generated_at: str class WhatsAppThreadReplyIn(BaseModel): text: str = Field(min_length=1) class WhatsAppThreadEscalateIn(BaseModel): target_queue_id: str = Field(min_length=1) class AIWhatsAppEnqueueIn(BaseModel): trigger_message_id: str | None = None class AIWhatsAppPauseIn(BaseModel): reason: str = Field(min_length=1) actor_user: str | None = None class WhatsAppThreadAIReplyIn(BaseModel): text: str = Field(min_length=1) agent_profile: str = "whatsapp_support" model: str | None = None trigger_message_id: str | None = None language: str | None = None confidence: float | None = None kb_refs: list[str] = Field(default_factory=list) payload: dict = Field(default_factory=dict) class WhatsAppThreadAIHandoffIn(BaseModel): reason: str = Field(min_length=1) agent_profile: str = "whatsapp_support" trigger_message_id: str | None = None confidence: float | None = None payload: dict = Field(default_factory=dict) class WebchatMessageIn(BaseModel): session_id: str text: str = Field(min_length=1) visitor_name: str | None = None customer_external_id: str | None = None queue_id: str | None = None priority: int = Field(default=3, ge=1, le=5) payload: dict = Field(default_factory=dict) class WebchatMessageOut(WebchatMessageIn): message_id: str interaction_id: str | None = None created_at: str class EmailMessageIn(BaseModel): from_email: str subject: str = Field(min_length=3) body: str = Field(min_length=1) customer_external_id: str | None = None queue_id: str | None = None priority: int = Field(default=3, ge=1, le=5) payload: dict = Field(default_factory=dict) class EmailMessageOut(EmailMessageIn): message_id: str interaction_id: str | None = None created_at: str class KBCategoryCreate(BaseModel): name: str description: str = "" class KBCategoryOut(KBCategoryCreate): category_id: str created_at: str class KBArticleCreate(BaseModel): category_id: str article_group_id: str | None = None language: str = "ru" title: str body: str tags: list[str] = Field(default_factory=list) class KBArticleUpdate(BaseModel): article_group_id: str | None = None language: str | None = None title: str | None = None body: str | None = None tags: list[str] | None = None class KBArticleOut(KBArticleCreate): article_id: str created_at: str updated_at: str class KpiEventIn(BaseModel): queue_id: str channel: Channel = "voice" agent_id: str | None = None answered: bool wait_seconds: int = Field(ge=0) handle_seconds: int = Field(ge=0) abandoned: bool = False resolved_first_contact: bool = False created_at: str | None = None class ReportingInteractionFactIn(BaseModel): interaction_id: str = Field(min_length=1) channel: Channel | None = None queue_id: str | None = None agent_id: str | None = None status: InteractionStatus | None = None created_at: str | None = None closed_at: str | None = None answered: bool | None = None abandoned: bool | None = None wait_seconds: int | None = Field(default=None, ge=0) handle_seconds: int | None = Field(default=None, ge=0) resolved_first_contact: bool | None = None source: str = Field(min_length=1) class ReportingMetricCoverageOut(BaseModel): status: Literal["exact", "partial", "unavailable"] = "unavailable" supported_channels: list[str] = Field(default_factory=list) exact_rows: int = 0 total_rows: int = 0 note: str | None = None class ReportingKpiCoverageOut(BaseModel): metric_status: dict[str, Literal["exact", "partial", "unavailable"]] = Field(default_factory=dict) supported_channels: dict[str, list[str]] = Field(default_factory=dict) exact_rows: int = 0 total_rows: int = 0 note: str | None = None metric_details: dict[str, ReportingMetricCoverageOut] = Field(default_factory=dict) class ReportingDrilldownFiltersOut(BaseModel): from_ts: str to_ts: str metric: str queue_id: str | None = None channel: str | None = None sl_threshold_seconds: int = 30 class ReportingDrilldownItemOut(InteractionOut): answered: bool | None = None abandoned: bool | None = None wait_seconds: int | None = None handle_seconds: int | None = None within_sla: bool | None = None resolved_first_contact: bool | None = None class ReportingDrilldownOut(BaseModel): items: list[ReportingDrilldownItemOut] = Field(default_factory=list) total: int limit: int offset: int metric: str coverage: ReportingMetricCoverageOut = Field(default_factory=ReportingMetricCoverageOut) filters: ReportingDrilldownFiltersOut class ReportingTimeseriesFiltersOut(BaseModel): from_ts: str to_ts: str metric: str interval: Literal["hour", "day"] = "day" queue_id: str | None = None channel: str | None = None sl_threshold_seconds: int = 30 class ReportingTimeseriesPointOut(BaseModel): ts: str value: float = 0.0 sample_size: int = 0 class ReportingTimeseriesOut(BaseModel): metric: str interval: Literal["hour", "day"] = "day" filters: ReportingTimeseriesFiltersOut points: list[ReportingTimeseriesPointOut] = Field(default_factory=list) class ReportingAgentAnalyticsFiltersOut(BaseModel): from_ts: str to_ts: str queue_id: str | None = None channel: str | None = None sort_by: Literal[ "interactions_total", "answered_total", "avg_handle_seconds", "fcr_rate", "last_activity_at", "agent_id", ] = "interactions_total" sort_dir: Literal["asc", "desc"] = "desc" limit: int = 25 class ReportingAgentAnalyticsTotalsOut(BaseModel): agents_total: int = 0 agents_with_activity: int = 0 interactions_total: int = 0 answered_total: int = 0 closed_total: int = 0 ready_now: int = 0 busy_now: int = 0 break_now: int = 0 offline_now: int = 0 avg_interactions_per_agent: float = 0.0 avg_handle_seconds: float | None = None avg_fcr_rate: float | None = None class ReportingAgentStateSnapshotOut(BaseModel): by_state: dict[str, int] = Field(default_factory=dict) updated_at: str | None = None class ReportingAgentAnalyticsRowOut(BaseModel): agent_id: str current_state: str | None = None current_queue_id: str | None = None current_state_updated_at: str | None = None dominant_queue_id: str | None = None last_activity_at: str | None = None interactions_total: int = 0 answered_total: int = 0 closed_total: int = 0 abandoned_total: int = 0 avg_wait_seconds: float | None = None avg_handle_seconds: float | None = None answer_rate: float = 0.0 fcr_rate: float | None = None channels: list[str] = Field(default_factory=list) class ReportingAgentAnalyticsTeamRowOut(BaseModel): team_key: str label: str agents_total: int = 0 agents_with_activity: int = 0 interactions_total: int = 0 answered_total: int = 0 avg_handle_seconds: float | None = None fcr_rate: float | None = None ready_now: int = 0 busy_now: int = 0 break_now: int = 0 offline_now: int = 0 class ReportingAgentAnalyticsShiftRowOut(BaseModel): shift_key: Literal["night", "day", "evening"] label: str agents_with_activity: int = 0 interactions_total: int = 0 answered_total: int = 0 avg_handle_seconds: float | None = None fcr_rate: float | None = None class ReportingAgentAnalyticsBreakdownsOut(BaseModel): by_team: list[ReportingAgentAnalyticsTeamRowOut] = Field(default_factory=list) by_shift: list[ReportingAgentAnalyticsShiftRowOut] = Field(default_factory=list) class ReportingAgentAnalyticsTrendFiltersOut(BaseModel): from_ts: str to_ts: str queue_id: str | None = None channel: str | None = None metric: Literal["agents_with_activity", "interactions_per_agent", "avg_handle_seconds", "fcr_rate"] = "interactions_per_agent" interval: Literal["hour", "day"] = "day" class ReportingAgentAnalyticsTrendPointOut(BaseModel): ts: str value: float = 0.0 agents_with_activity: int = 0 interactions_total: int = 0 class ReportingAgentAnalyticsTimeseriesOut(BaseModel): metric: Literal["agents_with_activity", "interactions_per_agent", "avg_handle_seconds", "fcr_rate"] = "interactions_per_agent" interval: Literal["hour", "day"] = "day" filters: ReportingAgentAnalyticsTrendFiltersOut points: list[ReportingAgentAnalyticsTrendPointOut] = Field(default_factory=list) class ReportingAgentAnalyticsOverviewOut(BaseModel): window: AIAnalyticsWindowOut filters: ReportingAgentAnalyticsFiltersOut totals: ReportingAgentAnalyticsTotalsOut = Field(default_factory=ReportingAgentAnalyticsTotalsOut) state_snapshot: ReportingAgentStateSnapshotOut = Field(default_factory=ReportingAgentStateSnapshotOut) breakdowns: ReportingAgentAnalyticsBreakdownsOut = Field(default_factory=ReportingAgentAnalyticsBreakdownsOut) items: list[ReportingAgentAnalyticsRowOut] = Field(default_factory=list) class ReportingSavedViewSnapshot(BaseModel): preset: str = "7d" fromTs: str = "" toTs: str = "" queueId: str = "all" channel: str = "all" compareMode: str = "previous" trendMetric: str = "volume" voiceNameTrendMetric: str = "scenario_calls" aiTrendMetric: str = "containment_rate" agentTrendMetric: str = "interactions_per_agent" class ReportingSavedViewIn(BaseModel): id: str | None = None name: str = Field(min_length=1, max_length=160) snapshot: ReportingSavedViewSnapshot = Field(default_factory=ReportingSavedViewSnapshot) class ReportingSavedViewOut(BaseModel): id: str name: str snapshot: ReportingSavedViewSnapshot = Field(default_factory=ReportingSavedViewSnapshot) created_at: str updated_at: str class AgentStateIn(BaseModel): agent_id: str state: Literal["READY", "BUSY", "BREAK", "OFFLINE"] queue_id: str | None = None class AgentStateOut(AgentStateIn): updated_at: str AgentLevel = Literal["L2", "L3"] AgentStatus = Literal["OFFLINE", "AVAILABLE", "RESERVED", "RINGING", "TALKING", "AFTER_CALL_WORK", "PAUSED"] class AgentCreate(BaseModel): tenant_ids: list[str] = Field(default_factory=list) extension: str = Field(min_length=1) endpoint: str | None = None display_name: str = Field(min_length=1) level: AgentLevel skills: list[str] = Field(default_factory=list) max_concurrent_calls: int = Field(default=1, ge=1) enabled: bool = True class AgentStatusUpdateIn(BaseModel): status: AgentStatus class AgentPoolOut(BaseModel): agent_id: str tenant_ids: list[str] = Field(default_factory=list) extension: str endpoint: str | None = None display_name: str level: AgentLevel skills: list[str] = Field(default_factory=list) status: AgentStatus current_call_id: str | None = None max_concurrent_calls: int enabled: bool calls_handled_count: int = 0 created_at: str updated_at: str class EscalationRequestIn(BaseModel): target_level: AgentLevel reason_code: str = Field(min_length=1) topic: str | None = None required_skills: list[str] = Field(default_factory=list) priority: int = Field(default=3, ge=1, le=5) summary: str | None = None class EscalationOut(BaseModel): escalation_id: str call_id: str tenant_id: str | None = None from_level: str to_level: str reason_code: str required_skills: list[str] = Field(default_factory=list) priority: int topic: str | None = None summary: str | None = None status: str assigned_agent_id: str | None = None requested_at: str connected_at: str | None = None completed_at: str | None = None class RoutingAgentReserveIn(BaseModel): call_id: str = Field(min_length=1) level: AgentLevel tenant_id: str | None = None required_skills: list[str] = Field(default_factory=list) exclude_agent_ids: list[str] = Field(default_factory=list) class RoutingAgentReserveOut(BaseModel): agent_id: str extension: str endpoint: str | None = None display_name: str class AIAnalyticsWindowOut(BaseModel): from_ts: str to_ts: str class AIAnalyticsFiltersOut(AIAnalyticsWindowOut): queue_id: str | None = None channel: str | None = None class AIAnalyticsTotalsOut(BaseModel): sessions_started: int = 0 sessions_contained: int = 0 sessions_handoff: int = 0 sessions_closed: int = 0 sessions_closed_without_operator: int = 0 assistant_turns: int = 0 class AIAnalyticsMetricsOut(BaseModel): containment_rate: float = 0.0 handoff_rate: float = 0.0 ai_latency_avg_ms: float | None = None ai_latency_p95_ms: float | None = None closed_without_operator_rate: float = 0.0 human_touched_rate: float = 0.0 class AIAnalyticsChannelBreakdownOut(BaseModel): channel: str sessions_started: int = 0 sessions_contained: int = 0 sessions_handoff: int = 0 sessions_closed_without_operator: int = 0 assistant_turns: int = 0 containment_rate: float = 0.0 handoff_rate: float = 0.0 closed_without_operator_rate: float = 0.0 ai_latency_avg_ms: float | None = None ai_only_sessions: int = 0 human_touched_sessions: int = 0 class AIAnalyticsOutcomeBreakdownOut(BaseModel): outcome: Literal["contained", "handoff", "human_touched", "closed_without_operator", "active", "error"] label: str sessions: int = 0 share: float = 0.0 class AIAnalyticsHandoffReasonBreakdownOut(BaseModel): reason_key: str label: str sessions: int = 0 share: float = 0.0 class AIAnalyticsBreakdownsOut(BaseModel): by_channel: list[AIAnalyticsChannelBreakdownOut] = Field(default_factory=list) by_outcome: list[AIAnalyticsOutcomeBreakdownOut] = Field(default_factory=list) by_handoff_reason: list[AIAnalyticsHandoffReasonBreakdownOut] = Field(default_factory=list) class AIAnalyticsCoverageOut(BaseModel): sessions_with_interaction_id: int = 0 sessions_with_queue_id: int = 0 sessions_with_latency_turns: int = 0 sessions_with_terminal_state: int = 0 sessions_with_handoff_reason: int = 0 class AIAnalyticsOverviewOut(BaseModel): window: AIAnalyticsWindowOut filters: AIAnalyticsFiltersOut totals: AIAnalyticsTotalsOut metrics: AIAnalyticsMetricsOut breakdowns: AIAnalyticsBreakdownsOut coverage: AIAnalyticsCoverageOut class AIAnalyticsTimeseriesPointOut(BaseModel): ts: str value: float | None = None sessions: int = 0 assistant_turns: int = 0 class AIAnalyticsTimeseriesOut(BaseModel): metric: Literal["containment_rate", "handoff_rate", "ai_latency_avg_ms", "closed_without_operator_rate", "human_touched_rate"] interval: Literal["hour", "day"] filters: AIAnalyticsFiltersOut points: list[AIAnalyticsTimeseriesPointOut] = Field(default_factory=list) class VoiceNameFlowAnalyticsFiltersOut(AIAnalyticsWindowOut): queue_id: str | None = None language: str | None = None class VoiceNameFlowAnalyticsTotalsOut(BaseModel): scenario_calls: int = 0 start_obtained: int = 0 downstream_ai_obtained: int = 0 followup_required: int = 0 name_not_obtained: int = 0 manual_corrected: int = 0 handoff_confirmed_name: int = 0 handoff_unconfirmed_name: int = 0 needed_downstream: int = 0 class VoiceNameFlowAnalyticsMetricsOut(BaseModel): start_capture_rate: float = 0.0 downstream_rescue_rate: float = 0.0 handoff_unconfirmed_rate: float = 0.0 manual_correction_rate: float = 0.0 class VoiceNameFlowAnalyticsFunnelStageOut(BaseModel): stage: Literal[ "scenario_calls", "start_obtained", "needed_downstream", "downstream_ai_obtained", "handoff_confirmed_name", "handoff_unconfirmed_name", ] label: str sessions: int = 0 share: float = 0.0 class VoiceNameFlowAnalyticsLanguageBreakdownOut(BaseModel): language: str scenario_calls: int = 0 start_obtained: int = 0 downstream_ai_obtained: int = 0 followup_required: int = 0 name_not_obtained: int = 0 manual_corrected: int = 0 handoff_confirmed_name: int = 0 handoff_unconfirmed_name: int = 0 start_capture_rate: float = 0.0 downstream_rescue_rate: float = 0.0 handoff_unconfirmed_rate: float = 0.0 manual_correction_rate: float = 0.0 class VoiceNameFlowAnalyticsQueueBreakdownOut(BaseModel): queue_id: str scenario_calls: int = 0 start_obtained: int = 0 downstream_ai_obtained: int = 0 followup_required: int = 0 name_not_obtained: int = 0 manual_corrected: int = 0 handoff_confirmed_name: int = 0 handoff_unconfirmed_name: int = 0 start_capture_rate: float = 0.0 downstream_rescue_rate: float = 0.0 handoff_unconfirmed_rate: float = 0.0 manual_correction_rate: float = 0.0 class VoiceNameFlowAnalyticsHandoffBreakdownOut(BaseModel): outcome: Literal["confirmed_name", "unconfirmed_name"] label: str sessions: int = 0 share: float = 0.0 class VoiceNameFlowAnalyticsBreakdownsOut(BaseModel): funnel: list[VoiceNameFlowAnalyticsFunnelStageOut] = Field(default_factory=list) by_language: list[VoiceNameFlowAnalyticsLanguageBreakdownOut] = Field(default_factory=list) by_queue: list[VoiceNameFlowAnalyticsQueueBreakdownOut] = Field(default_factory=list) handoff: list[VoiceNameFlowAnalyticsHandoffBreakdownOut] = Field(default_factory=list) class VoiceNameFlowAnalyticsCoverageOut(BaseModel): sessions_with_start_decision: int = 0 sessions_with_final_ai_state: int = 0 sessions_with_manual_overlay: int = 0 note: str | None = None class VoiceNameFlowAnalyticsOverviewOut(BaseModel): window: AIAnalyticsWindowOut filters: VoiceNameFlowAnalyticsFiltersOut totals: VoiceNameFlowAnalyticsTotalsOut = Field(default_factory=VoiceNameFlowAnalyticsTotalsOut) metrics: VoiceNameFlowAnalyticsMetricsOut = Field(default_factory=VoiceNameFlowAnalyticsMetricsOut) breakdowns: VoiceNameFlowAnalyticsBreakdownsOut = Field(default_factory=VoiceNameFlowAnalyticsBreakdownsOut) coverage: VoiceNameFlowAnalyticsCoverageOut = Field(default_factory=VoiceNameFlowAnalyticsCoverageOut) class VoiceNameFlowAnalyticsTimeseriesPointOut(BaseModel): ts: str value: float | None = None scenario_calls: int = 0 denominator: int = 0 class VoiceNameFlowAnalyticsTimeseriesOut(BaseModel): metric: Literal[ "scenario_calls", "start_capture_rate", "downstream_rescue_rate", "handoff_unconfirmed_rate", "manual_correction_rate", ] interval: Literal["hour", "day"] filters: VoiceNameFlowAnalyticsFiltersOut points: list[VoiceNameFlowAnalyticsTimeseriesPointOut] = Field(default_factory=list) class AIAnalyticsDrilldownFiltersOut(AIAnalyticsFiltersOut): slice: Literal["all", "contained", "handoff", "human_touched", "closed_without_operator", "active", "error"] = "all" reason_key: str | None = None status: str | None = None q: str | None = None sort_by: Literal["created_at", "updated_at", "ai_latency_avg_ms", "status"] = "created_at" sort_dir: Literal["asc", "desc"] = "desc" class AIAnalyticsDrilldownItemOut(BaseModel): session_id: str thread_id: str | None = None interaction_id: str | None = None channel: str queue_id: str | None = None status: str created_at: str updated_at: str closed_at: str | None = None contained: bool = False handoff: bool = False human_touched: bool = False closed_without_operator: bool = False reason_key: str | None = None reason_label: str | None = None raw_handoff_reason: str | None = None assigned_to: str | None = None claimed_by_user: str | None = None assistant_turns: int = 0 user_turns: int = 0 tool_turns: int = 0 ai_latency_avg_ms: float | None = None ai_latency_p95_ms: float | None = None class AIAnalyticsDrilldownOut(BaseModel): items: list[AIAnalyticsDrilldownItemOut] = Field(default_factory=list) total: int limit: int offset: int filters: AIAnalyticsDrilldownFiltersOut coverage: AIAnalyticsCoverageOut = Field(default_factory=AIAnalyticsCoverageOut) class AIAnalyticsSessionLinkedInteractionOut(BaseModel): interaction_id: str | None = None channel: str | None = None queue_id: str | None = None status: str | None = None assigned_to: str | None = None subject: str | None = None created_at: str | None = None updated_at: str | None = None class AIAnalyticsSessionEventOut(BaseModel): ts: str event_type: str label: str role: str | None = None source_type: str | None = None latency_ms: int | None = None finish_reason: str | None = None status: str | None = None metadata: dict = Field(default_factory=dict) class AIAnalyticsSessionDetailOut(BaseModel): session: AIAnalyticsDrilldownItemOut interaction: AIAnalyticsSessionLinkedInteractionOut | None = None timeline: list[AIAnalyticsSessionEventOut] = Field(default_factory=list) VoiceAISummaryOut.model_rebuild() CustomerHistoryOut.model_rebuild() import inspect for name, cls in list(locals().items()): if inspect.isclass(cls) and issubclass(cls, BaseModel) and cls is not BaseModel: try: cls.model_rebuild() except: pass