UNPKG

@anam-ai/js-sdk

Version:

Client side JavaScript SDK for Anam AI

418 lines 20.4 kB
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; import { Buffer } from 'buffer'; import { ClientError, ErrorCode } from './lib/ClientError'; import { ClientMetricMeasurement, DEFAULT_ANAM_API_VERSION, DEFAULT_ANAM_METRICS_BASE_URL, sendClientMetric, setClientMetricsBaseUrl, setMetricsContext, } from './lib/ClientMetrics'; import { generateCorrelationId } from './lib/correlationId'; import { CoreApiRestClient, InternalEventEmitter, MessageHistoryClient, PublicEventEmitter, StreamingClient, } from './modules'; import { AnamEvent, ConnectionClosedCode, } from './types'; export default class AnamClient { constructor(sessionToken, personaConfig, options) { var _a, _b; this.inputAudioState = { isMuted: false }; this.sessionId = null; this.organizationId = null; this.streamingClient = null; this._isStreaming = false; const configError = this.validateClientConfig(sessionToken, personaConfig, options); if (configError) { throw new ClientError(configError, ErrorCode.CLIENT_ERROR_CODE_CONFIGURATION_ERROR, 400); } this.personaConfig = personaConfig; this.clientOptions = options; if (((_a = options === null || options === void 0 ? void 0 : options.api) === null || _a === void 0 ? void 0 : _a.baseUrl) || ((_b = options === null || options === void 0 ? void 0 : options.api) === null || _b === void 0 ? void 0 : _b.apiVersion)) { setClientMetricsBaseUrl(options.api.baseUrl || DEFAULT_ANAM_METRICS_BASE_URL, options.api.apiVersion || DEFAULT_ANAM_API_VERSION); } this.publicEventEmitter = new PublicEventEmitter(); this.internalEventEmitter = new InternalEventEmitter(); this.apiClient = new CoreApiRestClient(sessionToken, options === null || options === void 0 ? void 0 : options.apiKey, options === null || options === void 0 ? void 0 : options.api); this.messageHistoryClient = new MessageHistoryClient(this.publicEventEmitter, this.internalEventEmitter); } decodeJwt(token) { try { const base64Payload = token.split('.')[1]; const payloadString = Buffer.from(base64Payload, 'base64').toString('utf8'); const payload = JSON.parse(payloadString); return payload; } catch (error) { throw new Error('Invalid session token format'); } } validateClientConfig(sessionToken, personaConfig, options) { var _a; // Validate authentication configuration if (!sessionToken && !(options === null || options === void 0 ? void 0 : options.apiKey)) { return 'Either sessionToken or apiKey must be provided'; } if ((options === null || options === void 0 ? void 0 : options.apiKey) && sessionToken) { return 'Only one of sessionToken or apiKey should be used'; } // Validate persona configuration based on session token if (sessionToken) { const decodedToken = this.decodeJwt(sessionToken); this.organizationId = decodedToken.accountId; setMetricsContext({ organizationId: this.organizationId, }); const tokenType = (_a = decodedToken.type) === null || _a === void 0 ? void 0 : _a.toLowerCase(); if (tokenType === 'legacy') { return 'Legacy session tokens are no longer supported. Please define your persona when creating your session token. See https://docs.anam.ai/resources/migrating-legacy for more information.'; } else if (tokenType === 'ephemeral' || tokenType === 'stateful') { if (personaConfig) { return 'This session token already contains a persona configuration. Please remove the personaConfig parameter.'; } } } else { // No session token (using apiKey) if (!personaConfig) { return 'Missing persona config. Persona configuration must be provided when using apiKey'; } } // Validate voice detection configuration if (options === null || options === void 0 ? void 0 : options.voiceDetection) { if (options.disableInputAudio) { return 'Voice detection is disabled because input audio is disabled. Please set disableInputAudio to false to enable voice detection.'; } // End of speech sensitivity must be a number between 0 and 1 if (options.voiceDetection.endOfSpeechSensitivity !== undefined) { if (typeof options.voiceDetection.endOfSpeechSensitivity !== 'number') { return 'End of speech sensitivity must be a number'; } if (options.voiceDetection.endOfSpeechSensitivity < 0 || options.voiceDetection.endOfSpeechSensitivity > 1) { return 'End of speech sensitivity must be between 0 and 1'; } } } return undefined; } buildStartSessionOptionsForClient() { var _a; const sessionOptions = {}; if ((_a = this.clientOptions) === null || _a === void 0 ? void 0 : _a.voiceDetection) { sessionOptions.voiceDetection = this.clientOptions.voiceDetection; } // return undefined if no options are set if (Object.keys(sessionOptions).length === 0) { return undefined; } return sessionOptions; } startSession(userProvidedAudioStream) { return __awaiter(this, void 0, void 0, function* () { var _a, _b, _c, _d, _e, _f, _g, _h, _j; const config = this.personaConfig; // build session options from client options const sessionOptions = this.buildStartSessionOptionsForClient(); // start a new session const response = yield this.apiClient.startSession(config, sessionOptions); const { sessionId, clientConfig, engineHost, engineProtocol, signallingEndpoint, } = response; const { heartbeatIntervalSeconds, maxWsReconnectionAttempts, iceServers } = clientConfig; this.sessionId = sessionId; setMetricsContext({ sessionId: this.sessionId, }); try { this.streamingClient = new StreamingClient(sessionId, { engine: { baseUrl: `${engineProtocol}://${engineHost}`, }, signalling: { heartbeatIntervalSeconds, maxWsReconnectionAttempts, url: { baseUrl: engineHost, protocol: engineProtocol, signallingPath: signallingEndpoint, }, }, iceServers, inputAudio: { inputAudioState: this.inputAudioState, userProvidedMediaStream: ((_a = this.clientOptions) === null || _a === void 0 ? void 0 : _a.disableInputAudio) ? undefined : userProvidedAudioStream, audioDeviceId: (_b = this.clientOptions) === null || _b === void 0 ? void 0 : _b.audioDeviceId, disableInputAudio: (_c = this.clientOptions) === null || _c === void 0 ? void 0 : _c.disableInputAudio, }, metrics: { showPeerConnectionStatsReport: (_f = (_e = (_d = this.clientOptions) === null || _d === void 0 ? void 0 : _d.metrics) === null || _e === void 0 ? void 0 : _e.showPeerConnectionStatsReport) !== null && _f !== void 0 ? _f : false, peerConnectionStatsReportOutputFormat: (_j = (_h = (_g = this.clientOptions) === null || _g === void 0 ? void 0 : _g.metrics) === null || _h === void 0 ? void 0 : _h.peerConnectionStatsReportOutputFormat) !== null && _j !== void 0 ? _j : 'console', }, }, this.publicEventEmitter, this.internalEventEmitter); } catch (error) { setMetricsContext({ sessionId: null, }); throw new ClientError('Failed to initialize streaming client', ErrorCode.CLIENT_ERROR_CODE_SERVER_ERROR, 500, { cause: error instanceof Error ? error.message : String(error), sessionId, }); } return sessionId; }); } startSessionIfNeeded(userProvidedAudioStream) { return __awaiter(this, void 0, void 0, function* () { if (!this.sessionId || !this.streamingClient) { yield this.startSession(userProvidedAudioStream); if (!this.sessionId || !this.streamingClient) { throw new ClientError('Session ID or streaming client is not available after starting session', ErrorCode.CLIENT_ERROR_CODE_SERVER_ERROR, 500, { cause: 'Failed to initialize session properly', }); } } }); } stream(userProvidedAudioStream) { return __awaiter(this, void 0, void 0, function* () { var _a; if (this._isStreaming) { throw new Error('Already streaming'); } // generate a new ID here to track the attempt const attemptCorrelationId = generateCorrelationId(); setMetricsContext({ attemptCorrelationId, sessionId: null, // reset sessionId }); sendClientMetric(ClientMetricMeasurement.CLIENT_METRIC_MEASUREMENT_SESSION_ATTEMPT, '1'); if (((_a = this.clientOptions) === null || _a === void 0 ? void 0 : _a.disableInputAudio) && userProvidedAudioStream) { console.warn('AnamClient:Input audio is disabled. User provided audio stream will be ignored.'); } yield this.startSessionIfNeeded(userProvidedAudioStream); this._isStreaming = true; return new Promise((resolve) => { var _a; // set stream callbacks to capture the stream const streams = []; let videoReceived = false; let audioReceived = false; this.publicEventEmitter.addListener(AnamEvent.VIDEO_STREAM_STARTED, (videoStream) => { streams.push(videoStream); videoReceived = true; if (audioReceived) { resolve(streams); } }); this.publicEventEmitter.addListener(AnamEvent.AUDIO_STREAM_STARTED, (audioStream) => { streams.push(audioStream); audioReceived = true; if (videoReceived) { resolve(streams); } }); // start streaming (_a = this.streamingClient) === null || _a === void 0 ? void 0 : _a.startConnection(); }); }); } /** * @deprecated This method is deprecated. Please use streamToVideoElement instead. */ streamToVideoAndAudioElements(videoElementId, audioElementId, userProvidedAudioStream) { return __awaiter(this, void 0, void 0, function* () { console.warn('AnamClient: streamToVideoAndAudioElements is deprecated. To avoid possible audio issues, please use streamToVideoElement instead.'); yield this.streamToVideoElement(videoElementId, userProvidedAudioStream); }); } streamToVideoElement(videoElementId, userProvidedAudioStream) { return __awaiter(this, void 0, void 0, function* () { var _a; // generate a new ID here to track the attempt const attemptCorrelationId = generateCorrelationId(); setMetricsContext({ attemptCorrelationId, sessionId: null, // reset sessionId }); sendClientMetric(ClientMetricMeasurement.CLIENT_METRIC_MEASUREMENT_SESSION_ATTEMPT, '1'); if (((_a = this.clientOptions) === null || _a === void 0 ? void 0 : _a.disableInputAudio) && userProvidedAudioStream) { console.warn('AnamClient:Input audio is disabled. User provided audio stream will be ignored.'); } try { yield this.startSessionIfNeeded(userProvidedAudioStream); } catch (error) { if (error instanceof ClientError) { throw error; } throw new ClientError('Failed to start session', ErrorCode.CLIENT_ERROR_CODE_SERVER_ERROR, 500, { cause: error instanceof Error ? error.message : String(error), sessionId: this.sessionId, }); } if (this._isStreaming) { throw new Error('Already streaming'); } this._isStreaming = true; if (!this.streamingClient) { throw new Error('Failed to stream: streaming client is not available'); } this.streamingClient.setMediaStreamTargetById(videoElementId); this.streamingClient.startConnection(); }); } /** * Send a talk command to make the persona speak the provided content. * @param content - The text content for the persona to speak * @throws Error if session is not started or not currently streaming */ talk(content) { return __awaiter(this, void 0, void 0, function* () { if (!this.streamingClient) { throw new Error('Failed to send talk command: session is not started. Have you called startSession?'); } if (!this._isStreaming) { throw new Error('Failed to send talk command: not currently streaming. Have you called stream?'); } yield this.streamingClient.sendTalkCommand(content); return; }); } /** * Send a raw data message through the WebRTC data channel. * @param message - The message string to send through the data channel * @throws Error if session is not started */ sendDataMessage(message) { if (this.streamingClient) { this.streamingClient.sendDataMessage(message); } else { throw new Error('Failed to send message: session is not started.'); } } /** * Send a user text message in the active streaming session. * @param content - The text message content to send * @throws Error if not currently streaming or session is not started */ sendUserMessage(content) { if (!this._isStreaming) { console.warn('AnamClient: Not currently streaming. User message will not be sent.'); throw new Error('Failed to send user message: not currently streaming'); } const sessionId = this.getActiveSessionId(); if (!sessionId) { throw new Error('Failed to send user message: no active session'); } const currentTimestamp = new Date().toISOString().replace('Z', ''); const body = JSON.stringify({ content, timestamp: currentTimestamp, session_id: sessionId, message_type: 'speech', }); this.sendDataMessage(body); } interruptPersona() { if (!this._isStreaming) { throw new Error('Failed to send interrupt command: not currently streaming'); } const sessionId = this.getActiveSessionId(); if (!sessionId) { throw new Error('Failed to send interrupt command: no active session'); } const body = JSON.stringify({ message_type: 'interrupt', session_id: sessionId, timestamp: new Date().toISOString(), // removing Z not needed }); this.sendDataMessage(body); } stopStreaming() { return __awaiter(this, void 0, void 0, function* () { if (this.streamingClient) { this.publicEventEmitter.emit(AnamEvent.CONNECTION_CLOSED, ConnectionClosedCode.NORMAL); yield this.streamingClient.stopConnection(); this.streamingClient = null; this.sessionId = null; setMetricsContext({ attemptCorrelationId: null, sessionId: null, organizationId: this.organizationId, }); this._isStreaming = false; } }); } isStreaming() { return this._isStreaming; } setPersonaConfig(personaConfig) { this.personaConfig = personaConfig; } getPersonaConfig() { return this.personaConfig; } getInputAudioState() { var _a; if ((_a = this.clientOptions) === null || _a === void 0 ? void 0 : _a.disableInputAudio) { console.warn('AnamClient: Audio state will not be used because input audio is disabled.'); } // if streaming client is available, make sure our state is up to date if (this.streamingClient) { this.inputAudioState = this.streamingClient.getInputAudioState(); } return this.inputAudioState; } muteInputAudio() { var _a, _b; if ((_a = this.clientOptions) === null || _a === void 0 ? void 0 : _a.disableInputAudio) { console.warn('AnamClient: Input audio is disabled. Muting input audio will have no effect.'); } if (this.streamingClient && !((_b = this.clientOptions) === null || _b === void 0 ? void 0 : _b.disableInputAudio)) { this.inputAudioState = this.streamingClient.muteInputAudio(); } else { this.inputAudioState = Object.assign(Object.assign({}, this.inputAudioState), { isMuted: true }); } return this.inputAudioState; } unmuteInputAudio() { var _a, _b; if ((_a = this.clientOptions) === null || _a === void 0 ? void 0 : _a.disableInputAudio) { console.warn('AnamClient: Input audio is disabled. Unmuting input audio will have no effect.'); } if (this.streamingClient && !((_b = this.clientOptions) === null || _b === void 0 ? void 0 : _b.disableInputAudio)) { this.inputAudioState = this.streamingClient.unmuteInputAudio(); } else { this.inputAudioState = Object.assign(Object.assign({}, this.inputAudioState), { isMuted: false }); } return this.inputAudioState; } createTalkMessageStream(correlationId) { if (!this.streamingClient) { throw new Error('Failed to start talk message stream: session is not started.'); } if (correlationId && correlationId.trim() === '') { throw new Error('Failed to start talk message stream: correlationId is empty'); } return this.streamingClient.startTalkMessageStream(correlationId); } /** * Event handling */ addListener(event, callback) { this.publicEventEmitter.addListener(event, callback); } removeListener(event, callback) { this.publicEventEmitter.removeListener(event, callback); } getActiveSessionId() { return this.sessionId; } } //# sourceMappingURL=AnamClient.js.map