feat: add initial Hold Slayer AI telephony gateway implementation
Complete project scaffolding and core implementation of an AI-powered telephony system that calls companies, navigates IVR menus, waits on hold, and transfers to the user when a human answers. Key components: - FastAPI server with REST API, WebSocket, and MCP (SSE) interfaces - SIP/VoIP call management via PJSUA2 with RTP audio streaming - LLM-powered IVR navigation using OpenAI/Anthropic with tool calling - Hold detection service combining audio analysis and silence detection - Real-time STT (Whisper/Deepgram) and TTS (OpenAI/Piper) pipelines - Call recording with per-channel and mixed audio capture - Event bus (asyncio pub/sub) for real-time client updates - Web dashboard with live call monitoring - SQLite persistence via SQLAlchemy with call history and analytics - Notification support (email, SMS, webhook, desktop) - Docker Compose deployment with Opal VoIP and Opal Media containers - Comprehensive test suite with unit, integration, and E2E tests - Simplified .gitignore and full project documentation in README
This commit is contained in:
108
models/call_flow.py
Normal file
108
models/call_flow.py
Normal file
@@ -0,0 +1,108 @@
|
||||
"""
|
||||
Call Flow models — IVR navigation trees.
|
||||
|
||||
Store known IVR structures for phone numbers you call regularly.
|
||||
The Hold Slayer follows the map instead of exploring blind.
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
from enum import Enum
|
||||
from typing import Optional
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class ActionType(str, Enum):
|
||||
"""Actions the Hold Slayer can take at each IVR step."""
|
||||
|
||||
DTMF = "dtmf" # Press a button
|
||||
SPEAK = "speak" # Say something (for speech-recognition IVRs)
|
||||
WAIT = "wait" # Wait for prompt
|
||||
LISTEN = "listen" # Listen and let LLM decide
|
||||
HOLD = "hold" # On hold — activate hold detection
|
||||
TRANSFER = "transfer" # Transfer to user's device
|
||||
|
||||
|
||||
class CallFlowStep(BaseModel):
|
||||
"""A single step in an IVR navigation tree."""
|
||||
|
||||
id: str
|
||||
description: str # Human-readable: "Main menu"
|
||||
expect: Optional[str] = None # What we expect to hear (regex or keywords)
|
||||
action: ActionType
|
||||
action_value: Optional[str] = None # DTMF digit(s), speech text, device target
|
||||
timeout: int = 30 # Seconds to wait before retry/fallback
|
||||
next_step: Optional[str] = None # Next step ID on success
|
||||
fallback_step: Optional[str] = None # Step ID if unexpected response
|
||||
notes: Optional[str] = None # "They changed this menu in Jan 2025"
|
||||
|
||||
|
||||
class CallFlow(BaseModel):
|
||||
"""A complete IVR navigation tree for a phone number."""
|
||||
|
||||
id: str
|
||||
name: str # "Chase Bank - Main Line"
|
||||
phone_number: str # "+18005551234"
|
||||
description: str = ""
|
||||
last_verified: Optional[datetime] = None
|
||||
steps: list[CallFlowStep]
|
||||
tags: list[str] = Field(default_factory=list)
|
||||
notes: Optional[str] = None
|
||||
|
||||
# Stats from previous runs
|
||||
avg_hold_time: Optional[int] = None # seconds
|
||||
success_rate: Optional[float] = None # 0.0 - 1.0
|
||||
last_used: Optional[datetime] = None
|
||||
times_used: int = 0
|
||||
|
||||
def get_step(self, step_id: str) -> Optional[CallFlowStep]:
|
||||
"""Look up a step by ID."""
|
||||
for step in self.steps:
|
||||
if step.id == step_id:
|
||||
return step
|
||||
return None
|
||||
|
||||
def first_step(self) -> Optional[CallFlowStep]:
|
||||
"""Get the first step in the flow."""
|
||||
return self.steps[0] if self.steps else None
|
||||
|
||||
def steps_by_id(self) -> dict[str, CallFlowStep]:
|
||||
"""Return a dict mapping step ID -> step for fast lookups."""
|
||||
return {s.id: s for s in self.steps}
|
||||
|
||||
|
||||
class CallFlowCreate(BaseModel):
|
||||
"""Request model for creating a new call flow."""
|
||||
|
||||
name: str
|
||||
phone_number: str
|
||||
description: str = ""
|
||||
steps: list[CallFlowStep]
|
||||
tags: list[str] = Field(default_factory=list)
|
||||
notes: Optional[str] = None
|
||||
|
||||
|
||||
class CallFlowUpdate(BaseModel):
|
||||
"""Request model for updating an existing call flow."""
|
||||
|
||||
name: Optional[str] = None
|
||||
description: Optional[str] = None
|
||||
steps: Optional[list[CallFlowStep]] = None
|
||||
tags: Optional[list[str]] = None
|
||||
notes: Optional[str] = None
|
||||
last_verified: Optional[datetime] = None
|
||||
|
||||
|
||||
class CallFlowSummary(BaseModel):
|
||||
"""Lightweight summary for list views."""
|
||||
|
||||
id: str
|
||||
name: str
|
||||
phone_number: str
|
||||
description: str = ""
|
||||
step_count: int
|
||||
avg_hold_time: Optional[int] = None
|
||||
success_rate: Optional[float] = None
|
||||
last_used: Optional[datetime] = None
|
||||
times_used: int = 0
|
||||
tags: list[str] = Field(default_factory=list)
|
||||
Reference in New Issue
Block a user