Replaces the hardcoded single-extension redirect for AI->human call
escalation with a real Agent Pool + Routing Engine:
- agents/escalations/routing_rules tables (migration 0031), asterisk_call_links
gains tenant_id/current_level/required_skills_json/priority.
- services/routing_service/engine.py: level/tenant/skill filtered agent
selection with atomic (CAS) reservation, no double-booking.
- routing-service: /agents CRUD + /internal/routing/reserve-agent and
/internal/routing/release-agent.
- asterisk-bridge-service: voice_ai.request_handoff now uses the Routing
Engine automatically for any queue_code configured in
ASTERISK_QUEUE_LEVEL_MAP_JSON (all other queue_codes keep the existing
static ASTERISK_TRANSFER_TARGET_MAP_JSON behavior unchanged); new
POST /asterisk/live-calls/{call_id}/escalations entrypoint; agent is
released back to AVAILABLE and the escalation closed when the call ends.
Targets the Tele2 Kazgaz DID +77476456048 (from-tele2-kazgaz context) as the
first queue wired to real L2 routing instead of AI-only.
Known gap (documented in docs/architecture/l1-l2-routing-engine.md):
automatic no-answer retry-to-next-agent needs a small, separately reviewed
dialplan change and is left for a follow-up MR rather than guessed at blind.
Tests: services/routing_service/engine.py covered by
tests/test_routing_engine.py (selection filtering, atomic reservation,
release); existing test_asterisk_bridge_service.py and
test_routing_service_pg_counter.py suites still pass unmodified.
1630 lines
47 KiB
Python
1630 lines
47 KiB
Python
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
|