Files
call-center/services/shared/models.py
T

1210 lines
33 KiB
Python

from __future__ import annotations
from typing import Literal
from pydantic import BaseModel, Field
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"]
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 CustomerOut(CustomerCreate):
customer_id: str
created_at: str
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_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"
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"
class VoiceAIStartOut(BaseModel):
session_id: str
language: str
greeting_text: str
disclosure_required: bool = True
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)
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"
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
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 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)