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:
173
tests/test_call_flows.py
Normal file
173
tests/test_call_flows.py
Normal file
@@ -0,0 +1,173 @@
|
||||
"""
|
||||
Tests for call flow models and serialization.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
from models.call_flow import ActionType, CallFlow, CallFlowCreate, CallFlowStep, CallFlowSummary
|
||||
|
||||
|
||||
class TestCallFlowStep:
|
||||
"""Test CallFlowStep model."""
|
||||
|
||||
def test_basic_dtmf_step(self):
|
||||
step = CallFlowStep(
|
||||
id="press_1",
|
||||
description="Press 1 for English",
|
||||
action=ActionType.DTMF,
|
||||
action_value="1",
|
||||
expect="for english|para español",
|
||||
next_step="main_menu",
|
||||
)
|
||||
assert step.id == "press_1"
|
||||
assert step.action == ActionType.DTMF
|
||||
assert step.action_value == "1"
|
||||
assert step.timeout == 30 # default
|
||||
|
||||
def test_hold_step(self):
|
||||
step = CallFlowStep(
|
||||
id="hold_queue",
|
||||
description="On hold waiting for agent",
|
||||
action=ActionType.HOLD,
|
||||
timeout=7200,
|
||||
next_step="agent_connected",
|
||||
notes="Average hold: 25-45 min. Plays Vivaldi. Kill me.",
|
||||
)
|
||||
assert step.action == ActionType.HOLD
|
||||
assert step.timeout == 7200
|
||||
assert "Vivaldi" in step.notes
|
||||
|
||||
def test_transfer_step(self):
|
||||
step = CallFlowStep(
|
||||
id="connected",
|
||||
description="Agent picked up!",
|
||||
action=ActionType.TRANSFER,
|
||||
action_value="sip_phone",
|
||||
)
|
||||
assert step.action == ActionType.TRANSFER
|
||||
|
||||
|
||||
class TestCallFlow:
|
||||
"""Test CallFlow model."""
|
||||
|
||||
@pytest.fixture
|
||||
def sample_flow(self):
|
||||
return CallFlow(
|
||||
id="test-bank",
|
||||
name="Test Bank - Main Line",
|
||||
phone_number="+18005551234",
|
||||
description="Test bank IVR",
|
||||
steps=[
|
||||
CallFlowStep(
|
||||
id="greeting",
|
||||
description="Language selection",
|
||||
action=ActionType.DTMF,
|
||||
action_value="1",
|
||||
expect="for english",
|
||||
next_step="main_menu",
|
||||
),
|
||||
CallFlowStep(
|
||||
id="main_menu",
|
||||
description="Main menu",
|
||||
action=ActionType.LISTEN,
|
||||
next_step="agent_request",
|
||||
fallback_step="agent_request",
|
||||
),
|
||||
CallFlowStep(
|
||||
id="agent_request",
|
||||
description="Request agent",
|
||||
action=ActionType.DTMF,
|
||||
action_value="0",
|
||||
next_step="hold_queue",
|
||||
),
|
||||
CallFlowStep(
|
||||
id="hold_queue",
|
||||
description="Hold queue",
|
||||
action=ActionType.HOLD,
|
||||
timeout=3600,
|
||||
next_step="agent_connected",
|
||||
),
|
||||
CallFlowStep(
|
||||
id="agent_connected",
|
||||
description="Agent connected",
|
||||
action=ActionType.TRANSFER,
|
||||
action_value="sip_phone",
|
||||
),
|
||||
],
|
||||
tags=["bank", "personal"],
|
||||
avg_hold_time=2100,
|
||||
success_rate=0.92,
|
||||
)
|
||||
|
||||
def test_step_count(self, sample_flow):
|
||||
assert len(sample_flow.steps) == 5
|
||||
|
||||
def test_get_step(self, sample_flow):
|
||||
step = sample_flow.get_step("hold_queue")
|
||||
assert step is not None
|
||||
assert step.action == ActionType.HOLD
|
||||
assert step.timeout == 3600
|
||||
|
||||
def test_get_step_not_found(self, sample_flow):
|
||||
assert sample_flow.get_step("nonexistent") is None
|
||||
|
||||
def test_first_step(self, sample_flow):
|
||||
first = sample_flow.first_step()
|
||||
assert first is not None
|
||||
assert first.id == "greeting"
|
||||
|
||||
def test_steps_by_id(self, sample_flow):
|
||||
steps = sample_flow.steps_by_id()
|
||||
assert len(steps) == 5
|
||||
assert "greeting" in steps
|
||||
assert "agent_connected" in steps
|
||||
assert steps["agent_connected"].action == ActionType.TRANSFER
|
||||
|
||||
def test_serialization_roundtrip(self, sample_flow):
|
||||
"""Test JSON serialization and deserialization."""
|
||||
json_str = sample_flow.model_dump_json()
|
||||
restored = CallFlow.model_validate_json(json_str)
|
||||
assert restored.id == sample_flow.id
|
||||
assert len(restored.steps) == len(sample_flow.steps)
|
||||
assert restored.steps[0].id == "greeting"
|
||||
assert restored.avg_hold_time == 2100
|
||||
|
||||
|
||||
class TestCallFlowCreate:
|
||||
"""Test call flow creation model."""
|
||||
|
||||
def test_minimal_create(self):
|
||||
create = CallFlowCreate(
|
||||
name="My Bank",
|
||||
phone_number="+18005551234",
|
||||
steps=[
|
||||
CallFlowStep(
|
||||
id="start",
|
||||
description="Start",
|
||||
action=ActionType.HOLD,
|
||||
next_step="end",
|
||||
),
|
||||
],
|
||||
)
|
||||
assert create.name == "My Bank"
|
||||
assert len(create.steps) == 1
|
||||
assert create.tags == []
|
||||
assert create.notes is None
|
||||
|
||||
|
||||
class TestCallFlowSummary:
|
||||
"""Test lightweight summary model."""
|
||||
|
||||
def test_summary(self):
|
||||
summary = CallFlowSummary(
|
||||
id="chase-bank-main",
|
||||
name="Chase Bank - Main",
|
||||
phone_number="+18005551234",
|
||||
step_count=6,
|
||||
avg_hold_time=2100,
|
||||
success_rate=0.92,
|
||||
times_used=15,
|
||||
tags=["bank"],
|
||||
)
|
||||
assert summary.step_count == 6
|
||||
assert summary.success_rate == 0.92
|
||||
Reference in New Issue
Block a user