ttp-agent-sdk 2.0.1 → 2.0.2

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.
@@ -1,390 +0,0 @@
1
- /**
2
- * VoiceSDK - Core voice interaction SDK
3
- * Handles WebSocket connection, audio recording, and audio playback
4
- */
5
- import EventEmitter from './EventEmitter.js';
6
- import WebSocketManagerV2 from './WebSocketManagerV2.js';
7
- import AudioRecorder from './AudioRecorder.js';
8
- import AudioPlayer from './AudioPlayer.js';
9
-
10
- export default class VoiceSDK extends EventEmitter {
11
- constructor(config = {}) {
12
- super();
13
-
14
- // Configuration
15
- this.config = {
16
- websocketUrl: config.websocketUrl || 'wss://speech.talktopc.com/ws/conv',
17
- agentId: config.agentId, // Optional - for direct agent access (unsecured method)
18
- appId: config.appId, // User's app ID for authentication
19
- ttpId: config.ttpId, // Optional - custom TTP ID (fallback if appId not provided)
20
- voice: config.voice || 'default',
21
- language: config.language || 'en',
22
- sampleRate: config.sampleRate || 16000,
23
- ...config
24
- };
25
-
26
- // State
27
- this.isConnected = false;
28
- this.isRecording = false;
29
- this.isPlaying = false;
30
- this.isDestroyed = false;
31
-
32
- // Components
33
- this.webSocketManager = new WebSocketManagerV2({
34
- ...this.config,
35
- autoReconnect: this.config.autoReconnect !== false // Default to true unless explicitly disabled
36
- });
37
- this.audioRecorder = new AudioRecorder(this.config);
38
- this.audioPlayer = new AudioPlayer(this.config);
39
-
40
- // Bind event handlers
41
- this.setupEventHandlers();
42
- }
43
-
44
- /**
45
- * Setup event handlers for all components
46
- */
47
- setupEventHandlers() {
48
- // WebSocket events
49
- this.webSocketManager.on('connected', () => {
50
- this.isConnected = true;
51
- this.sendHelloMessage();
52
- this.emit('connected');
53
- });
54
-
55
- this.webSocketManager.on('disconnected', () => {
56
- this.isConnected = false;
57
- this.emit('disconnected');
58
- });
59
-
60
- this.webSocketManager.on('error', (error) => {
61
- this.emit('error', error);
62
- });
63
-
64
- this.webSocketManager.on('message', (message) => {
65
- this.emit('message', message);
66
- });
67
-
68
- this.webSocketManager.on('binaryAudio', (audioData) => {
69
- this.audioPlayer.playAudio(audioData);
70
- });
71
-
72
- this.webSocketManager.on('bargeIn', (message) => {
73
- this.emit('bargeIn', message);
74
- });
75
-
76
- this.webSocketManager.on('stopPlaying', (message) => {
77
- this.emit('stopPlaying', message);
78
- // Immediately stop all audio playback
79
- this.audioPlayer.stopImmediate();
80
- });
81
-
82
- // Audio recorder events
83
- this.audioRecorder.on('recordingStarted', () => {
84
- this.isRecording = true;
85
- this.emit('recordingStarted');
86
- });
87
-
88
- this.audioRecorder.on('recordingStopped', () => {
89
- this.isRecording = false;
90
- this.emit('recordingStopped');
91
- });
92
-
93
- this.audioRecorder.on('audioData', (audioData) => {
94
- if (this.isConnected) {
95
- this.webSocketManager.sendBinary(audioData);
96
- }
97
- });
98
-
99
- // Audio player events
100
- this.audioPlayer.on('playbackStarted', () => {
101
- this.isPlaying = true;
102
- this.emit('playbackStarted');
103
- });
104
-
105
- this.audioPlayer.on('playbackStopped', () => {
106
- this.isPlaying = false;
107
- this.emit('playbackStopped');
108
- });
109
-
110
- this.audioPlayer.on('playbackError', (error) => {
111
- this.emit('playbackError', error);
112
- });
113
- }
114
-
115
- /**
116
- * Connect to the voice server
117
- */
118
- async connect() {
119
- if (this.isDestroyed) {
120
- return false; // Prevent connect after destroy
121
- }
122
-
123
- try {
124
- // Build WebSocket URL with query parameters if needed
125
- const wsUrl = this.buildWebSocketUrl();
126
- console.log('VoiceSDK: Using WebSocket URL:', wsUrl);
127
-
128
- // Update the WebSocket manager with the URL that includes query parameters
129
- this.webSocketManager.config.websocketUrl = wsUrl;
130
-
131
- await this.webSocketManager.connect();
132
- return true;
133
- } catch (error) {
134
- this.emit('error', error);
135
- return false;
136
- }
137
- }
138
-
139
- /**
140
- * Build WebSocket URL with query parameters for authentication
141
- */
142
- buildWebSocketUrl() {
143
- let url = this.config.websocketUrl;
144
- const params = new URLSearchParams();
145
-
146
- // Add agentId as query parameter if provided
147
- if (this.config.agentId) {
148
- params.append('agentId', this.config.agentId);
149
- console.log('VoiceSDK: Adding agentId to URL:', this.config.agentId);
150
- }
151
-
152
- // Add appId as query parameter if provided
153
- if (this.config.appId) {
154
- params.append('appId', this.config.appId);
155
- console.log('VoiceSDK: Adding appId to URL:', this.config.appId);
156
- }
157
-
158
- // Add other parameters if needed
159
- if (this.config.voice && this.config.voice !== 'default') {
160
- params.append('voice', this.config.voice);
161
- }
162
-
163
- if (this.config.language && this.config.language !== 'en') {
164
- params.append('language', this.config.language);
165
- }
166
-
167
- // Append query parameters to URL if any exist
168
- if (params.toString()) {
169
- const separator = url.includes('?') ? '&' : '?';
170
- url += separator + params.toString();
171
- }
172
-
173
- return url;
174
- }
175
-
176
- /**
177
- * Disconnect from the voice server
178
- */
179
- disconnect() {
180
- if (this.isDestroyed) {
181
- console.log(`🎙️ VoiceSDK: Disconnect called but already destroyed`);
182
- return; // Prevent disconnect after destroy
183
- }
184
- console.log(`🎙️ VoiceSDK: Disconnecting from voice server`);
185
- this.stopRecording();
186
- this.webSocketManager.disconnect();
187
- }
188
-
189
- /**
190
- * Reset reconnection attempts (useful for manual reconnection)
191
- */
192
- resetReconnectionAttempts() {
193
- if (this.isDestroyed) {
194
- return;
195
- }
196
- this.webSocketManager.resetReconnectionAttempts();
197
- }
198
-
199
- /**
200
- * Manually reconnect to the voice server
201
- */
202
- async reconnect() {
203
- if (this.isDestroyed) {
204
- return false;
205
- }
206
-
207
- this.disconnect();
208
- this.resetReconnectionAttempts();
209
- return await this.connect();
210
- }
211
-
212
- /**
213
- * Start voice recording and streaming
214
- */
215
- async startRecording() {
216
- if (!this.isConnected) {
217
- throw new Error('Not connected to voice server');
218
- }
219
-
220
- try {
221
- // Send start continuous mode message
222
- this.webSocketManager.sendMessage({
223
- t: 'start_continuous_mode',
224
- ttpId: this.generateTtpId(),
225
- voice: this.config.voice,
226
- language: this.config.language
227
- });
228
-
229
- // Start audio recording
230
- await this.audioRecorder.start();
231
- return true;
232
- } catch (error) {
233
- this.emit('error', error);
234
- return false;
235
- }
236
- }
237
-
238
- /**
239
- * Stop voice recording and streaming
240
- */
241
- async stopRecording() {
242
- if (!this.isRecording) {
243
- return;
244
- }
245
-
246
- try {
247
- // Send stop continuous mode message
248
- this.webSocketManager.sendMessage({
249
- t: 'stop_continuous_mode',
250
- ttpId: this.generateTtpId()
251
- });
252
-
253
- // Stop audio recording
254
- await this.audioRecorder.stop();
255
-
256
- // Stop audio playback immediately when stopping recording
257
- this.audioPlayer.stopImmediate();
258
-
259
- return true;
260
- } catch (error) {
261
- this.emit('error', error);
262
- return false;
263
- }
264
- }
265
-
266
- /**
267
- * Toggle recording state
268
- */
269
- async toggleRecording() {
270
- if (this.isRecording) {
271
- return await this.stopRecording();
272
- } else {
273
- return await this.startRecording();
274
- }
275
- }
276
-
277
- /**
278
- * Stop audio playback immediately (for barge-in scenarios)
279
- */
280
- stopAudioPlayback() {
281
- this.audioPlayer.stopImmediate();
282
- }
283
-
284
- /**
285
- * Handle barge-in (user starts speaking while audio is playing)
286
- */
287
- async handleBargeIn() {
288
- // Stop current audio playback immediately
289
- this.stopAudioPlayback();
290
-
291
- // If not already recording, start recording
292
- if (!this.isRecording) {
293
- await this.startRecording();
294
- }
295
- }
296
-
297
- /**
298
- * Get current connection status
299
- */
300
- getStatus() {
301
- return {
302
- isConnected: this.isConnected,
303
- isRecording: this.isRecording,
304
- isPlaying: this.isPlaying
305
- };
306
- }
307
-
308
- /**
309
- * Update configuration
310
- */
311
- updateConfig(newConfig) {
312
- this.config = { ...this.config, ...newConfig };
313
- }
314
-
315
- /**
316
- * Generate unique TTP ID
317
- */
318
- generateTtpId() {
319
- return 'sdk_' + Math.random().toString(36).substr(2, 9) + '_' + Date.now();
320
- }
321
-
322
- /**
323
- * Send hello message with appropriate authentication
324
- */
325
- sendHelloMessage() {
326
- if (!this.isConnected) {
327
- console.warn('VoiceSDK: Cannot send hello message - not connected');
328
- return;
329
- }
330
-
331
- const helloMessage = {
332
- t: "hello"
333
- };
334
-
335
- // Use app ID for authentication (preferred method)
336
- if (this.config.appId) {
337
- helloMessage.appId = this.config.appId;
338
- console.log('VoiceSDK: Sending hello message with appId (app-based authentication)');
339
- } else if (this.config.ttpId) {
340
- // Fallback to custom TTP ID if app ID not provided
341
- helloMessage.ttpId = this.config.ttpId;
342
- console.log('VoiceSDK: Sending hello message with custom TTP ID (fallback method)');
343
- } else {
344
- // Generate TTP ID as last resort
345
- helloMessage.ttpId = this.generateTtpId();
346
- console.log('VoiceSDK: Sending hello message with generated TTP ID (last resort)');
347
- }
348
-
349
- // Note: agentId is now sent as query parameter in WebSocket URL, not in hello message
350
-
351
- // Log authentication method for debugging
352
- if (this.config.appId) {
353
- console.log('VoiceSDK: Using app ID for authentication:', this.config.appId);
354
- } else if (this.config.ttpId) {
355
- console.log('VoiceSDK: Using custom TTP ID:', this.config.ttpId);
356
- } else {
357
- console.log('VoiceSDK: Using generated TTP ID:', helloMessage.ttpId);
358
- }
359
-
360
- try {
361
- this.webSocketManager.sendMessage(helloMessage);
362
- console.log('VoiceSDK: Hello message sent:', helloMessage);
363
- } catch (error) {
364
- console.error('VoiceSDK: Failed to send hello message:', error);
365
- this.emit('error', error);
366
- }
367
- }
368
-
369
- /**
370
- * Cleanup resources
371
- */
372
- destroy() {
373
- if (this.isDestroyed) {
374
- console.log(`🎙️ VoiceSDK: Destroy called but already destroyed`);
375
- return; // Prevent multiple destroy calls
376
- }
377
-
378
- console.log(`🎙️ VoiceSDK: Destroying VoiceSDK instance`);
379
-
380
- // Disconnect first, before setting isDestroyed
381
- this.disconnect();
382
-
383
- this.isDestroyed = true;
384
- this.audioRecorder.destroy();
385
- this.audioPlayer.destroy();
386
- this.removeAllListeners();
387
-
388
- console.log(`🎙️ VoiceSDK: VoiceSDK instance destroyed`);
389
- }
390
- }
@@ -1,218 +0,0 @@
1
- /**
2
- * WebSocketManager - Handles WebSocket connection and message routing
3
- */
4
- import EventEmitter from './EventEmitter.js';
5
- import connectionManager from './ConnectionManager.js';
6
-
7
- export default class WebSocketManager extends EventEmitter {
8
- constructor(config) {
9
- super();
10
- this.config = config;
11
- this.ws = null;
12
- this.isConnected = false;
13
- this.reconnectAttempts = 0;
14
- this.maxReconnectAttempts = config.autoReconnect !== false ? 3 : 0; // Disable auto-reconnect if explicitly set to false
15
- this.isReconnecting = false;
16
- this.isConnecting = false; // Track if we're currently trying to connect
17
- this.connectionId = null; // Unique ID for this connection attempt
18
- }
19
-
20
- /**
21
- * Connect to WebSocket
22
- */
23
- async connect() {
24
- return new Promise((resolve, reject) => {
25
- try {
26
- // Prevent multiple connections
27
- if (this.ws && (this.ws.readyState === WebSocket.CONNECTING || this.ws.readyState === WebSocket.OPEN)) {
28
- resolve();
29
- return;
30
- }
31
-
32
- // Prevent connection if already reconnecting
33
- if (this.isReconnecting) {
34
- resolve();
35
- return;
36
- }
37
-
38
- // Prevent connection if already connecting
39
- if (this.isConnecting) {
40
- resolve();
41
- return;
42
- }
43
-
44
- // Check if connection is allowed by global manager
45
- if (!connectionManager.isConnectionAllowed(this.config.websocketUrl)) {
46
- console.log(`🔌 WebSocketManager: Connection blocked by global manager for ${this.config.websocketUrl}`);
47
- resolve();
48
- return;
49
- }
50
-
51
- this.isConnecting = true;
52
- this.connectionId = Date.now() + '_' + Math.random().toString(36).substr(2, 9);
53
-
54
- // Register with global connection manager
55
- if (!connectionManager.registerConnection(this.config.websocketUrl, this.connectionId)) {
56
- console.log(`🔌 WebSocketManager: Connection registration failed for ${this.connectionId}`);
57
- this.isConnecting = false;
58
- resolve();
59
- return;
60
- }
61
-
62
- console.log(`🔌 WebSocketManager: Starting connection attempt ${this.connectionId}`);
63
- this.ws = new WebSocket(this.config.websocketUrl);
64
-
65
- this.ws.onopen = () => {
66
- console.log(`🔌 WebSocketManager: Connection successful ${this.connectionId}`);
67
- this.isConnected = true;
68
- this.reconnectAttempts = 0;
69
- this.isReconnecting = false;
70
- this.isConnecting = false;
71
- this.emit('connected');
72
- resolve();
73
- };
74
-
75
- this.ws.onmessage = (event) => {
76
- this.handleMessage(event);
77
- };
78
-
79
- this.ws.onclose = (event) => {
80
- console.log(`🔌 WebSocketManager: Connection closed ${this.connectionId} (Code: ${event.code})`);
81
- this.isConnected = false;
82
- this.isConnecting = false;
83
- this.emit('disconnected', event);
84
-
85
- // Attempt reconnection if not intentional and not already reconnecting
86
- if (event.code !== 1000 && this.reconnectAttempts < this.maxReconnectAttempts && !this.isReconnecting) {
87
- this.isReconnecting = true;
88
- this.reconnectAttempts++;
89
- console.log(`🔌 WebSocketManager: Attempting reconnection ${this.reconnectAttempts}/${this.maxReconnectAttempts}`);
90
- setTimeout(() => {
91
- this.isReconnecting = false;
92
- this.connect().catch(() => {
93
- // Ignore reconnection errors to prevent infinite loops
94
- });
95
- }, 1000 * this.reconnectAttempts);
96
- }
97
- };
98
-
99
- this.ws.onerror = (error) => {
100
- console.log(`🔌 WebSocketManager: Connection error ${this.connectionId}`, error);
101
- this.isConnecting = false;
102
- this.emit('error', error);
103
- reject(error);
104
- };
105
-
106
- } catch (error) {
107
- console.log(`🔌 WebSocketManager: Connection failed ${this.connectionId}`, error);
108
- this.isConnecting = false;
109
- reject(error);
110
- }
111
- });
112
- }
113
-
114
- /**
115
- * Disconnect from WebSocket
116
- */
117
- disconnect() {
118
- // Stop any reconnection attempts
119
- this.isReconnecting = false;
120
- this.reconnectAttempts = this.maxReconnectAttempts; // Prevent reconnection
121
-
122
- // Unregister from global connection manager
123
- if (this.connectionId) {
124
- connectionManager.unregisterConnection(this.config.websocketUrl, this.connectionId);
125
- }
126
-
127
- if (this.ws && this.ws.readyState === WebSocket.OPEN) {
128
- this.ws.close(1000, 'Intentional disconnect');
129
- }
130
- this.ws = null;
131
- this.isConnected = false;
132
- this.isConnecting = false;
133
- }
134
-
135
- /**
136
- * Reset reconnection attempts (useful for manual reconnection)
137
- */
138
- resetReconnectionAttempts() {
139
- this.reconnectAttempts = 0;
140
- this.isReconnecting = false;
141
- }
142
-
143
- /**
144
- * Clear all global connections (useful for testing)
145
- */
146
- static clearAllConnections() {
147
- connectionManager.clearAll();
148
- }
149
-
150
- /**
151
- * Send JSON message
152
- */
153
- sendMessage(message) {
154
- if (!this.isConnected || !this.ws) {
155
- throw new Error('WebSocket not connected');
156
- }
157
-
158
- this.ws.send(JSON.stringify(message));
159
- }
160
-
161
- /**
162
- * Send binary data
163
- */
164
- sendBinary(data) {
165
- if (!this.isConnected || !this.ws) {
166
- throw new Error('WebSocket not connected');
167
- }
168
-
169
- this.ws.send(data);
170
- }
171
-
172
- /**
173
- * Handle incoming WebSocket messages
174
- */
175
- handleMessage(event) {
176
- // Check if it's binary data first
177
- if (event.data instanceof ArrayBuffer) {
178
- this.emit('binaryAudio', event.data);
179
- return;
180
- } else if (event.data instanceof Blob) {
181
- event.data.arrayBuffer().then(arrayBuffer => {
182
- this.emit('binaryAudio', arrayBuffer);
183
- }).catch(err => {
184
- this.emit('error', err);
185
- });
186
- return;
187
- }
188
-
189
- // Handle JSON messages
190
- try {
191
- const message = JSON.parse(event.data);
192
-
193
- // Handle barge-in related messages
194
- if (message.t === 'barge_in_ack' || message.t === 'stop_sending') {
195
- this.emit('bargeIn', message);
196
- }
197
-
198
- // Handle stop playing message
199
- if (message.t === 'stop_playing') {
200
- this.emit('stopPlaying', message);
201
- }
202
-
203
- this.emit('message', message);
204
- } catch (error) {
205
- this.emit('error', error);
206
- }
207
- }
208
-
209
- /**
210
- * Get connection status
211
- */
212
- getStatus() {
213
- return {
214
- isConnected: this.isConnected,
215
- readyState: this.ws ? this.ws.readyState : WebSocket.CLOSED
216
- };
217
- }
218
- }