Call Service¶
Language: Java 21
Framework: Spring Boot 3.5.5
Source: call-service
Purpose¶
Call Service manages the complete lifecycle of communication calls (voice, video, chat). It handles call creation, participant routing, LiveKit session management, queue management, and real-time agent availability — all within a single multi-tenant microservice.
Responsibilities¶
| Responsibility | Implementation |
|---|---|
| Call creation | CallApi → LiveKitCallService → creates LiveKit room + saves Call entity |
| Participant routing | Redis sorted sets + Lua script → LiveKitCallService.inviteParticipant() |
| Agent availability | AvailabilityService (Redis-backed) + contact-service integration |
| Call state tracking | CallDataRecordService + PostgreSQL |
| Real-time updates | Redis Pub/Sub for availability changes; SSE for live queue |
| Call transfers | TransferCallQueueService + transfer-specific routing |
| LiveKit events | LiveKitWebhookApi processes room/participant webhooks |
| Recordings | LiveKitCallAudioService + LiveKit egress API → S3 |
| Multi-tenancy | TenantContextHolder + tenant-scoped Redis keys + Flyway schema separation |
AI Agent ID Resolution¶
Call Service resolves agent_id through two paths:
1. Explicit API parameter
Callers pass aiAgentId directly in the request DTO:
- DirectCall.calledParty when kind == "AI"
- AiParty.aiAgentId in scheduled call requests
2. Channel default
When a channel is configured with aiAgent: true, the system falls back to the team's default AI agent:
// CallApi.java:314–318
if (!routeConfig.isRouteToAiAgent() && channel.isAiAgent()) {
Optional.of(request.getTeamId())
.flatMap(contactService::findTeamById)
.map(TeamDetails::getAiAgent)
.ifPresent(routeConfig::setAiAgentId);
}
Once resolved, the agent record is fetched from the AI Provisioning Service (Compass) via Feign:
// AiProvisioningService.java
@FeignClient(name = "ai-provisioning-service")
@GetMapping("/tenants/agents/{agentId}")
Optional<AiAgent> findAiAgent(@PathVariable String agentId);
Routing & Queue Management¶
Routing is implemented entirely within Call Service using Redis sorted sets and atomic Lua scripts — there is no separate routing microservice.
Queue Keys (tenant-scoped)¶
| Key | Purpose |
|---|---|
{tenantId}:calls |
Main call queue |
{tenantId}:transfer-calls |
Transfer queue |
{tenantId}:channel-calls |
Channel-specific queue |
{tenantId}:calls/{callId}/contacts |
Eligible contacts for a specific call |
{tenantId}:contacts:available |
Sorted set of available contact IDs |
{tenantId}:contacts:inviting:{contactId} |
60 s TTL entry during active invite |
Routing Strategies¶
Two strategies are stored per call in {tenantId}:calls-routes:
NEXT_AVAILABLE (default)
Performs an atomic ZINTERSTORE across: available contacts ∩ call-eligible contacts ∩ skill/team/language group sets. Picks the minimum-scored (highest priority) contact not currently being invited.
NEXT_IN_QUEUE
Iterates the call's contact list in score (priority) order. Picks the first contact that is available and not currently being invited.
Both strategies are implemented in a single Redis Lua script:
src/main/resources/scripts/call_queue_script.lua
The script ensures atomicity — no race conditions between concurrent routing decisions.
Queue Classes¶
| Class | Queue |
|---|---|
CallQueueService |
calls |
TransferCallQueueService |
transfer-calls |
ChannelCallQueueService |
channel-calls |
BaseCallQueueService |
Abstract base; manages Redis pub/sub listeners for availability changes |
LiveKit Integration¶
Room Creation¶
Call Service creates a LiveKit room before accepting any call. Room metadata carries all context Atlas needs:
Room name format:
{tenantId}:{channelType}:{channelId}:{callId}
RoomMetadata fields passed to LiveKit:
| Field | Description |
|---|---|
callId |
Unique call identifier |
aiAgentId |
Resolved AI agent ID (used by Atlas to query Compass) |
callingPartyId/Name/Type/Number |
Caller identity |
callRecording, transcription |
Feature flags |
videoEnabled, chatEnabled |
Media flags |
languageCode |
Language hint for STT |
liveTranslate, transferable |
Feature flags |
ParticipantMetadata fields per participant:
| Field | Description |
|---|---|
id |
Contact/user ID |
displayName |
Display name |
kind |
"AI" or contact type |
roleId |
Team role |
Webhook Processing¶
LiveKit posts events to POST /livekit/webhook. Call Service handles:
| Event | Action |
|---|---|
room_started |
Mark call as active |
room_finished |
Mark call completed; trigger billing publish |
participant_joined |
Update participant status → JOINED |
participant_left |
Update participant status → LEFT; calculate duration |
track_published/unpublished |
Track media state |
REST API¶
| Method | Path | Purpose |
|---|---|---|
POST |
/calls |
Create call (Direct / Dialer / CallToAvailable / Scheduled) |
GET |
/calls |
Search calls (supports long-polling via waitMs) |
GET |
/calls/{id} |
Get call details |
GET |
/calls/latest |
Get latest call by phone number + type |
POST |
/calls/{id}/accept |
Accept invitation |
POST |
/calls/{id}/decline |
Decline invitation |
POST |
/calls/{id}/join |
Join existing call |
POST |
/calls/{id}/participants |
Invite participant |
DELETE |
/calls/{id}/participants/{participantId} |
Remove participant |
PUT |
/calls/{id}/hold |
Put participant on hold |
PUT |
/calls/{id}/unhold |
Resume from hold |
POST |
/calls/{id}/transfers |
Queue a transfer request |
POST |
/livekit/webhook |
Receive LiveKit events |
GET |
/live-queue/snapshot |
Live dashboard (team-pivoted) |
GET |
/live-queue/snapshot/summary |
Dashboard summary + agent status |
GET |
/live-queue/snapshot/teams |
Teams with call counts |
GET |
/live-queue/snapshot/waiting |
Calls in queue |
GET |
/live-queue/snapshot/agents |
Agent activity |
Request DTOs¶
| Class | Purpose |
|---|---|
DirectCall |
Call to a specific contact or AI agent |
DialerCall |
Call from dialler (phone number + contact type) |
CallToAvailable |
Route to an available team member by role + groups |
ScheduledCall |
Pre-scheduled call with AiParty / TeamParty / ContactParty |
InviteParticipantRequest |
Invite a participant to an active call |
TransferToContact / TransferToGroup |
Transfer routing targets |
Key Domain Entities¶
Call¶
| Field | Type | Description |
|---|---|---|
id |
UUID | Unique call ID |
channel |
FK | Associated channel |
callingParty / calledParty |
string | Contact or AI agent IDs |
status |
Enum | ROUTING, ALERTING, ACTIVE, COMPLETED, FAILED, BUSY, … |
routeConfig |
Embedded | See RouteConfig below |
startTime / connectedTime / endTime |
Instant | Timestamps |
callRecording, transcription, video |
boolean | Feature flags |
RouteConfig (embedded)¶
| Field | Description |
|---|---|
role |
Role name for skills-based routing |
aiAgentId |
Target AI agent ID |
contactId |
Target contact ID |
groups |
JSON array of skill/team/language groups |
lang |
Language code (BCP-47) |
Participant¶
| Field | Description |
|---|---|
participantId |
Contact/user identifier |
status |
JOINED, LEFT, MISSED, HOLD, DECLINED, … |
kind |
"AI" or contact type |
inviteId / expiry |
Invitation tracking |
joinedAt / leftTime / duration |
Call timing |
Channel¶
| Field | Description |
|---|---|
identity |
Channel ID (PK) |
type |
PHONE, VIDEO, WIDGET, … |
routeConfig |
Default route configuration |
aiAgent |
Whether this channel uses AI by default |
availabilityConfig |
Availability hours / rules |
transferCx |
FK to CxConnection for transfer routing |
External Dependencies¶
| Service | Client | Purpose |
|---|---|---|
contact-service |
Feign | Contact lookup, team info, availability |
ai-provisioning-service |
Feign | AI agent config (see Compass) |
notification-service |
Redis Pub/Sub + Feign | Call invitation delivery |
| LiveKit | io.livekit:livekit-server:0.10.0 |
Room creation, SIP, egress |
| PostgreSQL | Spring Data JPA + Flyway | Persistent call/participant records |
| Redis / Valkey | Spring Data Redis | Queues, availability, pub/sub |