1210 lines
33 KiB
Python
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)
|