Files
hold-slayer/models/routing.py
Robert Helewka 63f1a270bb feat: add call history API endpoints and TTS service client
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.
2026-05-22 06:28:33 -04:00

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 = ""