@calloop/agent-sdk 0.0.0-stage → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +231 -3
- package/package.json +45 -6
- package/src/Call.js +296 -0
- package/src/CalloopAgent.js +483 -0
- package/src/Monitor.js +173 -0
- package/src/emitter.js +25 -0
- package/src/index.d.ts +268 -0
- package/src/index.js +5 -0
package/src/index.d.ts
ADDED
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
// Types for @calloop/agent-sdk (docs/agent-protocol.md).
|
|
2
|
+
|
|
3
|
+
export type Availability = 'AVAILABLE' | 'ON_CALL' | 'OFFLINE';
|
|
4
|
+
|
|
5
|
+
export interface BoardFilter {
|
|
6
|
+
channelIds?: Array<number | string>;
|
|
7
|
+
queueIds?: Array<number | string>;
|
|
8
|
+
agentIds?: Array<number | string>;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface BoardCounters {
|
|
12
|
+
tenantId: number;
|
|
13
|
+
calls: { live: number; inIvr: number; waiting: number; ringing: number; inProgress: number };
|
|
14
|
+
agents: { total: number; available: number; onCall: number; offline: number };
|
|
15
|
+
at: string;
|
|
16
|
+
}
|
|
17
|
+
export type CallStatus = 'INITIATED' | 'RINGING' | 'ACCEPTED' | 'IN_PROGRESS' | 'TERMINATED' | 'FAILED' | 'CANCELLED';
|
|
18
|
+
export type CallStateName = 'ringing' | 'dialing' | 'connecting' | 'active' | 'elsewhere' | 'ended';
|
|
19
|
+
export type MonitorMode = 'listen' | 'whisper' | 'barge';
|
|
20
|
+
export type AgentRole = 'AGENT' | 'SUPERVISOR';
|
|
21
|
+
|
|
22
|
+
type Listener = (...args: any[]) => void;
|
|
23
|
+
|
|
24
|
+
/** Minimal event emitter: on() returns an unsubscribe function. */
|
|
25
|
+
export class Emitter {
|
|
26
|
+
on(event: string, listener: Listener): () => void;
|
|
27
|
+
once(event: string, listener: Listener): () => void;
|
|
28
|
+
off(event: string, listener: Listener): void;
|
|
29
|
+
emit(event: string, ...args: unknown[]): void;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface Customer {
|
|
33
|
+
address?: string;
|
|
34
|
+
addressType?: 'E164' | 'WHATSAPP_USER' | 'SIP_URI';
|
|
35
|
+
name?: string | null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** The call payload shared by call:incoming, call:started and calls:list. */
|
|
39
|
+
export interface CallData {
|
|
40
|
+
callId: number | string;
|
|
41
|
+
callUuid?: string;
|
|
42
|
+
tenantId?: number | string;
|
|
43
|
+
channel?: 'WHATSAPP' | 'SIP' | string;
|
|
44
|
+
channelId?: number | string;
|
|
45
|
+
channelAddress?: string;
|
|
46
|
+
queueId?: number | string | null;
|
|
47
|
+
direction?: 'INBOUND' | 'OUTBOUND';
|
|
48
|
+
status?: CallStatus;
|
|
49
|
+
state?: 'IVR' | 'QUEUE' | 'ACTIVE' | 'ON_HOLD' | null;
|
|
50
|
+
/** 'DIRECT': rtpengine alone carries it (a personal line's 1:1 call); listen-only monitoring. */
|
|
51
|
+
mediaTopology?: 'ROOM' | 'DIRECT';
|
|
52
|
+
customer?: Customer;
|
|
53
|
+
agentId?: number | string | null;
|
|
54
|
+
agentName?: string | null;
|
|
55
|
+
externalRef?: string | null;
|
|
56
|
+
ringingAt?: string | null;
|
|
57
|
+
answeredAt?: string | null;
|
|
58
|
+
endedAt?: string | null;
|
|
59
|
+
offeredAgentIds?: Array<number | string>;
|
|
60
|
+
assignmentType?: 'DIRECT' | 'QUEUED' | 'TRANSFERRED' | 'IVR' | string;
|
|
61
|
+
transferredFrom?: { id: number | string | null; name: string | null; isAssignment: boolean } | null;
|
|
62
|
+
assignedBy?: { id: number | string; name: string } | null;
|
|
63
|
+
deviceId?: string | null;
|
|
64
|
+
[field: string]: unknown;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** A call on a supervisor's board (no SDP). ivr: live IVR position, or null. */
|
|
68
|
+
export interface BoardCall extends CallData {
|
|
69
|
+
ivr?: { nodeType: string; nodeId: string } | null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface Session {
|
|
73
|
+
protocol: number;
|
|
74
|
+
agent: { id: number | string; ref: string; name: string | null; role: AgentRole | null };
|
|
75
|
+
tenant: { id: number | string; ref: string };
|
|
76
|
+
deviceId: string | null;
|
|
77
|
+
purpose: string;
|
|
78
|
+
iceServers: RTCIceServer[];
|
|
79
|
+
/** When TURN credentials expire (the SDK refreshes before that); null for static ones. */
|
|
80
|
+
iceServersExpireAt: string | null;
|
|
81
|
+
serverTime: string;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export interface TeamMember { agentId: number | string; availability: Availability; reason?: string; updatedAt?: string }
|
|
85
|
+
|
|
86
|
+
export interface QueueSnapshot {
|
|
87
|
+
tenantId: number | string;
|
|
88
|
+
queueId: number | string;
|
|
89
|
+
queueName?: string;
|
|
90
|
+
strategy?: 'RING_ALL' | 'ROUND_ROBIN' | 'PRIORITY';
|
|
91
|
+
[field: string]: unknown;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export interface CallError { callId: number | string | null; code: string; message: string }
|
|
95
|
+
|
|
96
|
+
export interface ConnectOptions {
|
|
97
|
+
/** Calloop's base URL. */
|
|
98
|
+
url: string;
|
|
99
|
+
/** A fixed agent JWT — or getToken, called on every (re)connect. */
|
|
100
|
+
token?: string;
|
|
101
|
+
getToken?: () => string | Promise<string>;
|
|
102
|
+
/** Stable id for this device (default: one kept in localStorage). */
|
|
103
|
+
deviceId?: string;
|
|
104
|
+
/** WebRTC outside a browser, e.g. @roamhq/wrtc in Node. */
|
|
105
|
+
webrtc?: { RTCPeerConnection: typeof RTCPeerConnection; MediaStream: typeof MediaStream };
|
|
106
|
+
/** The microphone (default: navigator.mediaDevices.getUserMedia). */
|
|
107
|
+
getUserMedia?: () => Promise<MediaStream>;
|
|
108
|
+
/** socket.io-client's io (default: the bundled one). */
|
|
109
|
+
io?: (url: string, opts: object) => unknown;
|
|
110
|
+
/**
|
|
111
|
+
* 'session' (default): the agent's main connection. Anything else is an
|
|
112
|
+
* auxiliary connection (e.g. one that only declines a call): no presence,
|
|
113
|
+
* no redelivered calls, and no calls:sync on connect.
|
|
114
|
+
*/
|
|
115
|
+
purpose?: string;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export type MonitorEndReason = 'stopped' | 'ended' | 'call_ended' | 'disconnected' | 'failed' | 'not_allowed' | 'closed';
|
|
119
|
+
|
|
120
|
+
/** Why a failed call failed, in the provider's own words. */
|
|
121
|
+
export interface CallFailure {
|
|
122
|
+
/** A SIP status (404, 503, …), a WhatsApp error code, or Calloop's own (PROVIDER_TRIGGER_FAILED). */
|
|
123
|
+
code: number | string | null;
|
|
124
|
+
/** Its text: a SIP reason phrase ("Not Found"), the provider's error title. */
|
|
125
|
+
title: string | null;
|
|
126
|
+
/** More, when the provider gave it: a SIP trunk's Reason / Warning headers. */
|
|
127
|
+
details?: string;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export interface EndInfo {
|
|
131
|
+
reason: 'hangup' | 'declined' | 'withdrawn' | 'answered_elsewhere' | 'terminated' | 'gone' | 'accept_failed' | 'media_failed' | 'error' | string;
|
|
132
|
+
terminationReason?: string;
|
|
133
|
+
terminatedBy?: 'AGENT' | 'CUSTOMER' | 'PROVIDER' | 'SYSTEM';
|
|
134
|
+
withdrawnReason?: 'declined' | 'taken' | 'timeout' | 'overflow' | string;
|
|
135
|
+
/** With `terminated`, a failed call: why. */
|
|
136
|
+
failure?: CallFailure | null;
|
|
137
|
+
[field: string]: unknown;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export class Call extends Emitter {
|
|
141
|
+
readonly id: number | string;
|
|
142
|
+
readonly data: CallData;
|
|
143
|
+
readonly state: CallStateName;
|
|
144
|
+
readonly endReason: string | null;
|
|
145
|
+
readonly muted: boolean;
|
|
146
|
+
readonly localStream: MediaStream | null;
|
|
147
|
+
readonly remoteStream: MediaStream | null;
|
|
148
|
+
/** The call's WebRTC leg to Calloop, or null when there is none (ringing, elsewhere, ended). */
|
|
149
|
+
readonly pc: RTCPeerConnection | null;
|
|
150
|
+
readonly direction: 'INBOUND' | 'OUTBOUND' | undefined;
|
|
151
|
+
readonly customer: Customer;
|
|
152
|
+
readonly channel: string | undefined;
|
|
153
|
+
readonly queueId: number | string | null;
|
|
154
|
+
readonly callUuid: string | null;
|
|
155
|
+
/** Offered to every member of a RING_ALL queue at once. */
|
|
156
|
+
readonly isRingAll: boolean;
|
|
157
|
+
/** A direct call (mediaTopology 'DIRECT'): it reconnects like any other call (the SDK sends its own offer). */
|
|
158
|
+
readonly isDirect: boolean;
|
|
159
|
+
|
|
160
|
+
accept(opts?: { stream?: MediaStream }): Promise<void>;
|
|
161
|
+
decline(): void;
|
|
162
|
+
hangup(): void;
|
|
163
|
+
mute(muted?: boolean): void;
|
|
164
|
+
transfer(target: { agentId: number | string } | { queueId: number | string }): void;
|
|
165
|
+
/** Talk privately to the monitoring supervisor. */
|
|
166
|
+
setPrivate(active: boolean): void;
|
|
167
|
+
/** Move a call active on another of your devices to this one. */
|
|
168
|
+
switchHere(opts?: { stream?: MediaStream }): Promise<void>;
|
|
169
|
+
|
|
170
|
+
on(event: 'state', listener: (state: CallStateName, previous: CallStateName) => void): () => void;
|
|
171
|
+
on(event: 'remoteStream', listener: (stream: MediaStream, track: MediaStreamTrack) => void): () => void;
|
|
172
|
+
on(event: 'ended', listener: (info: EndInfo) => void): () => void;
|
|
173
|
+
on(event: 'updated', listener: (data: CallData) => void): () => void;
|
|
174
|
+
on(event: 'customerMedia', listener: (p: { callId: number | string; state: 'active' | 'drop' }) => void): () => void;
|
|
175
|
+
on(event: 'networkTerminating', listener: (p: { callId: number | string }) => void): () => void;
|
|
176
|
+
on(event: 'dtmf', listener: (p: { callId: number | string; digit: string }) => void): () => void;
|
|
177
|
+
on(event: 'supervisorMode', listener: (p: { callId: number | string; mode: MonitorMode }) => void): () => void;
|
|
178
|
+
on(event: 'privateChanged', listener: (p: { callId: number | string; active: boolean }) => void): () => void;
|
|
179
|
+
on(event: string, listener: Listener): () => void;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export class Monitor extends Emitter {
|
|
183
|
+
readonly callId: number | string;
|
|
184
|
+
readonly state: 'connecting' | 'active' | 'ended';
|
|
185
|
+
readonly mode: MonitorMode;
|
|
186
|
+
readonly muted: boolean;
|
|
187
|
+
/** The call as the supervisor hears it: the customer and the agent, mixed. */
|
|
188
|
+
readonly stream: MediaStream | null;
|
|
189
|
+
|
|
190
|
+
/** listen: hear the call, heard by nobody · whisper: the agent hears you · barge: both hear you. */
|
|
191
|
+
setMode(mode: MonitorMode): void;
|
|
192
|
+
mute(muted?: boolean): void;
|
|
193
|
+
stop(): void;
|
|
194
|
+
|
|
195
|
+
on(event: 'state', listener: (state: Monitor['state'], previous: Monitor['state']) => void): () => void;
|
|
196
|
+
on(event: 'stream', listener: (stream: MediaStream, track: MediaStreamTrack) => void): () => void;
|
|
197
|
+
on(event: 'mode', listener: (mode: MonitorMode) => void): () => void;
|
|
198
|
+
on(event: 'agentPrivate', listener: (p: { callId: number | string; active: boolean }) => void): () => void;
|
|
199
|
+
on(event: 'agentReconnected', listener: (p: { callId: number | string }) => void): () => void;
|
|
200
|
+
/** A setMode() Calloop refused (MONITOR_MODE_NOT_ALLOWED / MONITOR_FAILED): the monitor goes on in its current mode. */
|
|
201
|
+
on(event: 'modeRefused', listener: (p: { mode: MonitorMode | null; code: string; message: string }) => void): () => void;
|
|
202
|
+
/** failed / not_allowed (the tenant doesn't allow monitoring) carry Calloop's code and message. */
|
|
203
|
+
on(event: 'ended', listener: (info: { reason: MonitorEndReason | string; code?: string; message?: string }) => void): () => void;
|
|
204
|
+
on(event: string, listener: Listener): () => void;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export class CalloopAgent extends Emitter {
|
|
208
|
+
constructor(opts: ConnectOptions);
|
|
209
|
+
readonly url: string;
|
|
210
|
+
readonly deviceId: string;
|
|
211
|
+
readonly purpose: string;
|
|
212
|
+
/** Set when Calloop ended the connection for good ('closed'); null otherwise. */
|
|
213
|
+
readonly closedReason: string | null;
|
|
214
|
+
readonly ready: Promise<CalloopAgent>;
|
|
215
|
+
readonly session: Session | null;
|
|
216
|
+
readonly agent: Session['agent'] | null;
|
|
217
|
+
readonly iceServers: RTCIceServer[];
|
|
218
|
+
readonly availability: Availability | null;
|
|
219
|
+
/** This agent's calls. */
|
|
220
|
+
readonly calls: Map<string, Call>;
|
|
221
|
+
readonly queues: Map<string, QueueSnapshot>;
|
|
222
|
+
/** Availability of the tenant's agents, as they change. */
|
|
223
|
+
readonly team: Map<string, TeamMember>;
|
|
224
|
+
/** Supervisors: the tenant's live calls. */
|
|
225
|
+
readonly board: Map<string, BoardCall>;
|
|
226
|
+
/** Supervisors: calls being monitored. */
|
|
227
|
+
readonly monitors: Map<string, Monitor>;
|
|
228
|
+
readonly isSupervisor: boolean;
|
|
229
|
+
|
|
230
|
+
/** Supervisors may set another agent's availability. */
|
|
231
|
+
setAvailability(availability: 'AVAILABLE' | 'OFFLINE', opts?: { agentId?: number | string }): void;
|
|
232
|
+
sync(): void;
|
|
233
|
+
/** Narrow this connection's board (any of the given lines, queues, agents); {} = the whole tenant. */
|
|
234
|
+
subscribeBoard(filter?: BoardFilter): Promise<BoardFilter | null>;
|
|
235
|
+
unsubscribeBoard(): Promise<void>;
|
|
236
|
+
/** A page of the live calls, newest first. */
|
|
237
|
+
boardCalls(query?: BoardFilter & { cursor?: number | string; limit?: number }): Promise<{ calls: CallData[]; nextCursor: number | null }>;
|
|
238
|
+
/** Supervisors: the tenant's counters now. */
|
|
239
|
+
boardCounters(): Promise<BoardCounters>;
|
|
240
|
+
startOutbound(callId: number | string, opts?: { stream?: MediaStream }): Promise<Call>;
|
|
241
|
+
call(callId: number | string): Call | null;
|
|
242
|
+
/** Supervisors: listen to a call. */
|
|
243
|
+
monitor(callId: number | string, opts?: { stream?: MediaStream }): Promise<Monitor>;
|
|
244
|
+
/** Supervisors: move any call to an agent or into a queue. */
|
|
245
|
+
transferCall(callId: number | string, target: { agentId: number | string } | { queueId: number | string }): void;
|
|
246
|
+
refreshSession(): void;
|
|
247
|
+
close(): void;
|
|
248
|
+
|
|
249
|
+
on(event: 'ready' | 'sessionRefreshed', listener: (session: Session) => void): () => void;
|
|
250
|
+
on(event: 'counters', listener: (counters: BoardCounters) => void): () => void;
|
|
251
|
+
on(event: 'incoming' | 'elsewhere', listener: (call: Call) => void): () => void;
|
|
252
|
+
on(event: 'callState', listener: (call: Call, state: CallStateName, previous: CallStateName) => void): () => void;
|
|
253
|
+
on(event: 'callEnded', listener: (call: Call, info: EndInfo) => void): () => void;
|
|
254
|
+
on(event: 'availability', listener: (p: { availability: Availability; reason?: string }) => void): () => void;
|
|
255
|
+
on(event: 'team', listener: (member: TeamMember) => void): () => void;
|
|
256
|
+
on(event: 'queue', listener: (snapshot: QueueSnapshot) => void): () => void;
|
|
257
|
+
on(event: 'board', listener: (board: Map<string, BoardCall>) => void): () => void;
|
|
258
|
+
on(event: 'boardCall', listener: (call: BoardCall) => void): () => void;
|
|
259
|
+
on(event: 'boardCallEnded', listener: (call: BoardCall, info: { terminationReason?: string; terminatedBy?: string }) => void): () => void;
|
|
260
|
+
on(event: 'error', listener: (error: CallError | Error) => void): () => void;
|
|
261
|
+
on(event: 'disconnected', listener: (reason: string) => void): () => void;
|
|
262
|
+
/** Calloop ended the connection and it won't reconnect (e.g. its signing key was revoked): close() and connect again. */
|
|
263
|
+
on(event: 'closed', listener: (info: { reason: 'io server disconnect' | string }) => void): () => void;
|
|
264
|
+
on(event: string, listener: Listener): () => void;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/** Connects and resolves once Calloop has identified the agent (session:ready). */
|
|
268
|
+
export function connect(opts: ConnectOptions): Promise<CalloopAgent>;
|
package/src/index.js
ADDED