Stage 6: learn_call_flow rebuilt on the learner, docs truth sweep

The call-flow learner finally gets fed: exploration mode records its
IVR discoveries on the call (ActiveCall.exploration_steps) instead of
throwing them away, persistence stores them in the call record's
metadata, and the rebuilt learn_call_flow MCP tool turns a completed
exploration call into a stored flow via CallFlowLearner — correct
constructor (llm_client from get_llm, heuristic labels when the LLM
is unavailable), build for a new number, merge/refine when a flow
already exists. save_learned_flow/update_flow_from_model keep the
CallFlow↔row mapping in call_persistence.

Test gaps closed: tests/test_learner.py (discoveries→linked steps,
exploration persistence, learn-then-refine through the in-memory MCP
client, no-data and unknown-call answers) and tests/test_websocket.py
(4401 without token, trunk-status-then-replay on connect, per-call
stream filtering).

Docs aligned to code: README (15 tools incl. learn_call_flow, HTTP
not SSE, Python 3.12+, PostgreSQL+Alembic — no SQLite fallback, media
pipeline marked stub-mode until pjsua2 installed, Alembic and honest
/health checked off); docs/mcp-server.md rewritten against the actual
tool surface (hangup not end_call, real params, 3 real resources,
/mcp/ streamable HTTP + bearer auth); architecture/development/
configuration drift fixed.

pyproject: pruned never-imported deps (websockets, librosa,
soundfile, python-multipart).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-10 13:45:35 -04:00
parent f7a11f2f20
commit ff7ea8623a
13 changed files with 444 additions and 107 deletions

View File

