PipeCast Bridge — Webhook Call Transfer & Grand Estancia Bots¶
Status: Planning
Date: 2026-04-02
Feature Name: PipeCast Bridge (Pipecat + AstraPBX Call Transfer Bridge)
Problem¶
- Secrets management doesn't scale. Managing 20+ client orgs with hardcoded API keys in pipecat bots means redeployment for every credential change.
- Functions are dumb.
flow_converter.pycreates handlers that only transition nodes — no value mapping, no state, no logic. Any real behavior requires Python code, defeating the flow editor's purpose. - Disconnected systems. pipecat-flow (SQLite) and AstraPBX (MySQL/Sequelize) have separate databases. Org credentials must be duplicated.
Goal: Build and deploy bots entirely from the flow editor — functions, webhooks, value mappings, secrets — with zero Python code per client.
Architecture Decision: Shared DB, Same VPS¶
Decision: Deploy pipecat-flow on the same VPS as AstraPBX (89.116.31.109). Migrate pipecat-flow from SQLite to MySQL, sharing AstraPBX's MySQL instance.
┌─────────────────────────────────────────────────────┐
│ Cloud VPS (89.116.31.109) │
│ │
│ ┌──────────┐ ┌──────────────┐ ┌────────────┐ │
│ │ AstraPBX │ │ pipecat-flow │ │ Asterisk │ │
│ │ :3000 │ │ :7860 │ │ AMI/ARI │ │
│ └────┬─────┘ └──────┬───────┘ └─────────────┘ │
│ │ │ │
│ └──────┬─────────┘ │
│ ▼ │
│ ┌────────────────┐ │
│ │ MySQL (shared) │ │
│ │ pbx_multitenant│ │
│ │ │ │
│ │ organizations │ ← AstraPBX owns │
│ │ api_keys │ │
│ │ ... │ │
│ │ │ │
│ │ pipecat_bots │ ← pipecat-flow owns │
│ │ pipecat_keys │ │
│ └────────────────┘ │
│ │
│ ┌─────────────────┐ │
│ │ flow-editor │ ← Static site / Cloudflare │
│ │ (reads orgs via │ Pages or same VPS │
│ │ pipecat API) │ │
│ └─────────────────┘ │
└──────────────────────────────────────────────────────┘
External:
┌──────────────┐
│ LogsUpdate │ ← GCP Cloud Run (events.astradial.com)
│ /bot-bridge │ JWT proxy + ticket creation
└──────────────┘
Why same VPS + shared MySQL: - Single source of truth — pipecat reads organizations table directly for api_key, api_secret, context_prefix. No sync, no duplication. - Flow editor can list orgs — admin API returns org names/IDs from same DB. When creating a bot, you pick the org. Bot↔DID↔Queue assignment all in one place. - Simpler ops — one server to SSH into, one DB to backup, one deploy pipeline. - Secure — DB not exposed over network. Both apps connect via localhost:3306. - Performance is NOT a concern — pipecat-flow is a lightweight WebSocket relay + state machine. All heavy compute (VAD, STT, TTS, LLM) runs on Gemini Live cloud. The VPS just shuffles PCM audio bytes and manages flow state — minimal CPU/RAM overhead on top of existing Asterisk + AstraPBX. - Call transfers via localhost — pipecat calls AstraPBX at localhost:3000 directly. No network hop, no JWT token dance, no secrets management. Internal auth only.
What stays external: LogsUpdate on GCP Cloud Run (events.astradial.com) — used only for Firestore operations (ticket creation, CRM logging). Not needed for call transfers.
Solution Architecture¶
Call Transfer Flow (direct localhost — Option A)¶
Pipecat Bot (webhook post_action)
↓ POST localhost:3000/api/v1/calls/{channel_id}/transfer
↓ Internal auth (shared secret or localhost whitelist)
AstraPBX API (same VPS)
↓ AMI Redirect
Asterisk → routes call to queue/extension/DID
No LogsUpdate proxy, no JWT, no secrets in flow JSON. Just a localhost HTTP call.
Ticket Creation Flow (via LogsUpdate)¶
Pipecat Bot (webhook post_action)
↓ POST events.astradial.com/bot-bridge {action: "create_ticket", ...}
LogsUpdate (GCP Cloud Run)
↓ Firestore write
Ticket created in /astrapbx/{org_id}/tickets/
Component Design¶
1. Enhanced Function System (flow_converter.py + flow editor)¶
The core change: Functions defined in the flow editor should be able to do everything — not just transition nodes. Add two new capabilities to the FlowFunction schema:
a) state_mappings — Set state values when function is called¶
{
"name": "select_department",
"description": "Route caller to the right department",
"properties": {
"department": {
"type": "string",
"enum": ["front_desk", "housekeeping", "restaurant", "spa", "maintenance", "billing"]
}
},
"state_mappings": {
"selected_department": "{args.department}",
"department_queue": "{value_map.department_queues.{args.department}}"
},
"next_node_id": "confirm_transfer"
}
state_mappings lets the editor define what gets saved to flow_manager.state when the LLM calls a function. Template variables: - {args.xxx} — LLM-provided argument value - {value_map.xxx.yyy} — Lookup from a value map (see below) - {call.channel_id} — From call_metadata
b) value_maps — Key-value lookups defined at flow level¶
Added to the flow JSON root (alongside meta, nodes, edges):
{
"meta": { "name": "Grand Estancia IVR" },
"value_maps": {
"department_queues": {
"front_desk": "5001",
"housekeeping": "5002",
"restaurant": "5003",
"spa": "5004",
"maintenance": "5005",
"billing": "5006"
}
},
"nodes": [...]
}
Why: This replaces hardcoded Python dicts. The IVR bot maps department → queue number entirely in JSON. The flow editor provides a UI to edit these maps. Different clients get different maps without code changes.
c) flow_converter.py handler upgrade¶
Current handler (dumb):
New handler (smart):
async def handler(args: FlowArgs, flow_manager):
# 1. Always store raw args in state
flow_manager.state.update(args)
# 2. Apply state_mappings with template resolution
for key, template in state_mappings.items():
flow_manager.state[key] = resolve_template(
template, args=args,
value_maps=value_maps,
call=flow_manager.call_metadata
)
# 3. Transition to next node
return args, build_node_config(target_node_id)
2. Webhook Action Type (actions.py)¶
New built-in action "webhook". Template variables in body resolved from flow_manager.call_metadata and flow_manager.state.
Transfer (direct localhost):
{
"type": "webhook",
"url": "http://localhost:3000/api/v1/calls/{call.channel_id}/transfer",
"auth": "internal",
"body": {
"destination": "{state.department_queue}",
"destination_type": "queue"
}
}
Ticket creation (via LogsUpdate):
{
"type": "webhook",
"url": "https://events.astradial.com/bot-bridge",
"body": {
"action": "create_ticket",
"org_id": "{call.org_id}",
"caller_number": "{call.endpoint}",
"category": "{state.category}",
"summary": "{state.summary}",
"guest_name": "{state.guest_name}",
"room_number": "{state.room_number}"
}
}
Auth modes: - "auth": "internal" — adds internal shared secret header (X-Internal-Key) for localhost AstraPBX calls. Key stored in env var, not in flow JSON. - No auth flag — plain POST (for LogsUpdate or external webhooks) - No API keys ever appear in flow JSON.
3. Channel ID Plumbing (pipeline.py)¶
AstraPBX already sends channel_id, org_id, endpoint via WebSocket customParameters (ariClient.js:311-316). Extract and attach to flow_manager.call_metadata.
4. Database Migration (pipecat-flow: SQLite → MySQL)¶
Migrate pipecat-flow gateway from SQLite (aiosqlite) to MySQL (aiomysql or databases). Tables:
pipecat_bots— bot definitions (flow JSON, model config)pipecat_api_keys— gateway API keys for WebSocket auth
Reads from AstraPBX's existing tables: - organizations — org_id, api_key, api_secret, context_prefix, name
5. Bot Bridge Endpoint (LogsUpdate)¶
POST /bot-bridge with two actions: - pbx_forward — JWT caching + proxy to AstraPBX (refactored from /whatsapp-bridge) - create_ticket — Firestore ticket creation
6. Transfer Endpoint Enhancement (AstraPBX)¶
Add destination_type parameter to transfer endpoint. Auto-resolves context prefix: - "extension" → ${org.context_prefix}_internal - "queue" → ${org.context_prefix}_queue - "external" → ${org.context_prefix}_outbound
7. Flow Editor Enhancements¶
a) Webhook action type — New option in action dropdown with URL, body, auth fields.
b) State Mappings UI — In the function inspector, below properties/required, add a "State Mappings" section. Key-value pairs where values support template syntax.
c) Value Maps UI — Flow-level panel (alongside meta/settings) to define named key-value lookup tables.
d) Org Selector — When the editor is connected to the gateway API, show org dropdown for bot assignment. The auth: "org" webhook flag resolves to whichever org the bot is assigned to.
8. Grand Estancia Bots¶
Both defined entirely as JSON flows in the editor. No Python code.
IVR Bot — Department Router¶
Flow: welcome → select_department (value_map lookup) → confirm_transfer (webhook post_action) → end
Runtime example — caller says "I want to book a table for dinner":
1. Call arrives → AudioSocket → pipecat
flow_manager.call_metadata = {channel_id: "1712345678.123", org_id: "org_abc"}
flow_manager.state = {}
2. Welcome node loads → Gemini gets system prompt + function schema
3. Caller says: "I want to book a table for dinner"
4. Gemini calls: select_department(department: "restaurant")
5. Handler runs (flow_converter.py):
├─ flow_manager.state.update({"department": "restaurant"}) # raw args
├─ state_mappings resolve:
│ "selected_dept" → "{args.department}" → "restaurant"
│ "queue_number" → "{value_map.department_queues.{args.department}}" → "5003"
└─ flow_manager.state = {department: "restaurant", selected_dept: "restaurant", queue_number: "5003"}
→ Transitions to confirm_transfer node
6. Gemini says: "I'll connect you to our restaurant right away."
7. Post-actions fire:
├─ webhook: POST http://localhost:3000/api/v1/calls/1712345678.123/transfer
│ Header: X-Internal-Key: <from env>
│ Body: {"destination": "5003", "destination_type": "queue"}
│ → AstraPBX does AMI Redirect → Asterisk routes to queue 5003
└─ end_conversation: call handed off, bot exits
Full flow JSON:
{
"meta": { "name": "Grand Estancia IVR", "version": "0.1.0" },
"value_maps": {
"department_queues": {
"restaurant": "5003",
"housekeeping": "5002",
"reception": "5001"
}
},
"nodes": [
{
"id": "welcome",
"type": "initial",
"position": { "x": 0, "y": 0 },
"data": {
"label": "Welcome",
"role_messages": [
{
"role": "system",
"content": "You are the Grand Estancia hotel receptionist. Listen to what the caller needs and route them to the right department: restaurant, housekeeping, or reception. Be warm and professional."
}
],
"functions": [
{
"name": "select_department",
"description": "Route the caller to the appropriate department",
"properties": {
"department": {
"type": "string",
"enum": ["restaurant", "housekeeping", "reception"],
"description": "The department the caller wants"
}
},
"required": ["department"],
"state_mappings": {
"selected_dept": "{args.department}",
"queue_number": "{value_map.department_queues.{args.department}}"
},
"next_node_id": "confirm_transfer"
}
]
}
},
{
"id": "confirm_transfer",
"type": "end",
"position": { "x": 0, "y": 300 },
"data": {
"label": "Transfer Call",
"task_messages": [
{
"role": "system",
"content": "Tell the caller you are connecting them to their requested department now. Be brief and polite."
}
],
"post_actions": [
{
"type": "webhook",
"url": "http://localhost:3000/api/v1/calls/{call.channel_id}/transfer",
"auth": "internal",
"body": {
"destination": "{state.queue_number}",
"destination_type": "queue"
}
},
{
"type": "end_conversation"
}
]
}
}
],
"edges": [
{ "id": "e1", "source": "welcome", "target": "confirm_transfer", "label": "select_department" }
]
}
Adding a new client with different departments? Just create a new flow with a different value_map and enum. Zero Python code. Zero redeployment.
Ticketing Bot — Missed Call Handler¶
Flow: welcome → identify_issue → FAQ path (end) OR collect_details → submit_ticket → confirmation → end
Triggered when: Queue timeout (no agent picks up). Configured as queue failover destination in AstraPBX dialplan.
Repos & File Changes¶
| Repo | File | Change |
|---|---|---|
| pipecat-flow | /gateway/pipeline.py | Extract call_metadata, attach to flow_manager |
| pipecat-flow | /gateway/database.py | SQLite → MySQL migration, read from organizations table |
| pipecat-flow | /gateway/flow_converter.py | Smart handlers: state_mappings, value_map resolution, template engine |
| pipecat-flow | /src/pipecat_flows/actions.py | New webhook action handler with template substitution |
| pipecat-flow | /gateway/router_admin.py | Org listing (reads from shared MySQL), bot CRUD |
| LogsUpdate | /server.py | /bot-bridge endpoint, extract shared forward_to_pbx() util |
| LogsUpdate | /firebase.py | create_ticket() method |
| AstraPBX | /src/routes/calls.js | destination_type on transfer endpoint |
| flow-editor | lib/schema/flow.schema.ts | state_mappings on FlowFunction, value_maps on FlowSchema, webhook Action fields |
| flow-editor | components/inspector/forms/FunctionItem.tsx | State mappings UI |
| flow-editor | components/inspector/forms/ActionItem.tsx | Webhook action fields |
| flow-editor | New: value maps panel | Flow-level value map editor |
Implementation Order¶
- DB migration — pipecat-flow SQLite → MySQL (shared with AstraPBX)
- Channel ID plumbing — pipeline.py call_metadata extraction
- Template engine — shared
resolve_template()for state_mappings and webhook bodies - Enhanced flow_converter — state_mappings, value_map resolution in function handlers
- Webhook action type — actions.py with template substitution and
auth: "org" - Bot bridge endpoint — LogsUpdate
/bot-bridge - Transfer endpoint — AstraPBX destination_type support
- Flow editor schema — state_mappings, value_maps, webhook action fields
- Flow editor UI — function state mappings, value maps panel, webhook action form
- Grand Estancia IVR bot — JSON flow
- Grand Estancia Ticketing bot — JSON flow
Test Cases¶
TC-1xx: Database & Shared Infrastructure¶
| ID | Test Case | Input | Expected Result | Status |
|---|---|---|---|---|
| TC-101 | pipecat connects to MySQL | Start pipecat-flow gateway | Connects to pbx_multitenant MySQL on localhost:3306 | ⬜ |
| TC-102 | pipecat reads organizations table | GET /admin/orgs | Returns orgs from AstraPBX's organizations table | ⬜ |
| TC-103 | pipecat bot CRUD in MySQL | Create/read/update/delete bot | pipecat_bots table operations work | ⬜ |
| TC-104 | AstraPBX unaffected by pipecat tables | Run AstraPBX normally | No interference from pipecat_* tables | ⬜ |
| TC-105 | Org credentials accessible | Query org by ID | api_key, api_secret readable (secret is bcrypt hash) | ⬜ |
TC-2xx: Channel ID Plumbing¶
| ID | Test Case | Input | Expected Result | Status |
|---|---|---|---|---|
| TC-201 | AstraPBX call delivers channel_id | AudioSocket with customParameters: {channel_id: "1712345678.123", org_id: "org_abc", provider: "astrapbx"} | flow_manager.call_metadata has channel_id, org_id, endpoint | ⬜ |
| TC-202 | Twilio call — no crash | Incoming call via Twilio (no customParameters) | flow_manager.call_metadata is {}, bot starts normally | ⬜ |
| TC-203 | Missing channel_id | AstraPBX call without channel_id in params | call_metadata["channel_id"] is "", no crash | ⬜ |
| TC-204 | State dict initialized | Any call connects | flow_manager.state is {} at start | ⬜ |
TC-3xx: Template Engine¶
| ID | Test Case | Input | Expected Result | Status |
|---|---|---|---|---|
| TC-301 | {args.xxx} substitution | Template: {args.department}, args: {department: "spa"} | Result: "spa" | ⬜ |
| TC-302 | {state.xxx} substitution | Template: {state.queue}, state: {queue: "5001"} | Result: "5001" | ⬜ |
| TC-303 | {call.xxx} substitution | Template: {call.channel_id}, metadata: {channel_id: "123.456"} | Result: "123.456" | ⬜ |
| TC-304 | {value_map.xxx.yyy} lookup | Template: {value_map.depts.spa}, maps: {depts: {spa: "5004"}} | Result: "5004" | ⬜ |
| TC-305 | Dynamic value_map key | Template: {value_map.depts.{args.department}}, args: {department: "spa"} | Result: "5004" | ⬜ |
| TC-306 | Nested dict substitution | Body: {action: "x", body: {dest: "{state.queue}"}} | Nested values substituted | ⬜ |
| TC-307 | Missing variable — graceful | Template: {state.nonexistent} | Returns "" or None, no crash | ⬜ |
| TC-308 | Missing value_map key | Template: {value_map.depts.unknown} | Returns "" or None, no crash | ⬜ |
TC-4xx: Enhanced Function Handlers (flow_converter)¶
| ID | Test Case | Input | Expected Result | Status |
|---|---|---|---|---|
| TC-401 | Raw args stored in state | LLM calls select_department(department: "spa") | flow_manager.state includes {department: "spa"} | ⬜ |
| TC-402 | state_mappings applied | Mapping: {queue: "{value_map.depts.{args.department}}"} | flow_manager.state.queue == "5004" | ⬜ |
| TC-403 | State accumulates across nodes | Node 1 sets {category: "maintenance"}, Node 2 sets {name: "John"} | State has both | ⬜ |
| TC-404 | State overwrites on conflict | Node 1: {priority: "low"}, Node 2: {priority: "high"} | state.priority == "high" | ⬜ |
| TC-405 | Function without state_mappings | Function with only next_node_id, no state_mappings | Raw args saved, no error | ⬜ |
| TC-406 | value_maps loaded from flow JSON | Flow JSON has value_maps: {depts: {...}} | Accessible in handler template resolution | ⬜ |
| TC-407 | Decision routing still works | Function with decision object | Conditional routing unaffected by state_mappings | ⬜ |
TC-5xx: Webhook Action Type¶
| ID | Test Case | Input | Expected Result | Status |
|---|---|---|---|---|
| TC-501 | Basic webhook fires | {type: "webhook", url: "https://httpbin.org/post", body: {test: true}} | HTTP POST sent, 200 logged | ⬜ |
| TC-502 | Template substitution in body | Body: {dest: "{state.queue}"} with state {queue: "5001"} | Sent: {dest: "5001"} | ⬜ |
| TC-503 | auth: "org" injects credentials | Action has auth: "org", org has api_key: "org_xxx" | api_key and api_secret injected into request body | ⬜ |
| TC-504 | auth: "org" reads from MySQL | Org ID from call_metadata → query organizations table | Correct creds fetched from shared DB | ⬜ |
| TC-505 | auth: "org" — org not found | call_metadata has unknown org_id | Error logged, webhook skipped, bot continues | ⬜ |
| TC-506 | Webhook timeout (10s) | URL never responds | Timeout logged, bot continues | ⬜ |
| TC-507 | Webhook HTTP error | URL returns 500 | Error logged with status, bot continues | ⬜ |
| TC-508 | Webhook network error | Unreachable hostname | Connection error logged, bot continues | ⬜ |
| TC-509 | Webhook as pre_action | In pre_actions array | Fires before node LLM response | ⬜ |
| TC-510 | Webhook as post_action | In post_actions array | Fires after node transition | ⬜ |
| TC-511 | Multiple webhooks in sequence | Two webhooks in post_actions | Both fire in order | ⬜ |
| TC-512 | Ongoing actions count | During action sequence | _ongoing_actions_count correct | ⬜ |
TC-6xx: Bot Bridge Endpoint (LogsUpdate)¶
| ID | Test Case | Input | Expected Result | Status |
|---|---|---|---|---|
| TC-601 | PBX forward — happy path | POST /bot-bridge {action: "pbx_forward", api_key, api_secret, endpoint, body} | JWT fetched, forwarded to AstraPBX, response returned | ⬜ |
| TC-602 | JWT cached | Two requests, same api_key, within 23h | Only one login call to AstraPBX | ⬜ |
| TC-603 | JWT expired (401 retry) | Cached token expired | Auto-refresh, retry, success | ⬜ |
| TC-604 | Invalid credentials | Wrong api_key/api_secret | 401 returned, logged | ⬜ |
| TC-605 | AstraPBX down | Unreachable | 502/503 returned, logged | ⬜ |
| TC-606 | Create ticket — happy path | {action: "create_ticket", org_id, caller_number, category, summary} | Ticket in Firestore, ticket_id returned | ⬜ |
| TC-607 | Create ticket — missing fields | Missing category or summary | 400 with validation error | ⬜ |
| TC-608 | Create ticket — timestamp | Any creation | created_at in IST, status: "open" | ⬜ |
| TC-609 | Unknown action | {action: "unknown"} | 400: "Unknown action" | ⬜ |
| TC-610 | Shared util with whatsapp-bridge | Same org on both endpoints | Same JWT cache, same forward_to_pbx() | ⬜ |
TC-7xx: Transfer Endpoint Enhancement (AstraPBX)¶
| ID | Test Case | Input | Expected Result | Status |
|---|---|---|---|---|
| TC-701 | destination_type=queue | {destination: "5001", destination_type: "queue"} | AMI Redirect to ${prefix}_queue | ⬜ |
| TC-702 | destination_type=extension | {destination: "101", destination_type: "extension"} | AMI Redirect to ${prefix}_internal | ⬜ |
| TC-703 | destination_type=external | {destination: "9876543210", destination_type: "external"} | AMI Redirect to ${prefix}_outbound | ⬜ |
| TC-704 | No destination_type (backward compat) | {destination: "101"} | Existing behavior unchanged | ⬜ |
| TC-705 | Invalid channel_id | /calls/nonexistent/transfer | 404 | ⬜ |
| TC-706 | Invalid destination_type | {destination_type: "invalid"} | 400 | ⬜ |
TC-8xx: Flow Editor — Schema & UI¶
| ID | Test Case | Input | Expected Result | Status |
|---|---|---|---|---|
| TC-801 | Webhook in action dropdown | Open action type selector | "Webhook" option visible | ⬜ |
| TC-802 | Webhook fields shown | Select webhook type | URL, body textarea, auth checkbox appear | ⬜ |
| TC-803 | Default URL pre-filled | New webhook action | https://events.astradial.com/bot-bridge | ⬜ |
| TC-804 | state_mappings UI on function | Open function inspector | "State Mappings" section with key-value pairs | ⬜ |
| TC-805 | state_mappings template autocomplete | Type { in value field | Suggests args., state., call., value_map. | ⬜ |
| TC-806 | Value Maps panel | Open flow settings | Named value map editor (key-value table) | ⬜ |
| TC-807 | Export includes all new fields | Export JSON | Contains value_maps, state_mappings, webhook actions | ⬜ |
| TC-808 | Import flow with new fields | Load JSON with value_maps + state_mappings | Editor displays correctly | ⬜ |
| TC-809 | Invalid JSON in webhook body | Malformed JSON in body textarea | Validation error shown | ⬜ |
| TC-810 | Org selector for bot | Create bot in editor | Dropdown lists orgs from gateway API | ⬜ |
TC-9xx: Grand Estancia IVR Bot¶
| ID | Test Case | Input | Expected Result | Status |
|---|---|---|---|---|
| TC-901 | Welcome message | Call connects | "Welcome to Grand Estancia..." | ⬜ |
| TC-902 | Direct department request | "I want housekeeping" | select_department(department: "housekeeping") | ⬜ |
| TC-903 | Indirect department detection | "My AC isn't working" | select_department(department: "maintenance") | ⬜ |
| TC-904 | Ambiguous request | "I need help" | Bot asks clarifying question | ⬜ |
| TC-905 | Value map resolves queue | department=spa | state.department_queue == "5004" (from value_map) | ⬜ |
| TC-906 | Transfer webhook fires | confirm_transfer node | POST to /bot-bridge with correct queue | ⬜ |
| TC-907 | All departments route | Test all 6 departments | Each maps to correct queue via value_map | ⬜ |
| TC-908 | Bot ends after transfer | Webhook sent | end_conversation fires | ⬜ |
| TC-909 | Flow JSON loads | Register bot via admin API | Bot loads, welcome node created | ⬜ |
TC-10xx: Grand Estancia Ticketing Bot¶
| ID | Test Case | Input | Expected Result | Status |
|---|---|---|---|---|
| TC-1001 | Welcome on timeout | Queue timeout → ticketing bot | "Sorry we missed your call..." | ⬜ |
| TC-1002 | FAQ — check-in time | "What time is check-in?" | Answers from FAQ, ends call | ⬜ |
| TC-1003 | FAQ — WiFi | "What's the WiFi?" | Answers from FAQ | ⬜ |
| TC-1004 | Non-FAQ → ticket path | "My shower is leaking" | is_faq: false, → collect_details | ⬜ |
| TC-1005 | Detail collection | "John, room 205" | State: {guest_name: "John", room_number: "205"} | ⬜ |
| TC-1006 | Ticket webhook fires | submit_ticket → ticket_confirmed | POST /bot-bridge with create_ticket + state | ⬜ |
| TC-1007 | Ticket in Firestore | After TC-1006 | Document at /astrapbx/{org_id}/tickets/{id} | ⬜ |
| TC-1008 | Confirmation message | Ticket created | "Your ticket has been created..." | ⬜ |
| TC-1009 | Bot ends after ticket | Confirmed | end_conversation fires | ⬜ |
| TC-1010 | Full state accumulation | Entire flow | State has category, summary, guest_name, room_number, details | ⬜ |
TC-E2E: End-to-End Integration¶
| ID | Test Case | Input | Expected Result | Status |
|---|---|---|---|---|
| TC-E01 | Full IVR transfer | PSTN → AstraPBX → AudioSocket → IVR bot → "front desk" → transfer | Call lands on queue, agent picks up | ⬜ |
| TC-E02 | Full ticketing | PSTN → queue timeout → ticketing bot → describe issue → ticket | Ticket in Firestore, call ends | ⬜ |
| TC-E03 | No secrets leak | Capture pipecat outbound traffic | No api_key/secret in webhook body (only in LogsUpdate→AstraPBX) | ⬜ |
| TC-E04 | Multi-org isolation | Two orgs, same bot flow | Each uses own creds, correct context routing | ⬜ |
| TC-E05 | Webhook failure recovery | AstraPBX down during transfer | Bot says error, ends gracefully | ⬜ |
| TC-E06 | Concurrent calls | 3 simultaneous calls | No cross-contamination of state/metadata | ⬜ |
| TC-E07 | Editor → deploy → call | Create flow in editor → export → register bot → make call | Full round-trip works with no code | ⬜ |
Total: 84 test cases
Bugs / Fixes Log¶
Track issues found during implementation here.
Context Notes¶
- AstraPBX DB: MySQL (
pbx_multitenant), Sequelize ORM,DB_HOST=localhost,DB_PORT=3306 - Auth decision: Option A (direct localhost call). For call transfers, pipecat calls AstraPBX API directly on
localhost:3000— no JWT, no LogsUpdate proxy. Internal auth via shared secret or localhost whitelist. LogsUpdate/bot-bridgeused only for Firestore operations (ticket creation). channel_idcomes from ARIchannel.id, format like1712345678.123channel.name(endpoint) format likePJSIP/101-00000001- JWT tokens cached 23h in LogsUpdate (24h expiry from AstraPBX)
- AudioSocket: raw 16-bit signed linear PCM at 8kHz
- Flow converter passes pre_actions/post_actions through as-is from JSON
- AstraPBX context prefix:
org_{timestamp}_{type}(e.g.,org_m1abc_internal) - Decision routing in editor uses
action(Python code) +conditions— this still works alongside state_mappings