@just-every/ensemble
Version:
LLM provider abstraction layer with unified streaming interface
383 lines • 15.3 kB
JavaScript
import { getModelFromAgent } from '../model_providers/model_provider.js';
import { getModelProvider } from '../model_providers/model_provider.js';
import { MessageHistory } from '../utils/message_history.js';
import { handleToolCall } from '../utils/tool_execution_manager.js';
import { processToolResult } from '../utils/tool_result_processor.js';
import { emitEvent } from '../utils/event_controller.js';
export async function* ensembleLive(config, agent, options) {
const startTime = Date.now();
let session = null;
let messageHistory = null;
let totalToolCalls = 0;
let currentTurnToolCalls = 0;
let isSessionActive = true;
let totalCost = 0;
let totalTokens = 0;
try {
const model = await getModelFromAgent(agent);
if (!model) {
throw new Error('No model specified in agent configuration');
}
const provider = getModelProvider(model);
if (!provider) {
throw new Error(`No provider found for model: ${model}`);
}
if (!provider.createLiveSession) {
throw new Error(`Provider ${provider.provider_id} does not support Live API`);
}
if (options?.messageHistory) {
messageHistory = new MessageHistory();
for (const message of options.messageHistory) {
if ('content' in message && message.content) {
await messageHistory.add(message);
}
}
}
session = await provider.createLiveSession(config, agent, model, options);
const startEvent = {
type: 'live_start',
timestamp: new Date().toISOString(),
sessionId: session.sessionId,
config,
};
yield startEvent;
emitEvent({
type: 'agent_start',
agent: {
agent_id: agent.agent_id,
name: agent.name,
model: agent.model,
modelClass: agent.modelClass,
},
timestamp: new Date().toISOString(),
}, agent);
if (messageHistory) {
const historyMessages = await messageHistory.getMessages();
if (historyMessages.length > 0) {
for (const message of historyMessages) {
if ('role' in message && message.role && 'content' in message) {
const role = message.role === 'assistant' ? 'assistant' : 'user';
const content = typeof message.content === 'string' ? message.content : '';
if (content) {
await session.sendText(content, role);
}
}
}
}
}
for await (const event of session.getEventStream()) {
if (!isSessionActive) {
break;
}
if (event.type === 'cost_update') {
const costEvent = event;
if (costEvent.usage.totalCost) {
totalCost += costEvent.usage.totalCost;
}
if (costEvent.usage.totalTokens) {
totalTokens += costEvent.usage.totalTokens;
}
}
if (event.type === 'tool_call') {
const toolCallEvent = event;
const toolResults = [];
const maxToolCalls = options?.maxToolCalls ?? agent.maxToolCalls ?? 200;
const maxToolCallRoundsPerTurn = options?.maxToolCallRoundsPerTurn ?? agent.maxToolCallRoundsPerTurn ?? Infinity;
if (totalToolCalls >= maxToolCalls) {
const errorEvent = {
type: 'error',
timestamp: new Date().toISOString(),
error: `Maximum tool calls (${maxToolCalls}) exceeded`,
code: 'MAX_TOOL_CALLS_EXCEEDED',
recoverable: false,
};
yield errorEvent;
break;
}
if (currentTurnToolCalls >= maxToolCallRoundsPerTurn) {
const errorEvent = {
type: 'error',
timestamp: new Date().toISOString(),
error: `Maximum tool call rounds per turn (${maxToolCallRoundsPerTurn}) exceeded`,
code: 'MAX_TURN_TOOL_CALLS_EXCEEDED',
recoverable: true,
};
yield errorEvent;
continue;
}
for (const toolCall of toolCallEvent.toolCalls) {
const toolStartEvent = {
type: 'tool_start',
timestamp: new Date().toISOString(),
toolCall,
};
yield toolStartEvent;
try {
const tools = agent.tools || [];
const tool = tools.find(t => t.definition.function.name === toolCall.function.name);
if (!tool) {
throw new Error(`Tool not found: ${toolCall.function.name}`);
}
const result = await handleToolCall(toolCall, tool, agent);
const processedResult = await processToolResult(toolCall, result, agent);
const toolCallResult = {
toolCall,
id: toolCall.id,
call_id: toolCall.call_id || toolCall.id,
output: processedResult,
};
toolResults.push(toolCallResult);
totalToolCalls++;
currentTurnToolCalls++;
const toolResultEvent = {
type: 'tool_result',
timestamp: new Date().toISOString(),
toolCallResult,
};
yield toolResultEvent;
if (agent.onToolResult) {
await agent.onToolResult(toolCallResult);
}
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
const toolCallResult = {
toolCall,
id: toolCall.id,
call_id: toolCall.call_id || toolCall.id,
error: errorMessage,
};
toolResults.push(toolCallResult);
const errorEvent = {
type: 'error',
timestamp: new Date().toISOString(),
error: `Tool call failed: ${errorMessage}`,
code: 'TOOL_CALL_ERROR',
recoverable: true,
};
yield errorEvent;
if (agent.onToolError) {
await agent.onToolError(toolCallResult);
}
}
}
if (toolResults.length > 0 && session.isActive()) {
await session.sendToolResponse(toolResults);
}
const toolDoneEvent = {
type: 'tool_done',
timestamp: new Date().toISOString(),
totalCalls: totalToolCalls,
};
yield toolDoneEvent;
}
if (event.type === 'turn_complete') {
currentTurnToolCalls = 0;
const turnEvent = event;
if (messageHistory && turnEvent.message) {
await messageHistory.add(turnEvent.message);
}
}
if (event.type === 'interrupted') {
currentTurnToolCalls = 0;
}
if (event.type === 'live_ready') {
emitEvent({
type: 'agent_status',
agent: {
agent_id: agent.agent_id,
name: agent.name,
model: agent.model,
modelClass: agent.modelClass,
},
status: 'ready',
timestamp: new Date().toISOString(),
}, agent);
}
yield event;
}
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
const errorEvent = {
type: 'error',
timestamp: new Date().toISOString(),
error: errorMessage,
code: error instanceof Error && 'code' in error ? String(error.code) : 'UNKNOWN_ERROR',
recoverable: false,
};
yield errorEvent;
throw error;
}
finally {
if (session && session.isActive()) {
await session.close();
}
isSessionActive = false;
const duration = Date.now() - startTime;
const endEvent = {
type: 'live_end',
timestamp: new Date().toISOString(),
reason: isSessionActive ? 'completed' : 'error',
duration,
totalTokens,
totalCost: totalCost > 0 ? totalCost : undefined,
};
yield endEvent;
emitEvent({
type: 'agent_done',
agent: {
agent_id: agent.agent_id,
name: agent.name,
model: agent.model,
modelClass: agent.modelClass,
},
duration_with_tools: duration,
request_cost: totalCost > 0 ? totalCost : undefined,
timestamp: new Date().toISOString(),
}, agent);
}
}
export async function* ensembleLiveAudio(audioSource, agent, options) {
const config = {
responseModalities: ['AUDIO'],
speechConfig: options?.voice
? {
voiceConfig: {
prebuiltVoiceConfig: { voiceName: options.voice },
},
languageCode: options.language,
}
: undefined,
inputAudioTranscription: {},
outputAudioTranscription: {},
};
if (options?.enableAffectiveDialog) {
config.enableAffectiveDialog = true;
}
if (options?.enableProactivity) {
config.proactivity = {
proactiveAudio: true,
};
}
const model = await getModelFromAgent(agent);
console.log('[ensembleLiveAudio] Using model:', model);
if (!model) {
throw new Error('No model specified in agent configuration');
}
const provider = getModelProvider(model);
console.log('[ensembleLiveAudio] Provider:', provider?.provider_id);
if (!provider || !provider.createLiveSession) {
throw new Error(`Provider does not support Live API for model: ${model}`);
}
console.log('[ensembleLiveAudio] Creating live session...');
const session = await provider.createLiveSession(config, agent, model, options);
console.log('[ensembleLiveAudio] Session created:', session.sessionId);
let audioChunkCount = 0;
let totalAudioBytes = 0;
const audioProcessingTask = (async () => {
try {
console.log('[ensembleLiveAudio] Starting audio processing task...');
for await (const chunk of audioSource) {
if (!session.isActive()) {
console.log('[ensembleLiveAudio] Session inactive, stopping audio processing');
break;
}
audioChunkCount++;
totalAudioBytes += chunk.length;
const base64Data = Buffer.from(chunk).toString('base64');
console.log(`[ensembleLiveAudio] Sending audio chunk ${audioChunkCount}, size: ${chunk.length} bytes, total: ${totalAudioBytes} bytes`);
await session.sendAudio({
data: base64Data,
mimeType: 'audio/pcm;rate=16000',
});
}
console.log(`[ensembleLiveAudio] Audio processing completed. Total chunks: ${audioChunkCount}, Total bytes: ${totalAudioBytes}`);
}
catch (error) {
console.error('[ensembleLiveAudio] Error processing audio:', error);
}
})();
try {
yield {
type: 'live_start',
timestamp: new Date().toISOString(),
sessionId: session.sessionId,
config,
};
console.log('[ensembleLiveAudio] Starting event processing...');
let eventCount = 0;
for await (const event of session.getEventStream()) {
eventCount++;
console.log(`[ensembleLiveAudio] Event ${eventCount}:`, event.type);
yield event;
}
console.log(`[ensembleLiveAudio] Event processing completed. Total events: ${eventCount}`);
}
finally {
await audioProcessingTask;
if (session.isActive()) {
await session.close();
}
yield {
type: 'live_end',
timestamp: new Date().toISOString(),
reason: 'completed',
};
}
}
export async function ensembleLiveText(agent, options) {
const config = {
responseModalities: ['TEXT'],
};
let session = null;
const sessionGenerator = ensembleLive(config, agent, options);
const eventQueue = [];
let eventPromiseResolve = null;
(async () => {
for await (const event of sessionGenerator) {
if (event.type === 'live_start') {
}
if (eventPromiseResolve) {
eventPromiseResolve({ value: event, done: false });
eventPromiseResolve = null;
}
else {
eventQueue.push(event);
}
}
if (eventPromiseResolve) {
eventPromiseResolve({ value: undefined, done: true });
}
})();
return {
sendMessage: async (text) => {
if (!session) {
throw new Error('Session not initialized');
}
await session.sendText(text, 'user');
},
getEvents: async function* () {
while (true) {
if (eventQueue.length > 0) {
yield eventQueue.shift();
}
else {
const result = await new Promise(resolve => {
eventPromiseResolve = resolve;
});
if (result.done)
break;
if (result.value)
yield result.value;
}
}
},
close: async () => {
if (session && session.isActive()) {
await session.close();
}
},
};
}
//# sourceMappingURL=ensemble_live.js.map