ttp-agent-sdk 2.0.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.
@@ -0,0 +1,462 @@
1
+ /**
2
+ * Legacy AgentSDK - Backward Compatibility Layer
3
+ *
4
+ * This maintains the original AgentSDK API while using the new VoiceSDK internally.
5
+ * This ensures existing integrations continue to work without changes.
6
+ */
7
+
8
+ import { VoiceSDK } from '../index.js';
9
+
10
+ export class AgentSDK {
11
+ constructor(config) {
12
+ this.config = config;
13
+ this.voiceSDK = null;
14
+ this.isConnected = false;
15
+ this.isListening = false;
16
+
17
+ // Legacy callback properties
18
+ this.onConnected = () => {};
19
+ this.onDisconnected = () => {};
20
+ this.onError = (error) => console.error('SDK Error:', error);
21
+ this.onTranscript = (text) => {};
22
+ this.onAgentSpeaking = (isStart) => {};
23
+ }
24
+
25
+ async connect(signedUrl) {
26
+ try {
27
+ if (!signedUrl) {
28
+ throw new Error('signedUrl is required');
29
+ }
30
+
31
+ // Create VoiceSDK instance
32
+ this.voiceSDK = new VoiceSDK({
33
+ websocketUrl: signedUrl,
34
+ autoReconnect: false
35
+ });
36
+
37
+ // Set up event handlers to map to legacy callbacks
38
+ this.voiceSDK.on('connected', () => {
39
+ this.isConnected = true;
40
+ this.onConnected();
41
+ });
42
+
43
+ this.voiceSDK.on('disconnected', () => {
44
+ this.isConnected = false;
45
+ this.onDisconnected();
46
+ });
47
+
48
+ this.voiceSDK.on('error', (error) => {
49
+ this.onError(error);
50
+ });
51
+
52
+ this.voiceSDK.on('message', (message) => {
53
+ this.handleWebSocketMessage(message);
54
+ });
55
+
56
+ this.voiceSDK.on('recordingStarted', () => {
57
+ this.isListening = true;
58
+ });
59
+
60
+ this.voiceSDK.on('recordingStopped', () => {
61
+ this.isListening = false;
62
+ });
63
+
64
+ this.voiceSDK.on('playbackStarted', () => {
65
+ this.onAgentSpeaking(true);
66
+ });
67
+
68
+ this.voiceSDK.on('playbackStopped', () => {
69
+ this.onAgentSpeaking(false);
70
+ });
71
+
72
+ // Connect using VoiceSDK
73
+ await this.voiceSDK.connect();
74
+
75
+ } catch (error) {
76
+ this.onError(error);
77
+ throw error;
78
+ }
79
+ }
80
+
81
+ handleWebSocketMessage(message) {
82
+ // Map new message format to legacy format
83
+ switch (message.type) {
84
+ case 'connected':
85
+ console.log('Session started successfully');
86
+ break;
87
+
88
+ case 'user_transcript':
89
+ this.onTranscript(message.user_transcription || message.text);
90
+ break;
91
+
92
+ case 'agent_response':
93
+ // Handle agent text response
94
+ break;
95
+
96
+ case 'barge_in':
97
+ // Handle barge-in
98
+ break;
99
+
100
+ case 'stop_playing':
101
+ // Handle stop playing
102
+ break;
103
+
104
+ case 'error':
105
+ this.onError(new Error(message.message));
106
+ break;
107
+ }
108
+ }
109
+
110
+ async startListening() {
111
+ if (this.voiceSDK) {
112
+ await this.voiceSDK.startRecording();
113
+ }
114
+ }
115
+
116
+ stopListening() {
117
+ if (this.voiceSDK) {
118
+ this.voiceSDK.stopRecording();
119
+ }
120
+ }
121
+
122
+ updateVariables(variables) {
123
+ if (this.voiceSDK && this.isConnected) {
124
+ // Send variables update message
125
+ this.voiceSDK.webSocketManager.sendMessage({
126
+ t: 'update_variables',
127
+ variables
128
+ });
129
+ }
130
+ }
131
+
132
+ disconnect() {
133
+ if (this.voiceSDK) {
134
+ this.voiceSDK.destroy();
135
+ this.voiceSDK = null;
136
+ }
137
+ this.isConnected = false;
138
+ this.isListening = false;
139
+ }
140
+ }
141
+
142
+ // ============================================
143
+ // WIDGET - Pre-built UI using the SDK
144
+ // ============================================
145
+
146
+ export class AgentWidget {
147
+ constructor(config) {
148
+ this.config = config;
149
+ this.sdk = new AgentSDK();
150
+ this.isOpen = false;
151
+ this.isActive = false;
152
+
153
+ this.position = config.position || 'bottom-right';
154
+ this.primaryColor = config.primaryColor || '#4F46E5';
155
+
156
+ this.setupEventHandlers();
157
+ this.createWidget();
158
+ }
159
+
160
+ setupEventHandlers() {
161
+ this.sdk.onConnected = () => {
162
+ this.updateStatus('connected');
163
+ };
164
+
165
+ this.sdk.onDisconnected = () => {
166
+ this.updateStatus('disconnected');
167
+ this.isActive = false;
168
+ };
169
+
170
+ this.sdk.onError = (error) => {
171
+ this.showError(error.message);
172
+ };
173
+
174
+ this.sdk.onTranscript = (text) => {
175
+ this.addMessage('user', text);
176
+ };
177
+
178
+ this.sdk.onAgentSpeaking = (isStart) => {
179
+ if (isStart) {
180
+ this.showAgentThinking();
181
+ } else {
182
+ this.hideAgentThinking();
183
+ }
184
+ };
185
+ }
186
+
187
+ createWidget() {
188
+ const widget = document.createElement('div');
189
+ widget.id = 'agent-widget';
190
+ widget.innerHTML = `
191
+ <style>
192
+ #agent-widget {
193
+ position: fixed;
194
+ ${this.position.includes('bottom') ? 'bottom: 20px;' : 'top: 20px;'}
195
+ ${this.position.includes('right') ? 'right: 20px;' : 'left: 20px;'}
196
+ z-index: 9999;
197
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
198
+ }
199
+
200
+ #agent-button {
201
+ width: 60px;
202
+ height: 60px;
203
+ border-radius: 50%;
204
+ background: ${this.primaryColor};
205
+ border: none;
206
+ cursor: pointer;
207
+ box-shadow: 0 4px 12px rgba(0,0,0,0.15);
208
+ display: flex;
209
+ align-items: center;
210
+ justify-content: center;
211
+ transition: transform 0.2s;
212
+ }
213
+
214
+ #agent-button:hover {
215
+ transform: scale(1.1);
216
+ }
217
+
218
+ #agent-button svg {
219
+ width: 28px;
220
+ height: 28px;
221
+ fill: white;
222
+ }
223
+
224
+ #agent-panel {
225
+ display: none;
226
+ position: absolute;
227
+ bottom: 80px;
228
+ ${this.position.includes('right') ? 'right: 0;' : 'left: 0;'}
229
+ width: 350px;
230
+ height: 500px;
231
+ background: white;
232
+ border-radius: 12px;
233
+ box-shadow: 0 8px 32px rgba(0,0,0,0.2);
234
+ flex-direction: column;
235
+ overflow: hidden;
236
+ }
237
+
238
+ #agent-panel.open {
239
+ display: flex;
240
+ }
241
+
242
+ #agent-header {
243
+ background: ${this.primaryColor};
244
+ color: white;
245
+ padding: 16px;
246
+ display: flex;
247
+ justify-content: space-between;
248
+ align-items: center;
249
+ }
250
+
251
+ #agent-close {
252
+ background: none;
253
+ border: none;
254
+ color: white;
255
+ cursor: pointer;
256
+ font-size: 24px;
257
+ }
258
+
259
+ #agent-messages {
260
+ flex: 1;
261
+ overflow-y: auto;
262
+ padding: 16px;
263
+ display: flex;
264
+ flex-direction: column;
265
+ gap: 12px;
266
+ }
267
+
268
+ .message {
269
+ padding: 12px;
270
+ border-radius: 8px;
271
+ max-width: 80%;
272
+ }
273
+
274
+ .message.user {
275
+ background: #E5E7EB;
276
+ align-self: flex-end;
277
+ }
278
+
279
+ .message.agent {
280
+ background: #F3F4F6;
281
+ align-self: flex-start;
282
+ }
283
+
284
+ #agent-controls {
285
+ padding: 16px;
286
+ border-top: 1px solid #E5E7EB;
287
+ display: flex;
288
+ justify-content: center;
289
+ }
290
+
291
+ #agent-mic-button {
292
+ width: 60px;
293
+ height: 60px;
294
+ border-radius: 50%;
295
+ border: none;
296
+ background: ${this.primaryColor};
297
+ cursor: pointer;
298
+ display: flex;
299
+ align-items: center;
300
+ justify-content: center;
301
+ transition: all 0.2s;
302
+ }
303
+
304
+ #agent-mic-button.active {
305
+ background: #EF4444;
306
+ animation: pulse 1.5s infinite;
307
+ }
308
+
309
+ #agent-mic-button svg {
310
+ width: 28px;
311
+ height: 28px;
312
+ fill: white;
313
+ }
314
+
315
+ @keyframes pulse {
316
+ 0%, 100% { transform: scale(1); }
317
+ 50% { transform: scale(1.05); }
318
+ }
319
+
320
+ .agent-thinking {
321
+ font-style: italic;
322
+ color: #6B7280;
323
+ }
324
+
325
+ .error-message {
326
+ background: #FEE2E2;
327
+ color: #991B1B;
328
+ padding: 12px;
329
+ border-radius: 8px;
330
+ margin: 8px;
331
+ }
332
+ </style>
333
+
334
+ <button id="agent-button">
335
+ <svg viewBox="0 0 24 24">
336
+ <path d="M12 14c1.66 0 3-1.34 3-3V5c0-1.66-1.34-3-3-3S9 3.34 9 5v6c0 1.66 1.34 3 3 3z"/>
337
+ <path d="M17 11c0 2.76-2.24 5-5 5s-5-2.24-5-5H5c0 3.53 2.61 6.43 6 6.92V21h2v-3.08c3.39-.49 6-3.39 6-6.92h-2z"/>
338
+ </svg>
339
+ </button>
340
+
341
+ <div id="agent-panel">
342
+ <div id="agent-header">
343
+ <h3 style="margin: 0;">Voice Assistant</h3>
344
+ <button id="agent-close">&times;</button>
345
+ </div>
346
+
347
+ <div id="agent-messages"></div>
348
+
349
+ <div id="agent-controls">
350
+ <button id="agent-mic-button">
351
+ <svg viewBox="0 0 24 24">
352
+ <path d="M12 14c1.66 0 3-1.34 3-3V5c0-1.66-1.34-3-3-3S9 3.34 9 5v6c0 1.66 1.34 3 3 3z"/>
353
+ <path d="M17 11c0 2.76-2.24 5-5 5s-5-2.24-5-5H5c0 3.53 2.61 6.43 6 6.92V21h2v-3.08c3.39-.49 6-3.39 6-6.92h-2z"/>
354
+ </svg>
355
+ </button>
356
+ </div>
357
+ </div>
358
+ `;
359
+
360
+ document.body.appendChild(widget);
361
+
362
+ document.getElementById('agent-button').onclick = () => this.togglePanel();
363
+ document.getElementById('agent-close').onclick = () => this.togglePanel();
364
+ document.getElementById('agent-mic-button').onclick = () => this.toggleVoice();
365
+ }
366
+
367
+ togglePanel() {
368
+ this.isOpen = !this.isOpen;
369
+ const panel = document.getElementById('agent-panel');
370
+ panel.classList.toggle('open');
371
+ }
372
+
373
+ async toggleVoice() {
374
+ if (!this.isActive) {
375
+ try {
376
+ const signedUrl = await this.getSignedUrl();
377
+ await this.sdk.connect(signedUrl);
378
+ await this.sdk.startListening();
379
+ this.isActive = true;
380
+ document.getElementById('agent-mic-button').classList.add('active');
381
+ this.addMessage('system', 'Listening...');
382
+ } catch (error) {
383
+ console.error('Failed to start:', error);
384
+ this.showError(error.message);
385
+ }
386
+ } else {
387
+ this.sdk.stopListening();
388
+ this.sdk.disconnect();
389
+ this.isActive = false;
390
+ document.getElementById('agent-mic-button').classList.remove('active');
391
+ }
392
+ }
393
+
394
+ async getSignedUrl() {
395
+ if (typeof this.config.getSessionUrl === 'string') {
396
+ const response = await fetch(this.config.getSessionUrl, {
397
+ method: 'POST',
398
+ headers: {
399
+ 'Content-Type': 'application/json',
400
+ },
401
+ body: JSON.stringify({
402
+ agentId: this.config.agentId,
403
+ variables: this.config.variables || {}
404
+ })
405
+ });
406
+
407
+ if (!response.ok) {
408
+ throw new Error(`Failed to get session URL: ${response.statusText}`);
409
+ }
410
+
411
+ const data = await response.json();
412
+ return data.signedUrl || data.wsUrl || data.url;
413
+ }
414
+ else if (typeof this.config.getSessionUrl === 'function') {
415
+ const result = await this.config.getSessionUrl({
416
+ agentId: this.config.agentId,
417
+ variables: this.config.variables || {}
418
+ });
419
+
420
+ return typeof result === 'string' ? result : (result.signedUrl || result.wsUrl || result.url);
421
+ }
422
+ else {
423
+ throw new Error('getSessionUrl is required (URL string or function)');
424
+ }
425
+ }
426
+
427
+ addMessage(type, text) {
428
+ const messages = document.getElementById('agent-messages');
429
+ const message = document.createElement('div');
430
+ message.className = `message ${type}`;
431
+ message.textContent = text;
432
+ messages.appendChild(message);
433
+ messages.scrollTop = messages.scrollHeight;
434
+ }
435
+
436
+ showAgentThinking() {
437
+ const messages = document.getElementById('agent-messages');
438
+ const thinking = document.createElement('div');
439
+ thinking.className = 'message agent agent-thinking';
440
+ thinking.id = 'thinking-indicator';
441
+ thinking.textContent = 'Agent is speaking...';
442
+ messages.appendChild(thinking);
443
+ messages.scrollTop = messages.scrollHeight;
444
+ }
445
+
446
+ hideAgentThinking() {
447
+ const thinking = document.getElementById('thinking-indicator');
448
+ if (thinking) thinking.remove();
449
+ }
450
+
451
+ showError(message) {
452
+ const messages = document.getElementById('agent-messages');
453
+ const error = document.createElement('div');
454
+ error.className = 'error-message';
455
+ error.textContent = message;
456
+ messages.appendChild(error);
457
+ }
458
+
459
+ updateStatus(status) {
460
+ console.log('Widget status:', status);
461
+ }
462
+ }
@@ -0,0 +1,163 @@
1
+ /**
2
+ * VoiceButton - React component for voice interaction
3
+ */
4
+ import React, { useState, useEffect, useRef } from 'react';
5
+ import VoiceSDK from '../core/VoiceSDK.js';
6
+
7
+ const VoiceButton = ({
8
+ websocketUrl,
9
+ agentId, // Optional - for direct agent access (unsecured method)
10
+ voice = 'default',
11
+ language = 'en',
12
+ autoReconnect = true,
13
+ onConnected,
14
+ onDisconnected,
15
+ onRecordingStarted,
16
+ onRecordingStopped,
17
+ onPlaybackStarted,
18
+ onPlaybackStopped,
19
+ onError,
20
+ onMessage,
21
+ onBargeIn,
22
+ onStopPlaying,
23
+ className = '',
24
+ style = {},
25
+ children
26
+ }) => {
27
+ const [isConnected, setIsConnected] = useState(false);
28
+ const [isRecording, setIsRecording] = useState(false);
29
+ const [isPlaying, setIsPlaying] = useState(false);
30
+ const [connectionStatus, setConnectionStatus] = useState('Disconnected');
31
+
32
+ const voiceSDKRef = useRef(null);
33
+
34
+ // Initialize VoiceSDK
35
+ useEffect(() => {
36
+ console.log(`🎙️ VoiceButton: Creating VoiceSDK instance for ${websocketUrl}`);
37
+
38
+ // Clean up existing instance if any
39
+ if (voiceSDKRef.current) {
40
+ console.log(`🎙️ VoiceButton: Destroying existing VoiceSDK instance`);
41
+ voiceSDKRef.current.destroy();
42
+ voiceSDKRef.current = null;
43
+ }
44
+
45
+ const voiceSDK = new VoiceSDK({
46
+ websocketUrl,
47
+ agentId, // Pass through agentId if provided
48
+ voice,
49
+ language,
50
+ autoReconnect
51
+ });
52
+
53
+ // Setup event listeners
54
+ voiceSDK.on('connected', () => {
55
+ setIsConnected(true);
56
+ setConnectionStatus('Connected');
57
+ onConnected?.();
58
+ });
59
+
60
+ voiceSDK.on('disconnected', () => {
61
+ setIsConnected(false);
62
+ setConnectionStatus('Disconnected');
63
+ onDisconnected?.();
64
+ });
65
+
66
+ voiceSDK.on('recordingStarted', () => {
67
+ setIsRecording(true);
68
+ onRecordingStarted?.();
69
+ });
70
+
71
+ voiceSDK.on('recordingStopped', () => {
72
+ setIsRecording(false);
73
+ onRecordingStopped?.();
74
+ });
75
+
76
+ voiceSDK.on('playbackStarted', () => {
77
+ setIsPlaying(true);
78
+ onPlaybackStarted?.();
79
+ });
80
+
81
+ voiceSDK.on('playbackStopped', () => {
82
+ setIsPlaying(false);
83
+ onPlaybackStopped?.();
84
+ });
85
+
86
+ voiceSDK.on('error', (error) => {
87
+ onError?.(error);
88
+ });
89
+
90
+ voiceSDK.on('message', (message) => {
91
+ onMessage?.(message);
92
+ });
93
+
94
+ voiceSDK.on('bargeIn', (message) => {
95
+ onBargeIn?.(message);
96
+ });
97
+
98
+ voiceSDK.on('stopPlaying', (message) => {
99
+ onStopPlaying?.(message);
100
+ });
101
+
102
+ voiceSDKRef.current = voiceSDK;
103
+
104
+ // Auto-connect
105
+ voiceSDK.connect();
106
+
107
+ // Cleanup on unmount
108
+ return () => {
109
+ console.log(`🎙️ VoiceButton: Cleaning up VoiceSDK instance for ${websocketUrl}`);
110
+ if (voiceSDKRef.current) {
111
+ voiceSDKRef.current.destroy();
112
+ voiceSDKRef.current = null;
113
+ }
114
+ };
115
+ }, [websocketUrl, agentId, voice, language]);
116
+
117
+ // Handle button click
118
+ const handleClick = async () => {
119
+ if (!voiceSDKRef.current) return;
120
+
121
+ try {
122
+ await voiceSDKRef.current.toggleRecording();
123
+ } catch (error) {
124
+ console.error('Error toggling recording:', error);
125
+ }
126
+ };
127
+
128
+ // Default button content
129
+ const defaultContent = (
130
+ <div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
131
+ <div style={{ fontSize: '20px' }}>
132
+ {isRecording ? '🔴' : '🎤'}
133
+ </div>
134
+ <div>
135
+ {isRecording ? 'Stop Listening' : 'Start Listening'}
136
+ </div>
137
+ </div>
138
+ );
139
+
140
+ return (
141
+ <button
142
+ className={`voice-button ${isRecording ? 'recording' : ''} ${className}`}
143
+ style={{
144
+ padding: '12px 24px',
145
+ border: 'none',
146
+ borderRadius: '8px',
147
+ backgroundColor: isRecording ? '#dc3545' : '#007bff',
148
+ color: 'white',
149
+ cursor: 'pointer',
150
+ fontSize: '16px',
151
+ fontWeight: '500',
152
+ transition: 'all 0.2s ease',
153
+ ...style
154
+ }}
155
+ onClick={handleClick}
156
+ disabled={!isConnected}
157
+ >
158
+ {children || defaultContent}
159
+ </button>
160
+ );
161
+ };
162
+
163
+ export default VoiceButton;