@anam-ai/js-sdk
Version:
Client side JavaScript SDK for Anam AI
418 lines • 20.4 kB
JavaScript
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