@@ -10,7 +10,7 @@ Hold Slayer is a single-process async Python application built on FastAPI. It ac
│ │
│ ┌──────────┐ ┌──────────┐ ┌───────────┐ ┌──────────────┐ │
│ │ REST API │ │WebSocket │ │MCP Server │ │ Dashboard │ │
│ │ /api/* │ │ /ws/* │ │ (SSE) │ │ /dashboard │ │
│ │ /api/* │ │ /ws/* │ │ (HTTP) │ │ /dashboard │ │
│ └────┬─────┘ └────┬─────┘ └─────┬─────┘ └──────────────┘ │
│ │ │ │ │
│ ┌────┴──────────────┴──────────────┴────┐ │
@@ -46,7 +46,7 @@ Hold Slayer is a single-process async Python application built on FastAPI. It ac
|-----------|------|----------|---------|
| REST API | `api/calls.py`, `api/call_flows.py`, `api/devices.py` | HTTP | Call management, CRUD, configuration |
| WebSocket | `api/websocket.py` | WS | Real-time event streaming to clients |
| MCP Server | `mcp_server/server.py` | SSE | AI assistant tool integration |
| MCP Server | `mcp_server/server.py` | Streamable HTTP at `/mcp/` | AI assistant tool integration |
### Orchestration Layer
@@ -75,7 +75,7 @@ Hold Slayer is a single-process async Python application built on FastAPI. It ac
| Recording | `services/recording.py` | WAV file management and storage |
| Analytics | `services/call_analytics.py` | Call metrics, hold time stats, trends |
| Notifications | `services/notification.py` | WebSocket + SMS alerts |
| Database | `db/database.py` | SQLAlchemy async (PostgreSQL or SQLite) |
| Database | `db/database.py` | SQLAlchemy async (PostgreSQL, Alembic migrations) |
## Data Flow — Hold Slayer Call

View File

@@ -126,7 +126,7 @@ uvicorn main:app --host 0.0.0.0 --port 8000 --reload
### Production
```bash
# Use PostgreSQL instead of SQLite
# PostgreSQL is required (no SQLite fallback)
DATABASE_URL=postgresql+asyncpg://user:pass@localhost/hold_slayer
# Use vLLM for faster inference

View File

@@ -4,7 +4,7 @@
### Prerequisites
- Python 3.13+
- Python 3.12+
- Ollama (or any OpenAI-compatible LLM) — for IVR menu analysis
- Speaches or Whisper API — for speech-to-text (optional for dev)
- A SIP trunk account — for making real calls (optional for dev)

View File

@@ -1,10 +1,20 @@
# MCP Server
The MCP (Model Context Protocol) server lets any MCP-compatible AI assistant control the Hold Slayer gateway. Built with [FastMCP](https://github.com/jlowin/fastmcp), it exposes tools and resources over SSE.
The MCP (Model Context Protocol) server lets any MCP-compatible AI assistant
control the Hold Slayer gateway. Built with [FastMCP](https://github.com/jlowin/fastmcp),
it is mounted on the FastAPI app at **`/mcp/`** (trailing slash) over
**streamable HTTP** and authenticates with the same static bearer token as the
REST API and WebSocket.
## Overview
An AI assistant connects via SSE to the MCP server and gains access to tools for placing calls, checking status, sending DTMF, getting transcripts, and managing call flows. The assistant can orchestrate an entire call through natural language.
An AI assistant connects to the MCP endpoint and gains access to 15 tools and
3 resources for placing calls, checking status, sending DTMF, getting
transcripts, and managing call flows. The assistant can orchestrate an entire
call through natural language.
`make_call` places a **real PSTN call** that may incur charges; emergency
numbers are always refused, and the concurrent-call cap applies.
## Tools
@@ -15,13 +25,32 @@ Place an outbound call through the SIP trunk.
| Param | Type | Required | Description |
|-------|------|----------|-------------|
| `number` | string | Yes | Phone number to call (E.164 format) |
| `mode` | string | No | Call mode: `direct`, `hold_slayer`, `ai_assisted` (default: `hold_slayer`) |
| `mode` | string | No | `direct`, `hold_slayer`, or `ai_assisted` (default: `direct`) |
| `intent` | string | No | What you want to accomplish on the call |
| `call_flow_id` | string | No | ID of a stored call flow to follow |
| `device` | string | No | Device to transfer to when a human is detected |
Returns: Call ID and initial status.
Returns: call ID and initial status.
### end_call
### get_call_status
Check the current state of a call — status, duration, hold time, current
audio classification, recent transcript.
| Param | Type | Required | Description |
|-------|------|----------|-------------|
| `call_id` | string | Yes | The call to check |
### transfer_call
Transfer an active call to a registered device.
| Param | Type | Required | Description |
|-------|------|----------|-------------|
| `call_id` | string | Yes | The call to transfer |
| `device` | string | Yes | Device ID or type to ring |
### hangup
Hang up an active call.
@@ -29,102 +58,117 @@ Hang up an active call.
|-------|------|----------|-------------|
| `call_id` | string | Yes | The call to hang up |
### send_dtmf
Send touch-tone digits to an active call (for manual IVR navigation).
| Param | Type | Required | Description |
|-------|------|----------|-------------|
| `call_id` | string | Yes | The call to send digits to |
| `digits` | string | Yes | DTMF digits to send (e.g., "1", "3#", "1234") |
### get_call_status
Check the current state of a call.
| Param | Type | Required | Description |
|-------|------|----------|-------------|
| `call_id` | string | Yes | The call to check |
Returns: Status, duration, hold time, audio classification, transcript excerpt.
### get_call_transcript
Get the live transcript of a call.
| Param | Type | Required | Description |
|-------|------|----------|-------------|
| `call_id` | string | Yes | The call to get transcript for |
Returns: Array of transcript chunks with timestamps and speaker labels.
### get_call_recording
Get recording metadata and file path for a call.
| Param | Type | Required | Description |
|-------|------|----------|-------------|
| `call_id` | string | Yes | The call to get recording for |
Returns: Recording path, duration, file size.
### list_active_calls
List all calls currently in progress. No parameters.
Returns: Array of active calls with status, number, duration.
### send_dtmf
Send touch-tone digits on an active call (manual IVR navigation).
| Param | Type | Required | Description |
|-------|------|----------|-------------|
| `call_id` | string | Yes | The call to send digits on |
| `digits` | string | Yes | DTMF digits (e.g., `"1"`, `"123#"`) |
### get_call_transcript
Get the full transcript of an active call.
| Param | Type | Required | Description |
|-------|------|----------|-------------|
| `call_id` | string | Yes | The call to get the transcript for |
### get_call_recording
Get recording metadata (path, duration) for a persisted call.
| Param | Type | Required | Description |
|-------|------|----------|-------------|
| `call_id` | string | Yes | The call to look up |
### get_call_summary
Get analytics summary — hold times, success rates, call volume. No parameters.
Stored summary, action items, and sentiment for a persisted call.
Returns: Aggregate statistics across all calls.
| Param | Type | Required | Description |
|-------|------|----------|-------------|
| `call_id` | string | Yes | The call to look up |
### search_call_history
Search past calls by number, company, or date range.
Search past call records.
| Param | Type | Required | Description |
|-------|------|----------|-------------|
| `query` | string | Yes | Search term (phone number, company name) |
| `limit` | int | No | Max results (default: 20) |
| `phone_number` | string | No | Filter by phone number (partial match) |
| `intent` | string | No | Filter by intent text (partial match) |
| `limit` | int | No | Max results (default: 10) |
### get_call_flow
Look up the stored IVR call flow for a phone number.
| Param | Type | Required | Description |
|-------|------|----------|-------------|
| `phone_number` | string | Yes | Number to look up (E.164) |
### create_call_flow
Store a new IVR call flow by hand.
| Param | Type | Required | Description |
|-------|------|----------|-------------|
| `name` | string | Yes | Human-readable name |
| `phone_number` | string | Yes | Phone number (E.164) |
| `steps_json` | string | Yes | JSON array of call flow steps |
| `notes` | string | No | General notes |
### learn_call_flow
Build a reusable call flow from a completed exploration call.
Build (or refine) a reusable IVR call flow from a completed hold-slayer
exploration call. Exploration calls record every IVR prompt heard and DTMF
sent; this turns those discoveries into a stored flow so the next call
navigates directly.
| Param | Type | Required | Description |
|-------|------|----------|-------------|
| `call_id` | string | Yes | The exploration call to learn from |
| `company` | string | No | Company name for the flow |
| `call_id` | string | Yes | A completed call that ran in exploration mode |
| `company_name` | string | No | Company name for labeling a new flow |
Returns: The generated CallFlow object.
### list_devices
List registered devices and their online/offline status. No parameters.
### gateway_status
Trunk registration, device count, active calls, engine mode. No parameters.
## Resources
MCP resources provide read-only data that assistants can reference:
| Resource URI | Description |
|-------------|-------------|
| `gateway://status` | Current gateway status — trunk registration, active calls, service health |
| `gateway://calls` | List of all active calls with current status |
| `gateway://calls/{call_id}` | Detailed status for a specific call |
| `gateway://flows` | List of all stored call flows |
| `gateway://analytics` | Call analytics summary |
| `gateway://status` | Current gateway status — trunk registration, active calls |
| `gateway://call-flows` | List of all stored call flows |
| `gateway://active-calls` | All active calls with current status |
## Configuration
## Connecting an AI Assistant
The MCP server is mounted on the FastAPI app at `/mcp`.
Claude Code:
### Connecting an AI Assistant
```bash
claude mcp add hold-slayer --transport http http://localhost:8000/mcp/ \
--header "Authorization: Bearer $API_TOKEN"
```
Add to your MCP client configuration (e.g., Claude Desktop, Cline):
Generic MCP client configuration:
```json
{
"mcpServers": {
"hold-slayer": {
"url": "http://localhost:8000/mcp/sse"
"url": "http://localhost:8000/mcp/",
"headers": {"Authorization": "Bearer <API_TOKEN>"}
}
}
}
@@ -132,8 +176,6 @@ Add to your MCP client configuration (e.g., Claude Desktop, Cline):
## Example Conversation
Here is how an AI assistant would use the MCP tools to handle a complete call:
**User:** "Call Chase Bank and dispute the Amazon charge from December 15th"
**Assistant actions:**
@@ -141,15 +183,8 @@ Here is how an AI assistant would use the MCP tools to handle a complete call:
1. Calls `make_call(number="+18005551234", mode="hold_slayer", intent="dispute Amazon charge Dec 15th", call_flow_id="chase-bank-main")`
2. Receives `call_id: "call_abc123"`
3. Polls `get_call_status("call_abc123")` periodically
4. Status progression: `trying``ringing``connected``on_hold`
4. Status progression: `initiating``ringing``connected``on_hold`
5. Tells user: "I'm on hold with Chase Bank. Currently 4 minutes in. I'll let you know when someone picks up."
6. Status changes to `transferring` — human detected!
7. Tells user: "A live agent just picked up. I'm transferring the call to your desk phone now. Pick up!"
8. After the call, calls `learn_call_flow("call_abc123", company="Chase Bank")` to save the IVR path for next time.
**User:** "How long was I on hold?"
**Assistant actions:**
1. Calls `get_call_summary()`
2. Reports: "Your Chase Bank call lasted 12 minutes total, with 8 minutes on hold. The disputes department averages 6 minutes hold time on Tuesdays."
8. After the call, calls `learn_call_flow("call_abc123", company_name="Chase Bank")` to save the IVR path for next time.