UNPKG

agent-widget-sdk

Version:

JavaScript SDK for Sarvam Agent Widget APIs and WebSocket connections

1,185 lines (1,176 loc) 41.6 kB
'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); class SDKError extends Error { constructor(message, code, details) { super(message); this.code = code; this.details = details; this.name = 'SDKError'; } } /** * Default logger implementation */ class DefaultLogger { constructor(enabled = true, level = 'info') { this.enabled = enabled; this.level = level; } shouldLog(level) { if (!this.enabled) return false; const levels = ['debug', 'info', 'warn', 'error']; return levels.indexOf(level) >= levels.indexOf(this.level); } debug(message, ...args) { if (this.shouldLog('debug')) { console.debug(`[SDK Debug] ${message}`, ...args); } } info(message, ...args) { if (this.shouldLog('info')) { console.info(`[SDK Info] ${message}`, ...args); } } warn(message, ...args) { if (this.shouldLog('warn')) { console.warn(`[SDK Warn] ${message}`, ...args); } } error(message, ...args) { if (this.shouldLog('error')) { console.error(`[SDK Error] ${message}`, ...args); } } } /** * Converts Float32Array to Int16Array for audio processing */ function convertFloat32toInt16(buffer) { const int16Buffer = new Int16Array(buffer.length); for (let i = 0; i < buffer.length; i++) { const s = Math.max(-1, Math.min(1, buffer[i])); int16Buffer[i] = s < 0 ? s * 0x8000 : s * 0x7fff; } return int16Buffer; } /** * Converts ArrayBuffer to base64 string */ function arrayBufferToBase64(buffer) { let binary = ''; const bytes = new Uint8Array(buffer); const len = bytes.byteLength; for (let i = 0; i < len; i++) { binary += String.fromCharCode(bytes[i]); } return btoa(binary); } /** * Calculates audio level from Float32Array */ function calculateAudioLevel(buffer) { let sum = 0; for (let i = 0; i < buffer.length; i++) { sum += buffer[i] * buffer[i]; } return Math.sqrt(sum / buffer.length); } /** * Generates a UUID v4 */ function generateUUID() { return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { const r = (Math.random() * 16) | 0; const v = c === 'x' ? r : (r & 0x3) | 0x8; return v.toString(16); }); } /** * Validates URL format */ function isValidUrl(url) { try { new URL(url); return true; } catch { return false; } } /** * Extracts clean URL path from full URL */ function extractCleanUrl(url) { try { const urlObj = new URL(url); return urlObj.pathname === '/' ? 'home' : urlObj.pathname.substring(1); } catch { return 'home'; } } /** * Retry function with exponential backoff */ async function retry(fn, maxAttempts = 3, baseDelay = 1000) { let lastError; for (let attempt = 1; attempt <= maxAttempts; attempt++) { try { return await fn(); } catch (error) { lastError = error; if (attempt === maxAttempts) { throw lastError; } const delay = baseDelay * Math.pow(2, attempt - 1); await new Promise((resolve) => setTimeout(resolve, delay)); } } throw lastError; } /** * Debounce function */ function debounce(func, wait) { let timeout = null; return (...args) => { if (timeout) { clearTimeout(timeout); } timeout = setTimeout(() => { func(...args); }, wait); }; } /** * Throttle function */ function throttle(func, limit) { let inThrottle = false; return (...args) => { if (!inThrottle) { func(...args); inThrottle = true; setTimeout(() => (inThrottle = false), limit); } }; } /** * Deep clone object */ function deepClone(obj) { if (obj === null || typeof obj !== 'object') { return obj; } if (obj instanceof Date) { return new Date(obj.getTime()); } if (obj instanceof Array) { return obj.map((item) => deepClone(item)); } if (typeof obj === 'object') { const cloned = {}; for (const key in obj) { if (obj.hasOwnProperty(key)) { cloned[key] = deepClone(obj[key]); } } return cloned; } return obj; } /** * API Client for Sarvam Agent Widget APIs */ class ApiClient { constructor(apiUrl = 'https://agent-widget-sarvam.vercel.app', logger) { this.apiUrl = apiUrl.replace(/\/$/, ''); // Remove trailing slash this.logger = logger || new DefaultLogger(); } /** * Makes a request to the proxy API */ async proxyRequest(url, options = {}) { const { method = 'GET', headers = {}, body } = options; const proxyUrl = `${this.apiUrl}/api/proxy?url=${encodeURIComponent(url)}`; const requestOptions = { method, headers: { 'Content-Type': 'application/json', ...headers } }; if (body && (method === 'POST' || method === 'PUT')) { requestOptions.body = typeof body === 'string' ? body : JSON.stringify(body); } try { this.logger.debug(`Making proxy request to ${url}`, { method, headers }); const response = await retry(() => fetch(proxyUrl, requestOptions)); if (!response.ok) { const errorData = await response.json().catch(() => ({})); throw new SDKError(`Proxy request failed: ${response.status} ${response.statusText}`, 'PROXY_REQUEST_FAILED', { status: response.status, statusText: response.statusText, errorData }); } return response; } catch (error) { this.logger.error('Proxy request failed', error); if (error instanceof SDKError) { throw error; } throw new SDKError(`Network error during proxy request: ${error instanceof Error ? error.message : 'Unknown error'}`, 'NETWORK_ERROR', { originalError: error }); } } /** * Gets audio processor script */ async getAudioProcessor() { try { this.logger.debug('Fetching audio processor script'); const response = await retry(() => fetch(`${this.apiUrl}/api/audio-processor`)); if (!response.ok) { throw new SDKError(`Failed to fetch audio processor: ${response.status} ${response.statusText}`, 'AUDIO_PROCESSOR_FETCH_FAILED', { status: response.status, statusText: response.statusText }); } const script = await response.text(); this.logger.debug('Audio processor script fetched successfully'); return script; } catch (error) { this.logger.error('Failed to get audio processor', error); if (error instanceof SDKError) { throw error; } throw new SDKError(`Failed to fetch audio processor: ${error instanceof Error ? error.message : 'Unknown error'}`, 'AUDIO_PROCESSOR_ERROR', { originalError: error }); } } /** * Gets widget configuration */ async getWidgetConfig(appId) { try { this.logger.debug(`Fetching widget config for appId: ${appId}`); const response = await retry(() => fetch(`${this.apiUrl}/api/widget-config?appId=${encodeURIComponent(appId)}`)); if (!response.ok) { const errorData = await response.json().catch(() => ({})); throw new SDKError(`Failed to fetch widget config: ${response.status} ${response.statusText}`, 'WIDGET_CONFIG_FETCH_FAILED', { status: response.status, statusText: response.statusText, errorData }); } const data = await response.json(); this.logger.debug('Widget config fetched successfully', data); return data.config; } catch (error) { this.logger.error('Failed to get widget config', error); if (error instanceof SDKError) { throw error; } throw new SDKError(`Failed to fetch widget config: ${error instanceof Error ? error.message : 'Unknown error'}`, 'WIDGET_CONFIG_ERROR', { originalError: error }); } } /** * Creates widget configuration */ async createWidgetConfig(config) { try { this.logger.debug('Creating widget config', config); const response = await retry(() => fetch(`${this.apiUrl}/api/widget-config`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(config) })); if (!response.ok) { const errorData = await response.json().catch(() => ({})); throw new SDKError(`Failed to create widget config: ${response.status} ${response.statusText}`, 'WIDGET_CONFIG_CREATE_FAILED', { status: response.status, statusText: response.statusText, errorData }); } const data = await response.json(); this.logger.debug('Widget config created successfully', data); return data.config; } catch (error) { this.logger.error('Failed to create widget config', error); if (error instanceof SDKError) { throw error; } throw new SDKError(`Failed to create widget config: ${error instanceof Error ? error.message : 'Unknown error'}`, 'WIDGET_CONFIG_CREATE_ERROR', { originalError: error }); } } /** * Updates widget configuration */ async updateWidgetConfig(appId, updates) { try { this.logger.debug(`Updating widget config for appId: ${appId}`, updates); const response = await retry(() => fetch(`${this.apiUrl}/api/widget-config?appId=${encodeURIComponent(appId)}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(updates) })); if (!response.ok) { const errorData = await response.json().catch(() => ({})); throw new SDKError(`Failed to update widget config: ${response.status} ${response.statusText}`, 'WIDGET_CONFIG_UPDATE_FAILED', { status: response.status, statusText: response.statusText, errorData }); } const data = await response.json(); this.logger.debug('Widget config updated successfully', data); return data.config; } catch (error) { this.logger.error('Failed to update widget config', error); if (error instanceof SDKError) { throw error; } throw new SDKError(`Failed to update widget config: ${error instanceof Error ? error.message : 'Unknown error'}`, 'WIDGET_CONFIG_UPDATE_ERROR', { originalError: error }); } } /** * Fetches app configuration from Sarvam API */ async fetchAppConfig(appId, token) { const url = `https://apps-staging.sarvam.ai/api/app-authoring/orgs/sarvamai/workspaces/default/apps/${appId}?app_version=1&version_filter=specific&with_deployment_status=true`; try { this.logger.debug(`Fetching app config for appId: ${appId}`); const response = await this.proxyRequest(url, { method: 'GET', headers: { Accept: 'application/json', Authorization: `Bearer ${token}`, Origin: 'http://localhost:4000' } }); const config = await response.json(); this.logger.debug('App config fetched successfully', config); return config; } catch (error) { this.logger.error('Failed to fetch app config', error); if (error instanceof SDKError) { throw error; } throw new SDKError(`Failed to fetch app config: ${error instanceof Error ? error.message : 'Unknown error'}`, 'APP_CONFIG_FETCH_ERROR', { originalError: error }); } } } /** * WebSocket Client for Sarvam Agent Widget */ class WebSocketClient { constructor(logger) { this.ws = null; this.config = null; this.reconnectAttempts = 0; this.maxReconnectAttempts = 5; this.reconnectInterval = 3000; this.isManuallyDisconnected = false; this.webContentSent = false; this.currentWebContent = ''; this.logger = logger || new DefaultLogger(); this.handlers = {}; } /** * Sets event handlers for WebSocket events */ setHandlers(handlers) { this.handlers = handlers; } /** * Connects to WebSocket */ async connect(config) { if (this.ws && this.ws.readyState === WebSocket.OPEN) { this.logger.warn('WebSocket is already connected'); return; } this.config = config; this.isManuallyDisconnected = false; const wsUrl = this.createWebSocketUrl(config); try { this.logger.debug('Connecting to WebSocket', { url: wsUrl }); this.ws = new WebSocket(wsUrl); this.setupWebSocketHandlers(); // Wait for connection to open await new Promise((resolve, reject) => { if (!this.ws) { reject(new SDKError('WebSocket instance not available', 'WEBSOCKET_NOT_AVAILABLE')); return; } const onOpen = () => { this.logger.info('WebSocket connected successfully'); this.reconnectAttempts = 0; resolve(); }; const onError = (error) => { this.logger.error('WebSocket connection failed', error); reject(new SDKError('WebSocket connection failed', 'WEBSOCKET_CONNECTION_FAILED', { error })); }; this.ws.addEventListener('open', onOpen, { once: true }); this.ws.addEventListener('error', onError, { once: true }); }); } catch (error) { this.logger.error('Failed to connect to WebSocket', error); throw new SDKError(`Failed to connect to WebSocket: ${error instanceof Error ? error.message : 'Unknown error'}`, 'WEBSOCKET_CONNECTION_ERROR', { originalError: error }); } } /** * Disconnects from WebSocket */ disconnect() { this.isManuallyDisconnected = true; if (this.ws) { this.logger.debug('Disconnecting from WebSocket'); this.ws.close(); this.ws = null; } } /** * Sends a JSON message through WebSocket */ sendMessage(message) { if (!this.ws || this.ws.readyState !== WebSocket.OPEN) { this.logger.warn('WebSocket not connected, cannot send message'); return; } const messageStr = JSON.stringify(message); this.logger.debug('Sending WebSocket message', message); this.ws.send(messageStr); } /** * Sends start interaction message */ sendStartInteraction(webContent) { if (webContent) { this.currentWebContent = webContent; } const message = { request_id: generateUUID(), start_interaction: true, sent_at: Date.now() / 1000, events: [ { event_type: 'ClientStartInteractionEvent', origin: 'client', metrics: { start_time: Date.now() / 1000, end_time: Date.now() / 1000 } } ], configs: { agentic_variables: { web_content: this.currentWebContent } } }; this.sendMessage(message); this.webContentSent = true; this.logger.info('Sent start interaction message'); } /** * Sends end interaction message */ sendEndInteraction() { const message = { request_id: generateUUID(), end_interaction: true, sent_at: Date.now() / 1000, events: [ { event_type: 'ClientEndInteractionEvent', origin: 'client', metrics: { start_time: Date.now() / 1000, end_time: Date.now() / 1000 } } ] }; this.sendMessage(message); this.logger.info('Sent end interaction message'); } /** * Sends audio chunk */ sendAudioChunk(audioBuffer, onAudioLevel) { if (!this.ws || this.ws.readyState !== WebSocket.OPEN) { this.logger.warn('WebSocket not connected, cannot send audio'); return; } // Calculate audio level const level = calculateAudioLevel(audioBuffer); if (onAudioLevel) { onAudioLevel(level); } // Convert audio to base64 const audioBufferInt16 = convertFloat32toInt16(audioBuffer); const audioBase64 = arrayBufferToBase64(audioBufferInt16.buffer); const message = { request_id: generateUUID(), sent_at: Date.now() / 1000, audio: { data: audioBase64, encoding: 'audio/wav', sample_rate: 16000 } }; // Include web content only if not sent yet if (!this.webContentSent && this.currentWebContent) { message.configs = { agentic_variables: { web_content: this.currentWebContent } }; this.webContentSent = true; this.logger.debug('Sent web content with audio chunk'); } this.sendMessage(message); } /** * Sends content update */ sendContentUpdate(content) { this.currentWebContent = content; const message = { request_id: generateUUID(), sent_at: Date.now() / 1000, content_update: true, configs: { agentic_variables: { web_content: content } } }; this.sendMessage(message); this.webContentSent = true; this.logger.info('Sent content update message'); } /** * Gets current connection state */ getState() { return this.ws ? this.ws.readyState : WebSocket.CLOSED; } /** * Checks if WebSocket is connected */ isConnected() { return this.ws !== null && this.ws.readyState === WebSocket.OPEN; } /** * Sets up WebSocket event handlers */ setupWebSocketHandlers() { if (!this.ws) return; this.ws.onopen = () => { this.logger.info('WebSocket opened'); this.handlers.onOpen?.(); }; this.ws.onmessage = (event) => { this.logger.debug('WebSocket message received', event.data); try { const data = JSON.parse(event.data); // Handle audio responses if (data.audio && data.audio.data) { this.handlers.onAudioResponse?.(data.audio); } // Handle text responses if (data.text) { this.handlers.onTextResponse?.(data.text); } this.handlers.onMessage?.(event); } catch (error) { this.logger.error('Error parsing WebSocket message', error); } }; this.ws.onclose = (event) => { this.logger.info('WebSocket closed', { code: event.code, reason: event.reason, wasClean: event.wasClean }); this.handlers.onClose?.(); // Attempt to reconnect if not manually disconnected if (!this.isManuallyDisconnected && this.reconnectAttempts < this.maxReconnectAttempts) { this.attemptReconnect(); } }; this.ws.onerror = (error) => { this.logger.error('WebSocket error', error); this.handlers.onError?.(error); }; } /** * Attempts to reconnect to WebSocket */ async attemptReconnect() { if (!this.config || this.isManuallyDisconnected) return; this.reconnectAttempts++; this.logger.info(`Attempting to reconnect (${this.reconnectAttempts}/${this.maxReconnectAttempts})`); try { await new Promise((resolve) => setTimeout(resolve, this.reconnectInterval)); await this.connect(this.config); } catch (error) { this.logger.error('Reconnection failed', error); if (this.reconnectAttempts >= this.maxReconnectAttempts) { this.logger.error('Max reconnection attempts reached'); throw new SDKError('Max reconnection attempts reached', 'WEBSOCKET_MAX_RECONNECT_ATTEMPTS', { attempts: this.reconnectAttempts }); } } } /** * Creates WebSocket URL */ createWebSocketUrl(config) { const { orgId, appId, token, appVersion } = config; let url = `wss://apps-staging.sarvam.ai/api/app-runtime/channels/web-call-custom-auth/orgs/${orgId}/workspaces/default/apps/${appId}/debug-call?token=${token}`; if (appVersion) { url += `&app_version=${appVersion}`; } return url; } /** * Updates web content */ setWebContent(content) { this.currentWebContent = content; this.webContentSent = false; } /** * Gets current web content */ getWebContent() { return this.currentWebContent; } /** * Reset content sent flag */ resetContentSent() { this.webContentSent = false; } } /** * Audio Manager for handling audio processing and playback */ class AudioManager { constructor(logger) { this.audioSetup = null; this.isRecording = false; this.playbackContext = null; this.logger = logger || new DefaultLogger(); } /** * Sets up audio context and microphone access */ async setupAudio(processorScript) { try { this.logger.debug('Setting up audio context and microphone access'); // Get microphone access const stream = await navigator.mediaDevices.getUserMedia({ audio: { echoCancellation: true, autoGainControl: true, noiseSuppression: true, sampleRate: 16000 } }); // Set up audio context const context = new AudioContext({ sampleRate: 16000 }); // Load the audio worklet const blob = new Blob([processorScript], { type: 'application/javascript' }); const processorUrl = URL.createObjectURL(blob); try { await context.audioWorklet.addModule(processorUrl); } finally { URL.revokeObjectURL(processorUrl); } // Create audio nodes const source = context.createMediaStreamSource(stream); const workletNode = new AudioWorkletNode(context, 'audio-processor'); // Create a GainNode to prevent echo const gainNode = context.createGain(); gainNode.gain.value = 0; // Mute the microphone feedback // Connect nodes source.connect(workletNode); workletNode.connect(gainNode); gainNode.connect(context.destination); // Set up message handler for audio data workletNode.port.onmessage = (event) => { if (event.data.type === 'audio-data') { const audioData = event.data.audioData; this.onAudioData?.(audioData); } }; this.audioSetup = { stream, context, source, workletNode, gainNode }; this.logger.info('Audio setup completed successfully'); } catch (error) { this.logger.error('Failed to setup audio', error); throw new SDKError(`Failed to setup audio: ${error instanceof Error ? error.message : 'Unknown error'}`, 'AUDIO_SETUP_FAILED', { originalError: error }); } } /** * Starts recording audio */ startRecording(onAudioData, onAudioLevel) { if (!this.audioSetup) { throw new SDKError('Audio not setup. Call setupAudio() first.', 'AUDIO_NOT_SETUP'); } if (this.isRecording) { this.logger.warn('Already recording audio'); return; } this.logger.debug('Starting audio recording'); this.onAudioData = onAudioData; this.onAudioLevel = onAudioLevel; this.isRecording = true; // Resume audio context if suspended if (this.audioSetup.context.state === 'suspended') { this.audioSetup.context.resume(); } } /** * Stops recording audio */ stopRecording() { if (!this.isRecording) { this.logger.warn('Not currently recording'); return; } this.logger.debug('Stopping audio recording'); this.isRecording = false; this.onAudioData = undefined; this.onAudioLevel = undefined; // Suspend audio context to save resources if (this.audioSetup?.context) { this.audioSetup.context.suspend(); } } /** * Plays audio from base64 data */ async playAudio(audioData, onPlaybackStart, onPlaybackEnd) { try { this.logger.debug('Playing audio from base64 data'); // Create or reuse playback context if (!this.playbackContext) { this.playbackContext = new AudioContext(); } // Resume context if suspended if (this.playbackContext.state === 'suspended') { await this.playbackContext.resume(); } // Decode base64 audio data const binaryString = atob(audioData); const bytes = new Uint8Array(binaryString.length); for (let i = 0; i < binaryString.length; i++) { bytes[i] = binaryString.charCodeAt(i); } // Decode audio buffer const audioBuffer = await this.playbackContext.decodeAudioData(bytes.buffer); // Create and configure audio source const source = this.playbackContext.createBufferSource(); source.buffer = audioBuffer; source.connect(this.playbackContext.destination); // Set up event handlers onPlaybackStart?.(); source.onended = () => { this.logger.debug('Audio playback finished'); onPlaybackEnd?.(); }; // Start playback source.start(); this.logger.debug('Audio playback started'); } catch (error) { this.logger.error('Failed to play audio', error); onPlaybackEnd?.(); // Ensure callback is called even on error throw new SDKError(`Failed to play audio: ${error instanceof Error ? error.message : 'Unknown error'}`, 'AUDIO_PLAYBACK_FAILED', { originalError: error }); } } /** * Gets current audio level */ getAudioLevel() { if (!this.audioSetup || !this.isRecording) { return 0; } // This would typically be implemented with an AnalyserNode // For now, return a placeholder value return 0; } /** * Checks if audio is recording */ isAudioRecording() { return this.isRecording; } /** * Checks if audio is set up */ isAudioSetup() { return this.audioSetup !== null; } /** * Gets available audio devices */ async getAudioDevices() { try { const devices = await navigator.mediaDevices.enumerateDevices(); return devices.filter((device) => device.kind === 'audioinput'); } catch (error) { this.logger.error('Failed to get audio devices', error); throw new SDKError(`Failed to get audio devices: ${error instanceof Error ? error.message : 'Unknown error'}`, 'AUDIO_DEVICES_ERROR', { originalError: error }); } } /** * Sets the audio input device */ async setAudioInputDevice(deviceId) { try { this.logger.debug(`Setting audio input device: ${deviceId}`); // Stop current recording if active const wasRecording = this.isRecording; if (wasRecording) { this.stopRecording(); } // Clean up existing audio setup if (this.audioSetup) { this.audioSetup.stream.getTracks().forEach((track) => track.stop()); await this.audioSetup.context.close(); this.audioSetup = null; } // Get new media stream with specified device const stream = await navigator.mediaDevices.getUserMedia({ audio: { deviceId: { exact: deviceId }, echoCancellation: true, autoGainControl: true, noiseSuppression: true, sampleRate: 16000 } }); // Set up audio context with new stream const context = new AudioContext({ sampleRate: 16000 }); const source = context.createMediaStreamSource(stream); const workletNode = new AudioWorkletNode(context, 'audio-processor'); const gainNode = context.createGain(); gainNode.gain.value = 0; // Connect nodes source.connect(workletNode); workletNode.connect(gainNode); gainNode.connect(context.destination); // Set up message handler workletNode.port.onmessage = (event) => { if (event.data.type === 'audio-data') { const audioData = event.data.audioData; this.onAudioData?.(audioData); } }; this.audioSetup = { stream, context, source, workletNode, gainNode }; this.logger.info('Audio input device changed successfully'); } catch (error) { this.logger.error('Failed to set audio input device', error); throw new SDKError(`Failed to set audio input device: ${error instanceof Error ? error.message : 'Unknown error'}`, 'AUDIO_DEVICE_SET_FAILED', { originalError: error }); } } /** * Cleans up audio resources */ async cleanup() { this.logger.debug('Cleaning up audio resources'); // Stop recording if (this.isRecording) { this.stopRecording(); } // Clean up audio setup if (this.audioSetup) { this.audioSetup.stream.getTracks().forEach((track) => track.stop()); await this.audioSetup.context.close(); this.audioSetup = null; } // Clean up playback context if (this.playbackContext) { await this.playbackContext.close(); this.playbackContext = null; } this.logger.info('Audio cleanup completed'); } } /** * Main SDK class for Sarvam Agent Widget */ class SarvamWidgetSDK { constructor(config = {}) { this.initialized = false; this.config = { apiUrl: 'https://agent-widget-sarvam.vercel.app', enableLogging: true, autoReconnect: true, reconnectInterval: 3000, maxReconnectAttempts: 5, ...config }; this.logger = new DefaultLogger(this.config.enableLogging); this.apiClient = new ApiClient(this.config.apiUrl, this.logger); this.wsClient = new WebSocketClient(this.logger); this.audioManager = new AudioManager(this.logger); this.logger.info('Sarvam Widget SDK initialized', this.config); } /** * Initializes the SDK with audio processor */ async initialize() { if (this.initialized) { this.logger.warn('SDK already initialized'); return; } try { this.logger.info('Initializing SDK'); // Fetch audio processor script const audioProcessorScript = await this.apiClient.getAudioProcessor(); // Setup audio await this.audioManager.setupAudio(audioProcessorScript); this.initialized = true; this.logger.info('SDK initialized successfully'); } catch (error) { this.logger.error('Failed to initialize SDK', error); throw new SDKError(`Failed to initialize SDK: ${error instanceof Error ? error.message : 'Unknown error'}`, 'SDK_INITIALIZATION_FAILED', { originalError: error }); } } /** * Widget Configuration API */ async getWidgetConfig(appId) { return await this.apiClient.getWidgetConfig(appId); } async createWidgetConfig(config) { return await this.apiClient.createWidgetConfig(config); } async updateWidgetConfig(appId, updates) { return await this.apiClient.updateWidgetConfig(appId, updates); } /** * Proxy API for making requests */ async proxyRequest(url, options = {}) { return await this.apiClient.proxyRequest(url, options); } /** * Fetches app configuration */ async fetchAppConfig(appId, token) { return await this.apiClient.fetchAppConfig(appId, token); } /** * Voice Session Management */ async startVoiceSession(sessionConfig, handlers) { if (!this.initialized) { throw new SDKError('SDK not initialized. Call initialize() first.', 'SDK_NOT_INITIALIZED'); } try { this.logger.info('Starting voice session', sessionConfig); // Set WebSocket handlers this.wsClient.setHandlers(handlers); // Connect to WebSocket await this.wsClient.connect(sessionConfig); this.logger.info('Voice session started successfully'); } catch (error) { this.logger.error('Failed to start voice session', error); throw new SDKError(`Failed to start voice session: ${error instanceof Error ? error.message : 'Unknown error'}`, 'VOICE_SESSION_START_FAILED', { originalError: error }); } } /** * Ends voice session */ async endVoiceSession() { try { this.logger.info('Ending voice session'); // Send end interaction message this.wsClient.sendEndInteraction(); // Stop audio recording this.audioManager.stopRecording(); // Disconnect WebSocket this.wsClient.disconnect(); this.logger.info('Voice session ended successfully'); } catch (error) { this.logger.error('Failed to end voice session', error); throw new SDKError(`Failed to end voice session: ${error instanceof Error ? error.message : 'Unknown error'}`, 'VOICE_SESSION_END_FAILED', { originalError: error }); } } /** * Starts audio recording and interaction */ startInteraction(webContent) { if (!this.initialized) { throw new SDKError('SDK not initialized. Call initialize() first.', 'SDK_NOT_INITIALIZED'); } if (!this.wsClient.isConnected()) { throw new SDKError('WebSocket not connected. Call startVoiceSession() first.', 'WEBSOCKET_NOT_CONNECTED'); } try { this.logger.info('Starting interaction'); // Extract web content if URL provided let processedContent = webContent; if (webContent && isValidUrl(webContent)) { processedContent = extractCleanUrl(webContent); } // Send start interaction message this.wsClient.sendStartInteraction(processedContent); // Start audio recording this.audioManager.startRecording((audioData) => { this.wsClient.sendAudioChunk(audioData); }); this.logger.info('Interaction started successfully'); } catch (error) { this.logger.error('Failed to start interaction', error); throw new SDKError(`Failed to start interaction: ${error instanceof Error ? error.message : 'Unknown error'}`, 'INTERACTION_START_FAILED', { originalError: error }); } } /** * Stops audio recording and interaction */ stopInteraction() { try { this.logger.info('Stopping interaction'); // Stop audio recording this.audioManager.stopRecording(); // Send end interaction message this.wsClient.sendEndInteraction(); this.logger.info('Interaction stopped successfully'); } catch (error) { this.logger.error('Failed to stop interaction', error); throw new SDKError(`Failed to stop interaction: ${error instanceof Error ? error.message : 'Unknown error'}`, 'INTERACTION_STOP_FAILED', { originalError: error }); } } /** * Sends content update */ sendContentUpdate(content) { if (!this.wsClient.isConnected()) { throw new SDKError('WebSocket not connected. Call startVoiceSession() first.', 'WEBSOCKET_NOT_CONNECTED'); } this.wsClient.sendContentUpdate(content); } /** * Plays audio response */ async playAudioResponse(audioData, onStart, onEnd) { if (!this.initialized) { throw new SDKError('SDK not initialized. Call initialize() first.', 'SDK_NOT_INITIALIZED'); } await this.audioManager.playAudio(audioData, onStart, onEnd); } /** * Audio Device Management */ async getAudioDevices() { return await this.audioManager.getAudioDevices(); } async setAudioInputDevice(deviceId) { await this.audioManager.setAudioInputDevice(deviceId); } /** * Status Methods */ isInitialized() { return this.initialized; } isConnected() { return this.wsClient.isConnected(); } isRecording() { return this.audioManager.isAudioRecording(); } getConnectionState() { return this.wsClient.getState(); } /** * Cleanup resources */ async cleanup() { try { this.logger.info('Cleaning up SDK resources'); // Stop any ongoing interaction if (this.audioManager.isAudioRecording()) { this.stopInteraction(); } // Disconnect WebSocket if (this.wsClient.isConnected()) { this.wsClient.disconnect(); } // Cleanup audio resources await this.audioManager.cleanup(); this.initialized = false; this.logger.info('SDK cleanup completed'); } catch (error) { this.logger.error('Failed to cleanup SDK', error); throw new SDKError(`Failed to cleanup SDK: ${error instanceof Error ? error.message : 'Unknown error'}`, 'SDK_CLEANUP_FAILED', { originalError: error }); } } } exports.ApiClient = ApiClient; exports.AudioManager = AudioManager; exports.DefaultLogger = DefaultLogger; exports.SDKError = SDKError; exports.SarvamWidgetSDK = SarvamWidgetSDK; exports.WebSocketClient = WebSocketClient; exports.arrayBufferToBase64 = arrayBufferToBase64; exports.calculateAudioLevel = calculateAudioLevel; exports.convertFloat32toInt16 = convertFloat32toInt16; exports.debounce = debounce; exports.deepClone = deepClone; exports.default = SarvamWidgetSDK; exports.extractCleanUrl = extractCleanUrl; exports.generateUUID = generateUUID; exports.isValidUrl = isValidUrl; exports.retry = retry; exports.throttle = throttle; //# sourceMappingURL=index.js.map