@trillet-ai/web-sdk
Version:
Trillet Web SDK for real-time audio communication with AI agents
307 lines (306 loc) • 13.1 kB
JavaScript
import { EventEmitter } from 'eventemitter3';
import { TrilletWebSDK } from './TrilletWebSDK.js';
import { TranscriptManager } from './TranscriptManager.js';
const TRILLET_API_URL = {
CONNECT: 'https://api.trillet.ai/v1/api/web-call',
PUBLIC_CONNECT: 'https://api.trillet.ai/v1/api/web-call/public',
};
const decoder = new TextDecoder();
// Browser compatibility check
const isBrowser = typeof window !== 'undefined';
const isSecureContext = isBrowser && window.isSecureContext;
export class TrilletAgent extends EventEmitter {
constructor(config) {
super();
this.sdkToken = null;
// Validate environment
if (!isBrowser) {
throw new Error('Trillet SDK must be used in a browser environment');
}
if (!isSecureContext) {
throw new Error('Trillet SDK requires a secure context (HTTPS or localhost)');
}
if (!config.agentId) {
throw new Error('agentId is required');
}
this.config = {
...config,
mode: config.mode || 'voice', // Default to voice mode
};
this.sdk = new TrilletWebSDK();
this.transcriptManager = new TranscriptManager();
this.setupEventForwarding();
}
setupEventForwarding() {
// Forward all SDK events to agent listeners
this.sdk.on('disconnected', () => this.emit('disconnected'));
this.sdk.on('error', (error) => this.emit('error', error));
this.sdk.on('status', (status) => this.emit('status', status));
this.sdk.on('metadata', (metadata) => this.emit('metadata', metadata));
this.sdk.on('assistantStartedSpeaking', () => this.emit('assistantStartedSpeaking'));
this.sdk.on('assistantStoppedSpeaking', () => this.emit('assistantStoppedSpeaking'));
this.sdk.on('audioData', (data) => this.emit('audioData', data));
// Handle incoming tool usage from the agent
this.sdk.on('toolStarted', (toolCall) => {
this.transcriptManager.addToolStarted(toolCall);
this.emit('toolStarted', toolCall);
this.emit('transcriptUpdate', this.transcriptManager.getTranscripts());
});
this.sdk.on('toolCompleted', (toolResult) => {
this.transcriptManager.addToolCompleted(toolResult);
this.emit('toolCompleted', toolResult);
this.emit('transcriptUpdate', this.transcriptManager.getTranscripts());
});
this.sdk.on('transcript', (transcriptData, participant) => {
const participantId = participant.identity;
const isAgent = participantId.startsWith('agent');
const updated = this.transcriptManager.handleTranscript(participantId, isAgent, transcriptData);
if (updated) {
this.emit('transcriptUpdate', this.transcriptManager.getTranscripts());
}
});
// Handle incoming text messages from the agent
this.sdk.on('message', (message) => {
var _a, _b;
const agentParticipant = Array.from((_b = (_a = this.sdk.getRoom()) === null || _a === void 0 ? void 0 : _a.remoteParticipants.values()) !== null && _b !== void 0 ? _b : []).find((p) => p.identity.startsWith('agent'));
this.transcriptManager.addMessage('assistant', message.text, agentParticipant === null || agentParticipant === void 0 ? void 0 : agentParticipant.identity);
this.emit('message', message);
this.emit('transcriptUpdate', this.transcriptManager.getTranscripts());
});
}
/**
* Set up default RPC handlers for common agent interactions
* This should be called after connection is established
*/
setupDefaultRpcHandlers() {
// Register handler for receiving chat messages from agent
this.registerRpcMethod('receiveChatMessage', async (data) => {
var _a, _b, _c;
const messageText = typeof data.payload === 'string' ? data.payload : ((_a = data.payload) === null || _a === void 0 ? void 0 : _a.text) || '';
if (messageText) {
const agentParticipant = Array.from((_c = (_b = this.sdk.getRoom()) === null || _b === void 0 ? void 0 : _b.remoteParticipants.values()) !== null && _c !== void 0 ? _c : []).find((p) => p.identity.startsWith('agent'));
this.transcriptManager.addMessage('assistant', messageText, agentParticipant === null || agentParticipant === void 0 ? void 0 : agentParticipant.identity);
this.emit('message', { text: messageText });
this.emit('transcriptUpdate', this.transcriptManager.getTranscripts());
}
return 'Message received';
});
// Register handler for agent status updates
this.registerRpcMethod('agentStatus', async (data) => {
this.emit('status', data.payload);
return 'Status received';
});
}
async startCall() {
try {
// 2. Initialize call with API KEY
const response = await fetch(TRILLET_API_URL.CONNECT, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
'X-API-KEY': this.config.apiKey,
},
body: JSON.stringify({
call_agent_id: this.config.agentId,
mode: this.config.mode,
dynamic_variables: this.config.variables || {},
callback_url: this.config.callbackUrl,
}),
});
if (!response.ok) {
let errorMessage;
try {
const errorData = await response.json();
errorMessage = errorData.message || errorData.error || 'Unknown error';
}
catch (_a) {
errorMessage = await response.text();
}
throw new Error(`API Error: ${errorMessage}`);
}
const data = await response.json();
if (!data.token || !data.roomName) {
throw new Error('Invalid response from server: missing token or room name');
}
// Initialize call with LiveKit token
await this.sdk.initializeCall({
token: data.token,
wsUrl: 'wss://trillet-ai-xdx0dw5r.livekit.cloud',
mode: this.config.mode,
audioSettings: {
sampleRate: 48000,
},
features: {
enableRawAudio: this.config.mode === 'voice',
},
});
// Enable audio playback for voice mode
if (this.config.mode === 'voice') {
await this.sdk.enableAudioPlayback();
}
// Set up default RPC handlers for text communication
this.setupDefaultRpcHandlers();
// Emit additional connection details
this.emit('connected', {
callId: data.callId,
roomName: data.roomName,
agent: data.agent,
});
}
catch (error) {
console.error('Trillet SDK Error:', error);
this.emit('error', error instanceof Error ? error : new Error(String(error)));
throw error;
}
}
async startPublicCall() {
// 2. Initialize public call
const response = await fetch(TRILLET_API_URL.PUBLIC_CONNECT, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
'x-workspace-id': this.config.workspaceId,
},
body: JSON.stringify({
call_agent_id: this.config.agentId,
mode: this.config.mode,
dynamic_variables: this.config.variables || {},
callback_url: this.config.callbackUrl,
}),
});
if (!response.ok) {
let errorMessage;
try {
const errorData = await response.json();
errorMessage = errorData.message || errorData.error || 'Unknown error';
}
catch (_a) {
errorMessage = await response.text();
}
throw new Error(`API Error: ${errorMessage}`);
}
const data = await response.json();
if (!data.token || !data.roomName) {
throw new Error('Invalid response from server: missing token or room name');
}
// Initialize call with LiveKit token
await this.sdk.initializeCall({
token: data.token,
wsUrl: 'wss://trillet-ai-xdx0dw5r.livekit.cloud',
mode: this.config.mode,
audioSettings: {
sampleRate: 48000,
},
features: {
enableRawAudio: this.config.mode === 'voice',
},
});
// Enable audio playback for voice mode
if (this.config.mode === 'voice') {
await this.sdk.enableAudioPlayback();
}
// Set up default RPC handlers for text communication
this.setupDefaultRpcHandlers();
// Emit additional connection details
this.emit('connected', {
callId: data.callId,
roomName: data.roomName,
agent: data.agent,
});
}
endCall() {
this.sdk.endCall();
}
toggleMicrophone(enabled) {
this.sdk.toggleMicrophone(enabled);
}
get isAssistantSpeaking() {
return this.sdk.isAssistantSpeaking;
}
getTranscripts() {
return this.transcriptManager.getTranscripts();
}
getCurrentTranscript() {
return this.transcriptManager.getCurrentTranscript();
}
/**
* Register an RPC method handler for receiving calls from the agent
* @param method - The RPC method name to handle
* @param handler - The function to handle the RPC call
*/
registerRpcMethod(method, handler) {
const room = this.sdk.getRoom();
if (!room) {
console.warn('Cannot register RPC method: not connected to room');
return;
}
room.localParticipant.registerRpcMethod(method, async (data) => {
try {
const result = await handler(data);
return result;
}
catch (error) {
console.error(`RPC method ${method} error:`, error);
throw error;
}
});
}
/**
* Send an RPC call to the agent
* @param method - The RPC method name
* @param payload - The data to send
* @param responseTimeout - Timeout in milliseconds (default: 30000)
*/
async sendRpcMessage(method, payload, responseTimeout = 30000) {
const room = this.sdk.getRoom();
if (!room || !this.sdk.isConnected) {
throw new Error('Not connected. Cannot send RPC message.');
}
const agentParticipant = Array.from(room.remoteParticipants.values()).find((p) => p.identity.startsWith('agent'));
if (!agentParticipant) {
throw new Error('No agent found to send RPC message to.');
}
try {
const response = await room.localParticipant.performRpc({
destinationIdentity: agentParticipant.identity,
method,
payload,
responseTimeout,
});
return response;
}
catch (error) {
console.error(`RPC call to method ${method} failed:`, error);
throw error;
}
}
sendTextMessage(text) {
if (this.config.mode !== 'text') {
console.warn('Cannot send text message in voice mode.');
return;
}
const room = this.sdk.getRoom();
if (!room || !this.sdk.isConnected) {
this.emit('error', new Error('Not connected. Cannot send message.'));
return;
}
// Add user message to transcripts immediately
this.transcriptManager.addMessage('user', text, room.localParticipant.identity);
this.emit('transcriptUpdate', this.getTranscripts());
// Send the raw text string over the data channel. The LiveKit agent framework
// is designed to receive this and process it as user input.
try {
const encoder = new TextEncoder();
const data = encoder.encode(text);
// We use publishData directly, which is what `sendText` wraps.
room.localParticipant.publishData(data, { reliable: true });
console.log('Text message sent to agent:', text);
}
catch (error) {
console.error('Failed to send text message:', error);
this.emit('error', new Error(`Failed to send message: ${error instanceof Error ? error.message : String(error)}`));
}
}
}