@anam-ai/js-sdk
Version:
Client side JavaScript SDK for Anam AI
421 lines • 20.7 kB
JavaScript
"use strict";
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());
});
};
Object.defineProperty(exports, "__esModule", { value: true });
const buffer_1 = require("buffer");
const ClientError_1 = require("./lib/ClientError");
const ClientMetrics_1 = require("./lib/ClientMetrics");
const correlationId_1 = require("./lib/correlationId");
const modules_1 = require("./modules");
const types_1 = require("./types");
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_1.ClientError(configError, ClientError_1.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)) {
(0, ClientMetrics_1.setClientMetricsBaseUrl)(options.api.baseUrl || ClientMetrics_1.DEFAULT_ANAM_METRICS_BASE_URL, options.api.apiVersion || ClientMetrics_1.DEFAULT_ANAM_API_VERSION);
}
this.publicEventEmitter = new modules_1.PublicEventEmitter();
this.internalEventEmitter = new modules_1.InternalEventEmitter();
this.apiClient = new modules_1.CoreApiRestClient(sessionToken, options === null || options === void 0 ? void 0 : options.apiKey, options === null || options === void 0 ? void 0 : options.api);
this.messageHistoryClient = new modules_1.MessageHistoryClient(this.publicEventEmitter, this.internalEventEmitter);
}
decodeJwt(token) {
try {
const base64Payload = token.split('.')[1];
const payloadString = buffer_1.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;
(0, ClientMetrics_1.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;
(0, ClientMetrics_1.setMetricsContext)({
sessionId: this.sessionId,
});
try {
this.streamingClient = new modules_1.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) {
(0, ClientMetrics_1.setMetricsContext)({
sessionId: null,
});
throw new ClientError_1.ClientError('Failed to initialize streaming client', ClientError_1.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_1.ClientError('Session ID or streaming client is not available after starting session', ClientError_1.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 = (0, correlationId_1.generateCorrelationId)();
(0, ClientMetrics_1.setMetricsContext)({
attemptCorrelationId,
sessionId: null, // reset sessionId
});
(0, ClientMetrics_1.sendClientMetric)(ClientMetrics_1.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(types_1.AnamEvent.VIDEO_STREAM_STARTED, (videoStream) => {
streams.push(videoStream);
videoReceived = true;
if (audioReceived) {
resolve(streams);
}
});
this.publicEventEmitter.addListener(types_1.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 = (0, correlationId_1.generateCorrelationId)();
(0, ClientMetrics_1.setMetricsContext)({
attemptCorrelationId,
sessionId: null, // reset sessionId
});
(0, ClientMetrics_1.sendClientMetric)(ClientMetrics_1.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_1.ClientError) {
throw error;
}
throw new ClientError_1.ClientError('Failed to start session', ClientError_1.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(types_1.AnamEvent.CONNECTION_CLOSED, types_1.ConnectionClosedCode.NORMAL);
yield this.streamingClient.stopConnection();
this.streamingClient = null;
this.sessionId = null;
(0, ClientMetrics_1.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;
}
}
exports.default = AnamClient;
//# sourceMappingURL=AnamClient.js.map