agent-widget-sdk
Version:
JavaScript SDK for Sarvam Agent Widget APIs and WebSocket connections
1,185 lines (1,176 loc) • 41.6 kB
JavaScript
'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