UNPKG

@just-every/ensemble

Version:

LLM provider abstraction layer with unified streaming interface

383 lines 15.3 kB
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