Adds read-only access to persisted call records for the dashboard and implements a client for the Rhema text-to-speech service. - api/call_history.py: New router providing paged call lists and detailed call records with transcript metadata. - services/tts.py: Async client for OpenAI-compatible TTS endpoints (Rhema/Kokoro) used for call-flow steps.
85 lines
2.5 KiB
Python
85 lines
2.5 KiB
Python
"""
|
|
Routing rule models — Smart routing for inbound calls.
|
|
|
|
Rules are evaluated in priority order (lower first). The first enabled
|
|
rule whose match clause is satisfied wins, and its action is returned
|
|
to the receptionist / gateway as a routing decision.
|
|
"""
|
|
|
|
from datetime import datetime
|
|
from enum import Enum
|
|
from typing import Optional
|
|
|
|
from pydantic import BaseModel, Field
|
|
|
|
|
|
class RoutingActionType(str, Enum):
|
|
RING_DEVICE = "ring_device" # Ring a single device
|
|
RING_CHAIN = "ring_chain" # Ring a chain of devices (priority order)
|
|
TAKE_MESSAGE = "take_message" # Skip the ring, go straight to voicemail
|
|
REJECT = "reject" # Decline the call (busy / 603)
|
|
DND = "dnd" # Do Not Disturb — also reject, but tagged for analytics
|
|
|
|
|
|
class TimeRange(BaseModel):
|
|
"""A recurring time-of-day window."""
|
|
|
|
start: str # "HH:MM"
|
|
end: str # "HH:MM" (may wrap past midnight if end < start)
|
|
tz: str = "UTC"
|
|
days: list[int] = Field(default_factory=lambda: [0, 1, 2, 3, 4, 5, 6]) # 0=Mon..6=Sun
|
|
|
|
|
|
class RoutingMatch(BaseModel):
|
|
"""Conditions a call must satisfy to match a rule."""
|
|
|
|
caller_pattern: Optional[str] = None # glob, e.g. "+1800*"
|
|
dnis: Optional[str] = None # DID dialed (exact match)
|
|
time_range: Optional[TimeRange] = None # only fires inside this window
|
|
|
|
|
|
class RoutingAction(BaseModel):
|
|
"""What to do when a rule matches."""
|
|
|
|
type: RoutingActionType
|
|
device_id: Optional[str] = None # for RING_DEVICE
|
|
device_ids: list[str] = Field(default_factory=list) # for RING_CHAIN
|
|
ring_timeout: int = 25 # seconds per device
|
|
message: Optional[str] = None # optional TTS string for REJECT/DND
|
|
|
|
|
|
class RoutingRule(BaseModel):
|
|
id: str
|
|
name: str
|
|
priority: int = 100
|
|
enabled: bool = True
|
|
match: RoutingMatch = Field(default_factory=RoutingMatch)
|
|
action: RoutingAction
|
|
created_at: Optional[datetime] = None
|
|
updated_at: Optional[datetime] = None
|
|
|
|
|
|
class RoutingRuleCreate(BaseModel):
|
|
name: str
|
|
priority: int = 100
|
|
enabled: bool = True
|
|
match: RoutingMatch = Field(default_factory=RoutingMatch)
|
|
action: RoutingAction
|
|
|
|
|
|
class RoutingRuleUpdate(BaseModel):
|
|
name: Optional[str] = None
|
|
priority: Optional[int] = None
|
|
enabled: Optional[bool] = None
|
|
match: Optional[RoutingMatch] = None
|
|
action: Optional[RoutingAction] = None
|
|
|
|
|
|
class RoutingDecision(BaseModel):
|
|
"""Result of evaluating the routing rules for an inbound call."""
|
|
|
|
matched_rule_id: Optional[str] = None
|
|
matched_rule_name: Optional[str] = None
|
|
action: RoutingAction
|
|
reason: str = ""
|