@anganyai/voice-sdk 0.0.1 → 0.0.4
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/dist/index.cjs +389 -55
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +76 -7
- package/dist/index.d.ts +76 -7
- package/dist/index.js +389 -55
- package/dist/index.js.map +1 -1
- package/package.json +9 -1
package/dist/index.js
CHANGED
|
@@ -3459,8 +3459,18 @@ var AuthManager = class extends EventEmitter {
|
|
|
3459
3459
|
}
|
|
3460
3460
|
/**
|
|
3461
3461
|
* Get ephemeral credentials for WebRTC connections
|
|
3462
|
+
*
|
|
3463
|
+
* Returns cached credentials if available and valid, otherwise fetches new ones.
|
|
3464
|
+
* This supports both:
|
|
3465
|
+
* - Externally-provided credentials (via setEphemeralCredentials)
|
|
3466
|
+
* - SDK-managed credentials (fetched using access token)
|
|
3462
3467
|
*/
|
|
3463
3468
|
async getEphemeralCredentials() {
|
|
3469
|
+
const cached = this.credentialManager.getCachedEphemeralCredentials();
|
|
3470
|
+
if (cached) {
|
|
3471
|
+
this.logger.debug("Using cached ephemeral credentials");
|
|
3472
|
+
return cached;
|
|
3473
|
+
}
|
|
3464
3474
|
const accessToken = await this.getAccessToken();
|
|
3465
3475
|
if (!accessToken) {
|
|
3466
3476
|
throw new AuthenticationError("No access token available for ephemeral credentials");
|
|
@@ -16236,6 +16246,26 @@ var UserAgent = class _UserAgent {
|
|
|
16236
16246
|
}
|
|
16237
16247
|
};
|
|
16238
16248
|
|
|
16249
|
+
// src/utils/platform.ts
|
|
16250
|
+
function detectPlatform() {
|
|
16251
|
+
if (typeof navigator !== "undefined" && navigator.product === "ReactNative") {
|
|
16252
|
+
return "react-native";
|
|
16253
|
+
}
|
|
16254
|
+
if (typeof window !== "undefined" && typeof document !== "undefined" && typeof navigator !== "undefined") {
|
|
16255
|
+
return "browser";
|
|
16256
|
+
}
|
|
16257
|
+
return "node";
|
|
16258
|
+
}
|
|
16259
|
+
function isReactNative() {
|
|
16260
|
+
return detectPlatform() === "react-native";
|
|
16261
|
+
}
|
|
16262
|
+
function isBrowser() {
|
|
16263
|
+
return detectPlatform() === "browser";
|
|
16264
|
+
}
|
|
16265
|
+
function hasAudioContext() {
|
|
16266
|
+
return typeof AudioContext !== "undefined" || typeof globalThis.webkitAudioContext !== "undefined";
|
|
16267
|
+
}
|
|
16268
|
+
|
|
16239
16269
|
// src/services/SipManager.ts
|
|
16240
16270
|
var SipManager = class extends EventEmitter {
|
|
16241
16271
|
constructor() {
|
|
@@ -16534,7 +16564,9 @@ var SipManager = class extends EventEmitter {
|
|
|
16534
16564
|
}
|
|
16535
16565
|
}
|
|
16536
16566
|
setupAudioHandling(session) {
|
|
16537
|
-
this.logger.debug("Setting up audio handling using SIP.js streams"
|
|
16567
|
+
this.logger.debug("Setting up audio handling using SIP.js streams", {
|
|
16568
|
+
platform: isReactNative() ? "react-native" : "browser"
|
|
16569
|
+
});
|
|
16538
16570
|
const sessionDescriptionHandler = session.sessionDescriptionHandler;
|
|
16539
16571
|
if (sessionDescriptionHandler) {
|
|
16540
16572
|
this.logger.debug("Session description handler found");
|
|
@@ -16542,18 +16574,22 @@ var SipManager = class extends EventEmitter {
|
|
|
16542
16574
|
if (remoteStream) {
|
|
16543
16575
|
this.logger.info("Remote media stream available");
|
|
16544
16576
|
this.remoteStream = remoteStream;
|
|
16545
|
-
|
|
16546
|
-
|
|
16547
|
-
|
|
16548
|
-
this.
|
|
16549
|
-
|
|
16550
|
-
|
|
16577
|
+
if (isReactNative()) {
|
|
16578
|
+
this.logger.info("React Native: Remote audio will be played automatically by WebRTC");
|
|
16579
|
+
} else {
|
|
16580
|
+
this.createAudioElement();
|
|
16581
|
+
if (this.audioElement) {
|
|
16582
|
+
this.audioElement.srcObject = remoteStream;
|
|
16583
|
+
this.audioElement.play().catch((error) => {
|
|
16584
|
+
this.logger.warn("Autoplay prevented, enabling controls for user interaction", {
|
|
16585
|
+
error
|
|
16586
|
+
});
|
|
16587
|
+
if (this.audioElement) {
|
|
16588
|
+
this.audioElement.controls = true;
|
|
16589
|
+
this.audioElement.style.display = "block";
|
|
16590
|
+
}
|
|
16551
16591
|
});
|
|
16552
|
-
|
|
16553
|
-
this.audioElement.controls = true;
|
|
16554
|
-
this.audioElement.style.display = "block";
|
|
16555
|
-
}
|
|
16556
|
-
});
|
|
16592
|
+
}
|
|
16557
16593
|
}
|
|
16558
16594
|
this.startAudioMonitoring();
|
|
16559
16595
|
} else {
|
|
@@ -16572,16 +16608,22 @@ var SipManager = class extends EventEmitter {
|
|
|
16572
16608
|
if (event.streams[0]) {
|
|
16573
16609
|
this.remoteStream = event.streams[0];
|
|
16574
16610
|
this.logger.info("Remote audio stream received via ontrack fallback");
|
|
16575
|
-
|
|
16576
|
-
|
|
16577
|
-
|
|
16578
|
-
|
|
16579
|
-
|
|
16580
|
-
|
|
16581
|
-
|
|
16582
|
-
|
|
16583
|
-
|
|
16584
|
-
|
|
16611
|
+
if (isReactNative()) {
|
|
16612
|
+
this.logger.info(
|
|
16613
|
+
"React Native: Remote audio from ontrack will be played automatically"
|
|
16614
|
+
);
|
|
16615
|
+
} else {
|
|
16616
|
+
this.createAudioElement();
|
|
16617
|
+
if (this.audioElement && this.remoteStream) {
|
|
16618
|
+
this.audioElement.srcObject = this.remoteStream;
|
|
16619
|
+
this.audioElement.play().catch((error) => {
|
|
16620
|
+
this.logger.warn("Failed to auto-play remote audio", { error });
|
|
16621
|
+
if (this.audioElement) {
|
|
16622
|
+
this.audioElement.controls = true;
|
|
16623
|
+
this.audioElement.style.display = "block";
|
|
16624
|
+
}
|
|
16625
|
+
});
|
|
16626
|
+
}
|
|
16585
16627
|
}
|
|
16586
16628
|
this.startAudioMonitoring();
|
|
16587
16629
|
}
|
|
@@ -16610,8 +16652,19 @@ var SipManager = class extends EventEmitter {
|
|
|
16610
16652
|
}
|
|
16611
16653
|
/**
|
|
16612
16654
|
* Create and configure the audio element for remote audio playback
|
|
16655
|
+
* In React Native, audio is handled differently through WebRTC streams
|
|
16613
16656
|
*/
|
|
16614
16657
|
createAudioElement() {
|
|
16658
|
+
if (isReactNative()) {
|
|
16659
|
+
this.logger.debug(
|
|
16660
|
+
"React Native detected - skipping audio element creation (handled by WebRTC)"
|
|
16661
|
+
);
|
|
16662
|
+
return;
|
|
16663
|
+
}
|
|
16664
|
+
if (!isBrowser()) {
|
|
16665
|
+
this.logger.debug("Non-browser environment - skipping audio element creation");
|
|
16666
|
+
return;
|
|
16667
|
+
}
|
|
16615
16668
|
if (this.audioElement) {
|
|
16616
16669
|
return;
|
|
16617
16670
|
}
|
|
@@ -16670,6 +16723,22 @@ var SipManager = class extends EventEmitter {
|
|
|
16670
16723
|
if (!this.localStream || this.audioMonitorInterval) {
|
|
16671
16724
|
return;
|
|
16672
16725
|
}
|
|
16726
|
+
if (isReactNative() || !hasAudioContext()) {
|
|
16727
|
+
this.logger.debug("AudioContext not available - using simulated audio monitoring");
|
|
16728
|
+
this.audioMonitorInterval = setInterval(() => {
|
|
16729
|
+
if (this.state === "incall" && this.localStream) {
|
|
16730
|
+
const audioTrack = this.localStream.getAudioTracks()[0];
|
|
16731
|
+
if (audioTrack && audioTrack.enabled) {
|
|
16732
|
+
const simulatedLevel = 0.2 + Math.random() * 0.5;
|
|
16733
|
+
this.emit("audioLevel", simulatedLevel);
|
|
16734
|
+
} else {
|
|
16735
|
+
this.emit("audioLevel", 0);
|
|
16736
|
+
}
|
|
16737
|
+
}
|
|
16738
|
+
}, 100);
|
|
16739
|
+
this.logger.debug("Simulated audio monitoring started");
|
|
16740
|
+
return;
|
|
16741
|
+
}
|
|
16673
16742
|
try {
|
|
16674
16743
|
this.audioContext = new AudioContext();
|
|
16675
16744
|
const source = this.audioContext.createMediaStreamSource(this.localStream);
|
|
@@ -16701,9 +16770,11 @@ var SipManager = class extends EventEmitter {
|
|
|
16701
16770
|
delete this.audioAnalyser;
|
|
16702
16771
|
}
|
|
16703
16772
|
cleanupAudioResources() {
|
|
16704
|
-
this.logger.debug("\u{1F9F9} Performing complete audio resource cleanup..."
|
|
16773
|
+
this.logger.debug("\u{1F9F9} Performing complete audio resource cleanup...", {
|
|
16774
|
+
platform: isReactNative() ? "react-native" : "browser"
|
|
16775
|
+
});
|
|
16705
16776
|
this.stopAudioMonitoring();
|
|
16706
|
-
if (this.audioElement) {
|
|
16777
|
+
if (this.audioElement && isBrowser()) {
|
|
16707
16778
|
this.logger.debug("\u{1F9F9} Cleaning up audio element...");
|
|
16708
16779
|
if (this.audioElement.srcObject) {
|
|
16709
16780
|
const tracks = this.audioElement.srcObject.getTracks();
|
|
@@ -16864,7 +16935,7 @@ var TranscriptionService = class extends EventEmitter {
|
|
|
16864
16935
|
* Connect to SSE endpoint with token refresh support
|
|
16865
16936
|
*/
|
|
16866
16937
|
async connectToSSE(accessToken, isRetry = false) {
|
|
16867
|
-
const sseUrl = `${this.apiUrl}/api/v1/events?event_types=transcription`;
|
|
16938
|
+
const sseUrl = `${this.apiUrl}/api/v1/events?event_types=transcription,call_event`;
|
|
16868
16939
|
this.logger.debug("Connecting to SSE endpoint", { url: sseUrl, isRetry });
|
|
16869
16940
|
const response = await fetch(sseUrl, {
|
|
16870
16941
|
method: "GET",
|
|
@@ -16994,13 +17065,36 @@ var TranscriptionService = class extends EventEmitter {
|
|
|
16994
17065
|
}
|
|
16995
17066
|
}
|
|
16996
17067
|
handleMessage(data) {
|
|
16997
|
-
this.logger.debug("Handling SSE message", { data });
|
|
16998
|
-
if (data.
|
|
16999
|
-
|
|
17000
|
-
|
|
17001
|
-
|
|
17002
|
-
|
|
17068
|
+
this.logger.debug("Handling SSE message", { type: data.type, data });
|
|
17069
|
+
if (data.call_id && !this.currentCallId) {
|
|
17070
|
+
this.currentCallId = data.call_id;
|
|
17071
|
+
this.emit("callId", data.call_id);
|
|
17072
|
+
this.logger.info("Captured call ID", { callId: data.call_id, messageType: data.type });
|
|
17073
|
+
}
|
|
17074
|
+
if (data.type === "connection" || data.type === "welcome" || data.type === "connected") {
|
|
17075
|
+
this.logger.info("Received connection event", {
|
|
17076
|
+
type: data.type,
|
|
17077
|
+
status: data.status
|
|
17078
|
+
});
|
|
17079
|
+
if (data.type === "connected" || data.status === "connected") {
|
|
17080
|
+
this.emit("connected");
|
|
17081
|
+
}
|
|
17082
|
+
} else if (data.type === "call_started") {
|
|
17083
|
+
this.logger.info("Received call_started event", {
|
|
17084
|
+
callId: data.call_id,
|
|
17085
|
+
organizationId: data.organization_id
|
|
17086
|
+
});
|
|
17087
|
+
if (data.call_id) {
|
|
17088
|
+
this.emit("callStarted", data.call_id);
|
|
17003
17089
|
}
|
|
17090
|
+
} else if (data.type === "call_ended") {
|
|
17091
|
+
this.logger.info("Received call_ended event", {
|
|
17092
|
+
callId: data.call_id
|
|
17093
|
+
});
|
|
17094
|
+
if (data.call_id) {
|
|
17095
|
+
this.emit("callEnded", data.call_id);
|
|
17096
|
+
}
|
|
17097
|
+
} else if (data.type === "transcription" && data.text) {
|
|
17004
17098
|
const event = {
|
|
17005
17099
|
speaker: data.direction === "agent" ? "agent" : "user",
|
|
17006
17100
|
text: data.text.trim(),
|
|
@@ -17008,6 +17102,9 @@ var TranscriptionService = class extends EventEmitter {
|
|
|
17008
17102
|
isFinal: true,
|
|
17009
17103
|
// Assume final for now
|
|
17010
17104
|
callId: data.call_id,
|
|
17105
|
+
humanTurnId: data.human_turn_id ?? void 0,
|
|
17106
|
+
agentTurnId: data.agent_turn_id ?? void 0,
|
|
17107
|
+
speakerId: data.speaker_id ?? void 0,
|
|
17011
17108
|
metadata: data
|
|
17012
17109
|
};
|
|
17013
17110
|
this.logger.debug("Emitting transcription event", { event });
|
|
@@ -17079,7 +17176,8 @@ var ApiService = class {
|
|
|
17079
17176
|
this.logger.debug("Sending voice text", {
|
|
17080
17177
|
callId: options.callId,
|
|
17081
17178
|
textLength: options.text.length,
|
|
17082
|
-
|
|
17179
|
+
interruptsConversation: options.interruptsConversation,
|
|
17180
|
+
queueWhenSpeaking: options.queueWhenSpeaking,
|
|
17083
17181
|
muteAgent: options.muteAgent
|
|
17084
17182
|
});
|
|
17085
17183
|
try {
|
|
@@ -17093,10 +17191,10 @@ var ApiService = class {
|
|
|
17093
17191
|
},
|
|
17094
17192
|
body: JSON.stringify({
|
|
17095
17193
|
text: options.text,
|
|
17096
|
-
interrupts_conversation: options.
|
|
17097
|
-
|
|
17098
|
-
|
|
17099
|
-
|
|
17194
|
+
interrupts_conversation: options.interruptsConversation ?? true,
|
|
17195
|
+
queue_when_speaking: options.queueWhenSpeaking ?? false,
|
|
17196
|
+
mute_agent: options.muteAgent ?? null,
|
|
17197
|
+
voice_settings: options.voiceSettings ?? null
|
|
17100
17198
|
})
|
|
17101
17199
|
}
|
|
17102
17200
|
);
|
|
@@ -17182,11 +17280,103 @@ var ApiService = class {
|
|
|
17182
17280
|
throw new NetworkError("Failed to set agent mute status", { cause: error });
|
|
17183
17281
|
}
|
|
17184
17282
|
}
|
|
17283
|
+
/**
|
|
17284
|
+
* Mute a call (POST /calls/{call_id}/mute)
|
|
17285
|
+
*/
|
|
17286
|
+
async muteCall(callId, accessToken) {
|
|
17287
|
+
this.logger.debug("Muting call", { callId });
|
|
17288
|
+
try {
|
|
17289
|
+
const response = await fetch(`${this.apiUrl}/api/v1/conversations/calls/${callId}/mute`, {
|
|
17290
|
+
method: "POST",
|
|
17291
|
+
headers: {
|
|
17292
|
+
Authorization: `Bearer ${accessToken}`
|
|
17293
|
+
}
|
|
17294
|
+
});
|
|
17295
|
+
if (response.status === 401) {
|
|
17296
|
+
throw new AuthenticationError("Authentication failed");
|
|
17297
|
+
}
|
|
17298
|
+
if (!response.ok) {
|
|
17299
|
+
const errorText = await response.text();
|
|
17300
|
+
throw new NetworkError(
|
|
17301
|
+
`Failed to mute call: ${response.status} ${response.statusText} - ${errorText}`
|
|
17302
|
+
);
|
|
17303
|
+
}
|
|
17304
|
+
this.logger.info("Call muted successfully", { callId });
|
|
17305
|
+
} catch (error) {
|
|
17306
|
+
if (error instanceof AuthenticationError || error instanceof NetworkError) {
|
|
17307
|
+
throw error;
|
|
17308
|
+
}
|
|
17309
|
+
this.logger.error("Failed to mute call", { error });
|
|
17310
|
+
throw new NetworkError("Failed to mute call", { cause: error });
|
|
17311
|
+
}
|
|
17312
|
+
}
|
|
17313
|
+
/**
|
|
17314
|
+
* Unmute a call (DELETE /calls/{call_id}/mute)
|
|
17315
|
+
*/
|
|
17316
|
+
async unmuteCall(callId, accessToken) {
|
|
17317
|
+
this.logger.debug("Unmuting call", { callId });
|
|
17318
|
+
try {
|
|
17319
|
+
const response = await fetch(`${this.apiUrl}/api/v1/conversations/calls/${callId}/mute`, {
|
|
17320
|
+
method: "DELETE",
|
|
17321
|
+
headers: {
|
|
17322
|
+
Authorization: `Bearer ${accessToken}`
|
|
17323
|
+
}
|
|
17324
|
+
});
|
|
17325
|
+
if (response.status === 401) {
|
|
17326
|
+
throw new AuthenticationError("Authentication failed");
|
|
17327
|
+
}
|
|
17328
|
+
if (!response.ok) {
|
|
17329
|
+
const errorText = await response.text();
|
|
17330
|
+
throw new NetworkError(
|
|
17331
|
+
`Failed to unmute call: ${response.status} ${response.statusText} - ${errorText}`
|
|
17332
|
+
);
|
|
17333
|
+
}
|
|
17334
|
+
this.logger.info("Call unmuted successfully", { callId });
|
|
17335
|
+
} catch (error) {
|
|
17336
|
+
if (error instanceof AuthenticationError || error instanceof NetworkError) {
|
|
17337
|
+
throw error;
|
|
17338
|
+
}
|
|
17339
|
+
this.logger.error("Failed to unmute call", { error });
|
|
17340
|
+
throw new NetworkError("Failed to unmute call", { cause: error });
|
|
17341
|
+
}
|
|
17342
|
+
}
|
|
17343
|
+
/**
|
|
17344
|
+
* Get call mute status (GET /calls/{call_id}/mute)
|
|
17345
|
+
*/
|
|
17346
|
+
async getCallMuteStatus(callId, accessToken) {
|
|
17347
|
+
this.logger.debug("Getting call mute status", { callId });
|
|
17348
|
+
try {
|
|
17349
|
+
const response = await fetch(`${this.apiUrl}/api/v1/conversations/calls/${callId}/mute`, {
|
|
17350
|
+
method: "GET",
|
|
17351
|
+
headers: {
|
|
17352
|
+
Authorization: `Bearer ${accessToken}`
|
|
17353
|
+
}
|
|
17354
|
+
});
|
|
17355
|
+
if (response.status === 401) {
|
|
17356
|
+
throw new AuthenticationError("Authentication failed");
|
|
17357
|
+
}
|
|
17358
|
+
if (!response.ok) {
|
|
17359
|
+
const errorText = await response.text();
|
|
17360
|
+
throw new NetworkError(
|
|
17361
|
+
`Failed to get call mute status: ${response.status} ${response.statusText} - ${errorText}`
|
|
17362
|
+
);
|
|
17363
|
+
}
|
|
17364
|
+
const data = await response.json();
|
|
17365
|
+
this.logger.debug("Call mute status retrieved", { callId, muted: data.muted });
|
|
17366
|
+
return data;
|
|
17367
|
+
} catch (error) {
|
|
17368
|
+
if (error instanceof AuthenticationError || error instanceof NetworkError) {
|
|
17369
|
+
throw error;
|
|
17370
|
+
}
|
|
17371
|
+
this.logger.error("Failed to get call mute status", { error });
|
|
17372
|
+
throw new NetworkError("Failed to get call mute status", { cause: error });
|
|
17373
|
+
}
|
|
17374
|
+
}
|
|
17185
17375
|
};
|
|
17186
17376
|
|
|
17187
17377
|
// src/conversation/Conversation.ts
|
|
17188
17378
|
var Conversation = class extends EventEmitter {
|
|
17189
|
-
constructor(id, options, authManager,
|
|
17379
|
+
constructor(id, options, authManager, urls) {
|
|
17190
17380
|
super();
|
|
17191
17381
|
this.logger = getLogger(["angany", "sdk", "conversation"]);
|
|
17192
17382
|
this.state = "idle";
|
|
@@ -17195,12 +17385,17 @@ var Conversation = class extends EventEmitter {
|
|
|
17195
17385
|
this.id = id;
|
|
17196
17386
|
this.options = options;
|
|
17197
17387
|
this.authManager = authManager;
|
|
17198
|
-
this.
|
|
17388
|
+
this.urls = urls;
|
|
17389
|
+
const sseUrl = urls.sseUrl || urls.apiUrl;
|
|
17199
17390
|
this.sipManager = new SipManager();
|
|
17200
|
-
this.transcriptionService = new TranscriptionService(
|
|
17201
|
-
this.apiService = new ApiService(apiUrl);
|
|
17391
|
+
this.transcriptionService = new TranscriptionService(sseUrl);
|
|
17392
|
+
this.apiService = new ApiService(urls.apiUrl);
|
|
17202
17393
|
this.logger = this.logger.with({ conversationId: id, resource: options.resource });
|
|
17203
|
-
this.logger.debug("Conversation created"
|
|
17394
|
+
this.logger.debug("Conversation created", {
|
|
17395
|
+
apiUrl: urls.apiUrl,
|
|
17396
|
+
sseUrl,
|
|
17397
|
+
sipUrl: urls.sipUrl || "will be derived"
|
|
17398
|
+
});
|
|
17204
17399
|
}
|
|
17205
17400
|
/**
|
|
17206
17401
|
* Initialize and start the conversation
|
|
@@ -17297,8 +17492,12 @@ var Conversation = class extends EventEmitter {
|
|
|
17297
17492
|
if (this.ephemeralCredentials.sip.realm) {
|
|
17298
17493
|
sipConfig.realm = this.ephemeralCredentials.sip.realm;
|
|
17299
17494
|
}
|
|
17300
|
-
if (this.
|
|
17495
|
+
if (this.urls.sipUrl) {
|
|
17496
|
+
sipConfig.websocketUrl = this.urls.sipUrl;
|
|
17497
|
+
this.logger.debug("Using configured SIP URL", { url: sipConfig.websocketUrl });
|
|
17498
|
+
} else if (this.ephemeralCredentials.sip.websocketUrl) {
|
|
17301
17499
|
sipConfig.websocketUrl = this.ephemeralCredentials.sip.websocketUrl;
|
|
17500
|
+
this.logger.debug("Using platform-provided WebSocket URL", { url: sipConfig.websocketUrl });
|
|
17302
17501
|
} else if (this.ephemeralCredentials.sip.uris && this.ephemeralCredentials.sip.uris.length > 0) {
|
|
17303
17502
|
const wssUris = this.ephemeralCredentials.sip.uris.filter(
|
|
17304
17503
|
(uri) => uri.includes("transport=wss")
|
|
@@ -17309,7 +17508,7 @@ var Conversation = class extends EventEmitter {
|
|
|
17309
17508
|
const wsUri = publicUri || wssUris[0];
|
|
17310
17509
|
if (wsUri) {
|
|
17311
17510
|
this.logger.debug("Selected SIP URI", { uri: wsUri, isPublic: !!publicUri });
|
|
17312
|
-
const apiDomain = new URL(this.apiUrl).hostname;
|
|
17511
|
+
const apiDomain = new URL(this.urls.apiUrl).hostname;
|
|
17313
17512
|
sipConfig.websocketUrl = `wss://${apiDomain}/api/webrtc/`;
|
|
17314
17513
|
this.logger.debug("Derived WebSocket URL from API domain", {
|
|
17315
17514
|
apiDomain,
|
|
@@ -17317,15 +17516,18 @@ var Conversation = class extends EventEmitter {
|
|
|
17317
17516
|
});
|
|
17318
17517
|
}
|
|
17319
17518
|
}
|
|
17320
|
-
if (sipConfig.websocketUrl) {
|
|
17519
|
+
if (sipConfig.websocketUrl && !this.urls.sipUrl) {
|
|
17321
17520
|
this.logger.debug("Original WebSocket URL", { url: sipConfig.websocketUrl });
|
|
17322
17521
|
const privateIpPattern = /wss?:\/\/(192\.168\.|10\.|172\.(1[6-9]|2[0-9]|3[01])\.)/;
|
|
17323
17522
|
if (privateIpPattern.test(sipConfig.websocketUrl)) {
|
|
17324
17523
|
this.logger.warn("Detected private IP in WebSocket URL, replacing with API domain");
|
|
17325
|
-
const apiDomain = new URL(this.apiUrl).hostname;
|
|
17524
|
+
const apiDomain = new URL(this.urls.apiUrl).hostname;
|
|
17326
17525
|
const originalUrl = sipConfig.websocketUrl;
|
|
17327
17526
|
const correctedUrl = `wss://${apiDomain}/api/webrtc/`;
|
|
17328
|
-
this.logger.debug("Extracted API domain", {
|
|
17527
|
+
this.logger.debug("Extracted API domain", {
|
|
17528
|
+
domain: apiDomain,
|
|
17529
|
+
apiUrl: this.urls.apiUrl
|
|
17530
|
+
});
|
|
17329
17531
|
sipConfig.websocketUrl = correctedUrl;
|
|
17330
17532
|
this.logger.info("Corrected WebSocket URL", {
|
|
17331
17533
|
original: originalUrl,
|
|
@@ -17397,11 +17599,11 @@ var Conversation = class extends EventEmitter {
|
|
|
17397
17599
|
const sendVoiceOptions = {
|
|
17398
17600
|
text,
|
|
17399
17601
|
callId: this.callId,
|
|
17400
|
-
|
|
17602
|
+
interruptsConversation: options?.interruptsConversation,
|
|
17603
|
+
queueWhenSpeaking: options?.queueWhenSpeaking,
|
|
17604
|
+
muteAgent: options?.muteAgent,
|
|
17605
|
+
voiceSettings: options?.voiceSettings
|
|
17401
17606
|
};
|
|
17402
|
-
if (options?.interrupt !== void 0) {
|
|
17403
|
-
sendVoiceOptions.interrupt = options.interrupt;
|
|
17404
|
-
}
|
|
17405
17607
|
try {
|
|
17406
17608
|
await this.apiService.sendVoice(sendVoiceOptions, this.accessToken);
|
|
17407
17609
|
} catch (error) {
|
|
@@ -17488,6 +17690,118 @@ var Conversation = class extends EventEmitter {
|
|
|
17488
17690
|
isAgentMuted() {
|
|
17489
17691
|
return this.agentMuted;
|
|
17490
17692
|
}
|
|
17693
|
+
/**
|
|
17694
|
+
* Get the current call ID (available after connection)
|
|
17695
|
+
*/
|
|
17696
|
+
getCallId() {
|
|
17697
|
+
return this.callId;
|
|
17698
|
+
}
|
|
17699
|
+
/**
|
|
17700
|
+
* Mute the call via API (POST /calls/{call_id}/mute)
|
|
17701
|
+
*/
|
|
17702
|
+
async muteCall() {
|
|
17703
|
+
this.logger.debug("Muting call via API");
|
|
17704
|
+
if (!this.callId) {
|
|
17705
|
+
throw new ConversationError("No call ID available - call may not be connected yet");
|
|
17706
|
+
}
|
|
17707
|
+
if (!this.accessToken) {
|
|
17708
|
+
throw new AuthenticationError("No access token available");
|
|
17709
|
+
}
|
|
17710
|
+
try {
|
|
17711
|
+
await this.apiService.muteCall(this.callId, this.accessToken);
|
|
17712
|
+
this.logger.info("Call muted via API", { callId: this.callId });
|
|
17713
|
+
} catch (error) {
|
|
17714
|
+
if (error instanceof AuthenticationError) {
|
|
17715
|
+
this.logger.debug("API call failed with auth error, attempting token refresh");
|
|
17716
|
+
try {
|
|
17717
|
+
const freshToken = await this.authManager.getAccessToken();
|
|
17718
|
+
if (freshToken && freshToken !== this.accessToken) {
|
|
17719
|
+
this.accessToken = freshToken;
|
|
17720
|
+
this.logger.debug("Token refreshed, retrying mute call operation");
|
|
17721
|
+
await this.apiService.muteCall(this.callId, this.accessToken);
|
|
17722
|
+
return;
|
|
17723
|
+
}
|
|
17724
|
+
throw error;
|
|
17725
|
+
} catch (refreshError) {
|
|
17726
|
+
this.logger.error("Failed to refresh token for mute call operation", {
|
|
17727
|
+
error: refreshError
|
|
17728
|
+
});
|
|
17729
|
+
throw error;
|
|
17730
|
+
}
|
|
17731
|
+
}
|
|
17732
|
+
throw error;
|
|
17733
|
+
}
|
|
17734
|
+
}
|
|
17735
|
+
/**
|
|
17736
|
+
* Unmute the call via API (DELETE /calls/{call_id}/mute)
|
|
17737
|
+
*/
|
|
17738
|
+
async unmuteCall() {
|
|
17739
|
+
this.logger.debug("Unmuting call via API");
|
|
17740
|
+
if (!this.callId) {
|
|
17741
|
+
throw new ConversationError("No call ID available - call may not be connected yet");
|
|
17742
|
+
}
|
|
17743
|
+
if (!this.accessToken) {
|
|
17744
|
+
throw new AuthenticationError("No access token available");
|
|
17745
|
+
}
|
|
17746
|
+
try {
|
|
17747
|
+
await this.apiService.unmuteCall(this.callId, this.accessToken);
|
|
17748
|
+
this.logger.info("Call unmuted via API", { callId: this.callId });
|
|
17749
|
+
} catch (error) {
|
|
17750
|
+
if (error instanceof AuthenticationError) {
|
|
17751
|
+
this.logger.debug("API call failed with auth error, attempting token refresh");
|
|
17752
|
+
try {
|
|
17753
|
+
const freshToken = await this.authManager.getAccessToken();
|
|
17754
|
+
if (freshToken && freshToken !== this.accessToken) {
|
|
17755
|
+
this.accessToken = freshToken;
|
|
17756
|
+
this.logger.debug("Token refreshed, retrying unmute call operation");
|
|
17757
|
+
await this.apiService.unmuteCall(this.callId, this.accessToken);
|
|
17758
|
+
return;
|
|
17759
|
+
}
|
|
17760
|
+
throw error;
|
|
17761
|
+
} catch (refreshError) {
|
|
17762
|
+
this.logger.error("Failed to refresh token for unmute call operation", {
|
|
17763
|
+
error: refreshError
|
|
17764
|
+
});
|
|
17765
|
+
throw error;
|
|
17766
|
+
}
|
|
17767
|
+
}
|
|
17768
|
+
throw error;
|
|
17769
|
+
}
|
|
17770
|
+
}
|
|
17771
|
+
/**
|
|
17772
|
+
* Get call mute status via API (GET /calls/{call_id}/mute)
|
|
17773
|
+
*/
|
|
17774
|
+
async getCallMuteStatus() {
|
|
17775
|
+
this.logger.debug("Getting call mute status via API");
|
|
17776
|
+
if (!this.callId) {
|
|
17777
|
+
throw new ConversationError("No call ID available - call may not be connected yet");
|
|
17778
|
+
}
|
|
17779
|
+
if (!this.accessToken) {
|
|
17780
|
+
throw new AuthenticationError("No access token available");
|
|
17781
|
+
}
|
|
17782
|
+
try {
|
|
17783
|
+
return await this.apiService.getCallMuteStatus(this.callId, this.accessToken);
|
|
17784
|
+
} catch (error) {
|
|
17785
|
+
if (error instanceof AuthenticationError) {
|
|
17786
|
+
this.logger.debug("API call failed with auth error, attempting token refresh");
|
|
17787
|
+
try {
|
|
17788
|
+
const freshToken = await this.authManager.getAccessToken();
|
|
17789
|
+
if (freshToken && freshToken !== this.accessToken) {
|
|
17790
|
+
this.accessToken = freshToken;
|
|
17791
|
+
this.logger.debug("Token refreshed, retrying get call mute status operation");
|
|
17792
|
+
return await this.apiService.getCallMuteStatus(this.callId, this.accessToken);
|
|
17793
|
+
}
|
|
17794
|
+
throw error;
|
|
17795
|
+
} catch (refreshError) {
|
|
17796
|
+
this.logger.error("Failed to refresh token for get call mute status operation", {
|
|
17797
|
+
error: refreshError
|
|
17798
|
+
});
|
|
17799
|
+
throw error;
|
|
17800
|
+
}
|
|
17801
|
+
}
|
|
17802
|
+
throw error;
|
|
17803
|
+
}
|
|
17804
|
+
}
|
|
17491
17805
|
/**
|
|
17492
17806
|
* Get conversation status
|
|
17493
17807
|
*/
|
|
@@ -17643,12 +17957,24 @@ var Conversation = class extends EventEmitter {
|
|
|
17643
17957
|
speaker: event.speaker,
|
|
17644
17958
|
text: event.text,
|
|
17645
17959
|
timestamp: event.timestamp,
|
|
17646
|
-
isFinal: event.isFinal
|
|
17960
|
+
isFinal: event.isFinal,
|
|
17961
|
+
humanTurnId: event.humanTurnId,
|
|
17962
|
+
agentTurnId: event.agentTurnId,
|
|
17963
|
+
speakerId: event.speakerId
|
|
17647
17964
|
});
|
|
17648
17965
|
});
|
|
17649
17966
|
this.transcriptionService.on("callId", (callId) => {
|
|
17650
17967
|
this.logger.info("Received call ID", { callId });
|
|
17651
17968
|
this.callId = callId;
|
|
17969
|
+
this.emit("callId", callId);
|
|
17970
|
+
});
|
|
17971
|
+
this.transcriptionService.on("callStarted", (callId) => {
|
|
17972
|
+
this.logger.info("Call started", { callId });
|
|
17973
|
+
this.emit("callStarted", callId);
|
|
17974
|
+
});
|
|
17975
|
+
this.transcriptionService.on("callEnded", (callId) => {
|
|
17976
|
+
this.logger.info("Call ended", { callId });
|
|
17977
|
+
this.emit("callEnded", callId);
|
|
17652
17978
|
});
|
|
17653
17979
|
this.transcriptionService.on("error", (error) => {
|
|
17654
17980
|
this.logger.error("Transcription error", { error });
|
|
@@ -17706,8 +18032,12 @@ var AnganyVoice = class extends EventEmitter {
|
|
|
17706
18032
|
this.logger = getLogger(["angany", "sdk", "core"]);
|
|
17707
18033
|
this.conversations = /* @__PURE__ */ new Map();
|
|
17708
18034
|
this.config = config;
|
|
17709
|
-
this.auth = new AuthManager(config.apiUrl, config.apiUrl);
|
|
17710
|
-
this.logger.debug("AnganyVoice initialized", {
|
|
18035
|
+
this.auth = new AuthManager(config.apiUrl, config.issuer || config.apiUrl);
|
|
18036
|
+
this.logger.debug("AnganyVoice initialized", {
|
|
18037
|
+
apiUrl: config.apiUrl,
|
|
18038
|
+
sseUrl: config.sseUrl || config.apiUrl,
|
|
18039
|
+
sipUrl: config.sipUrl || "derived from apiUrl"
|
|
18040
|
+
});
|
|
17711
18041
|
}
|
|
17712
18042
|
/**
|
|
17713
18043
|
* Get the current configuration
|
|
@@ -17742,7 +18072,11 @@ var AnganyVoice = class extends EventEmitter {
|
|
|
17742
18072
|
...options
|
|
17743
18073
|
},
|
|
17744
18074
|
this.auth,
|
|
17745
|
-
|
|
18075
|
+
{
|
|
18076
|
+
apiUrl: this.config.apiUrl,
|
|
18077
|
+
sseUrl: this.config.sseUrl,
|
|
18078
|
+
sipUrl: this.config.sipUrl
|
|
18079
|
+
}
|
|
17746
18080
|
);
|
|
17747
18081
|
conversation.on("ended", () => {
|
|
17748
18082
|
this.conversations.delete(conversationId);
|
|
@@ -17795,7 +18129,7 @@ var AnganyVoice = class extends EventEmitter {
|
|
|
17795
18129
|
};
|
|
17796
18130
|
|
|
17797
18131
|
// src/version.ts
|
|
17798
|
-
var VERSION = "0.0.
|
|
18132
|
+
var VERSION = "0.0.2";
|
|
17799
18133
|
|
|
17800
18134
|
export { AnganyError, AnganyVoice, AuthManager, AuthenticationError, ConfigurationError, Conversation, ConversationError, ErrorCodes, EventEmitter, MediaError, NetworkError, PermissionError, ResourceError, VERSION, ValidationError, hasErrorCode, isAnganyError };
|
|
17801
18135
|
//# sourceMappingURL=index.js.map
|