Click any plane node below to see what it does and how it connects to the rest of the system.
Blue arrows = Handled automatically by our call routing software.
Red arrows = New logic we need to build (matching a phone number to the right business).
Green arrows = Already exists but needs updating to support multiple businesses.
Purple = The live voice conversation (already working).
What is a PBX?
Think of a PBX as a traffic controller for phone calls — just like a receptionist at a front desk who decides which department to transfer a caller to.
Our client owns a Yeastar P550 — a physical phone routing box in their office. Businesses get phone numbers from the client. When a customer dials one of those numbers, the Yeastar routes the call to our AI voice agent.
What is a SIP Trunk?
A SIP Trunk is the digital phone line that connects two systems so they can exchange calls. Instead of a physical cable, it's a network connection between the client's office phone system and our server.
72.62.70.61, port 15060). The Yeastar is configured to send calls to that address whenever one comes in.
We use a "Peer Trunk" — meaning the PBX just sends calls directly to our server's address. No login required. Trust is based on the IP address of the sender.
What happens when a call starts?
When a customer dials a number, the phone system sends a "start call" request to our server. This request includes everything needed to begin the conversation.
"Start Call" Request (INVITE) = The system saying "someone wants to talk, please pick up"
Unique Call ID = Like a ticket number — every call gets its own ID so the system never confuses two simultaneous calls
Audio Setup Info (SDP) = Technical details like "send the voice audio to this address on this channel"
"OK, I accept" Response (200 OK) = Our server saying "got it, call connected, sending audio back"
"End Call" Signal (BYE) = Either side saying "the conversation is over, hang up"
How does actual voice audio travel?
Once the call is accepted, the live voice audio flows through a separate channel specifically designed for real-time audio streaming.
Each active call needs its own dedicated channel. This is why multiple calls can happen at the same time without the voices mixing together — each gets its own private audio lane.
What does our system actually do with a call?
Here is the good news: most of the technical phone infrastructure is handled for us automatically. Our call routing software (LiveKit) acts as the SIP server. It:
1. Listens for incoming calls on our server
2. Automatically assigns each call a unique tracking ID
3. Creates a private virtual "room" for each call
4. Converts the phone audio format to the format our AI can process
5. Our AI agent joins the room and begins the conversation with the caller
How do multiple businesses share the same system?
This is the core challenge — the same server receives calls for many different businesses. Each call must be handled with the right agent, knowledge base, and customer data for that specific business.
What the client's phone system does (automatic): When a call comes in, the Yeastar sends each call separately with a unique ID. It never confuses two calls — this is handled for us.
What our system must do (the part we're building): We create a separate virtual room for each call. Each room gets its own AI agent instance. Currently, our system only allows one active call per business at a time — this is the limit we need to remove.
The "Single Number" Problem We're Fixing
Right now, our system is configured to only accept calls from one specific test number. This worked for development, but for a real product with multiple business clients:
The problem: If Business A's customer calls +39-055-1234567 and Business B's customer calls +39-055-7654321, both calls arrive at our server. But our agent currently has no way to tell which business the call is for.
The fix: We need to look at which number the customer dialed, match it to the correct business in our database, and load that business's agent, knowledge base, and customer data. This is the main routing problem we are solving.
Where: LiveKit SIP configuration (likely via LiveKit API or config YAML). Check
docs/Yeastar-setup.md for the current config.How: Instead of matching
called_number: "1000", configure a wildcard or
regex match. The dispatch rule should put the called_number and caller_number
into the Room metadata so the agent can read it.Simple example: Think of it like changing a route from
/api/agent/1000 to
/api/agent/:phoneNumber. The number becomes a variable, not a hardcoded value.Reference: LiveKit SIP dispatch rules docs:
https://docs.livekit.io/agents/sip/
called_number from
room metadata, queries sip_numbers table, and gets the
organization_id.Where:
services/supabase_client.py has
lookup_org_by_sip_number() already. Check if it works properly and returns the right data
including any per-number config overrides.Where (agent):
agent.py and voice_agent.py. The agent's
entrypoint function needs to: (1) check if it's a SIP call, (2) extract called_number, (3) call lookup,
(4) use that org_id for everything.Simple example: Like a middleware in Express.js that reads the API key from the request header, looks up the tenant, and attaches
req.tenant for all downstream handlers.
services/session_gate.py currently enforces ONE active session per
organization per GPU. For SIP calls, we need to allow multiple concurrent calls per org (a restaurant
might get 5 calls at once).How: Change the gate from "1 session per org" to "N sessions per org" with a configurable limit. Or better: track per-org concurrent count and reject new calls with a SIP 486 (Busy) when the limit is reached.
Consideration: GPU memory is limited. Each concurrent call needs its own STT + LLM context + TTS. For a single RTX 3090, maybe 3 to 5 concurrent sessions max. This should be configurable per org or per GPU.
agent_instances, (2) check if the sip_numbers row has a
config_override JSONB, (3) merge the override on top of the org config.Where:
services/tenant_config.py. The TenantConfig dataclass
needs a merge method that accepts per-number overrides.Why: Business has number 1001 for orders (greeting: "Ready to take your order") and 1002 for support (greeting: "How can I help?"). Same org, different agent behavior.
sip_numbers table exists with basic columns (phone_number,
organization_id, active).New columns needed:
-
label (text) = Human-friendly name like "Sales Line"-
config_override (jsonb) = Per-number agent config overrides-
assigned_by (uuid, nullable) = Who assigned this number (for audit)-
assigned_at (timestamptz) = When it was assigned-
max_concurrent_calls (int, default 1) = How many simultaneous calls this number can
handleIndex: Add unique index on
phone_number (one number can only belong to one
org at a time).RLS: Update Row Level Security policies so tenants can only see their own numbers.
active_calls table (or use Redis) to track which calls are
currently active per org and per number.Columns:
-
id (uuid)-
session_id (fk to sessions)-
organization_id (fk to organizations)-
sip_number_id (fk to sip_numbers)-
started_at (timestamptz)-
call_id_sip (text) = The SIP Call-ID for debuggingPurpose: Quick COUNT query to check concurrent calls before accepting a new call. Rows are deleted when calls end.
Features:
- Table showing: phone number, label, status (active/inactive), concurrent call limit
- Click a number to edit: label, greeting override, language override, system prompt override
- Show live call count (how many active calls on this number right now)
- Toggle active/inactive
Note: The actual number ASSIGNMENT is done by the client (stakeholder) via an admin panel or API. The business tenant can only VIEW and CONFIGURE their assigned numbers, not create new ones.
Where:
src/app/(dashboard)/settings/ or a new
src/app/(dashboard)/phone-numbers/ route.
- Add new phone numbers to the system
- Assign numbers to organizations
- Reassign numbers between organizations
- See which numbers are in use across all tenants
Who uses this: Only the client/stakeholder, not the business tenants.
Consideration: Could be a separate admin route (e.g.,
/admin/numbers) or
an API-only feature initially.
- Currently active calls (live count) per number
- Call history with caller number, duration, tickets created
- Simple analytics: calls per day, average duration, busiest hours
Where: Enhance existing
src/app/(dashboard)/operations/page.tsx
How: Two approaches:
- Firewall level: iptables/ufw rule on the VPS to DROP any UDP to port 15060 that does not come from the Yeastar's public IP
- LiveKit level: Configure the SIP trunk's
allowed_addresses to only
accept the PBX IPThe client's message explicitly mentions this: "The bot's SIP listener should silently drop any UDP packets on port 15060 that do not originate from the Yeastar PBX's designated IP address."
How: Before the agent joins a Room, check concurrent call count. If over limit, do not join and let LiveKit handle the SIP rejection. Or configure LiveKit dispatch rules with a max participant limit.
Also needed: Ghost session cleanup. If a call drops without a SIP BYE (network failure), detect the silence after 10 to 15 seconds and force-close the session to free the slot.
Current: Ports 10000 to 12000 are open for RTP. If we increase concurrent calls, verify this range is sufficient.
Rule of thumb: Each call uses 2 RTP ports (audio send + receive). With 50 concurrent calls, we need at least 100 ports. 10000 to 12000 gives us 2000 ports, so plenty for now.
Features Currently In Progress
These items are actively being worked on by our development team. Updates will be reflected here as work advances.