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