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
174 lines
5.4 KiB
Python
174 lines
5.4 KiB
Python
"""
|
|
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
|