UNPKG

@lamemind/react-agent-ts

Version:

Streaming ReAct agent in typescript with multiple LLM providers

136 lines (135 loc) 5.65 kB
import { AIMessageChunksCollector } from "./chunk-collector.js"; import { userMessage } from "./messages.js"; const MAX_ITERATIONS = 10; export class ReActAgent { constructor(model, tools, maxIterations = MAX_ITERATIONS) { this.toolsMap = {}; this.isToolCallsComplete = false; this.onMessageCallback = null; this.model = model.bindTools(tools); this.tools = tools; this.tools.forEach(tool => { this.toolsMap[tool.name] = tool; }); this.maxIterations = maxIterations; this.messages = []; if (tools.length > 0) console.log(`Tools: ${tools.map(tool => tool.name).join(", ")}`); } onMessage(callback) { this.onMessageCallback = callback; } async invoke(messages) { if (typeof messages === "string") { this.messages.push(userMessage(messages)); } else { this.messages = messages; } return await this.run(); } /** * Estrae l'ultima risposta dell'assistente, concatenando i messaggi di tipo text ed escludendo il resto (tool call, ecc.) * @returns L'ultimo messaggio di tipo text dell'assistente */ async extractAiTextResponse() { // 1. Trova l'ultimo messaggio dell'utente, possoo essere più di uno // 2. Trova tutti i messaggi role=assistant successivi // 3. Concatena i contenuti di tipo text di questi messaggi const reversed = [...this.messages].reverse(); const userMeggageIndex = reversed.findIndex((m) => m.role === "user"); const assistantMessages = reversed .slice(0, userMeggageIndex) .filter((m) => m.role === "assistant") .reverse(); const assistantTextMessage = assistantMessages .flatMap((m) => m.content) .filter((c) => c.type === "text") .map((c) => c.text) .join("\n"); return assistantTextMessage; } async run() { this.isToolCallsComplete = false; let iterations = 0; while (!this.isToolCallsComplete && iterations < this.maxIterations) { iterations++; // console.log(`\n--- Iterazione ${iterations} ---`); const stream = await this.model.stream(this.messages); const collector = new AIMessageChunksCollector(); await collector.consume(stream); // collector.dumpResponse(); const llmMessage = collector.formatMessage(); this.messages.push(llmMessage); const toolResults = await this.callTools(collector.result.tool_calls); if (!this.isToolCallsComplete) { this.messages.push(...toolResults); // console.log("\nContinuo la conversazione con risultati degli strumenti..."); // console.log(JSON.stringify(this.messages, null, 2)); const shouldContinue = this.onMessageCallback?.(this.messages); if (shouldContinue === false) { console.log("\nConversazione interrotta dall'utente."); break; } } } if (iterations >= this.maxIterations) console.warn(`\nRaggiunto il numero massimo di iterazioni (${this.maxIterations})`); // this.dumpConversation(); return this.messages[this.messages.length - 1].content; } dumpConversation() { console.log("\nConversazione completa:"); console.log("-----------------------"); this.messages.forEach((msg, i) => { if (msg.role === "user") { console.log(`[${i}] User: ${msg.content}`); } else if (msg.role === "assistant") { console.log(`[${i}] Assistant: ${JSON.stringify(msg.content)}...`); } else if (msg.role === "tool") { const tool_call_id = msg.tool_call_id; console.log(`[${i}] Tool (${tool_call_id}): ${msg.content}`); } else console.log(`[${i}] Messaggio sconosciuto: ${JSON.stringify(msg)}`); }); console.log("-----------------------"); } async callTools(tool_calls) { if (tool_calls && tool_calls.length > 0) { // console.log(`\nTool calls richieste (${tool_calls.length}):`); const toolResults = []; for (const call of tool_calls) { console.log(`Call #${toolResults.length + 1} ${call.name} ${JSON.stringify(call.input, null, 2)}`); const result = await this.executeToolCall(call); // console.log(`Risultato: ${JSON.stringify(result, null, 2)}`); toolResults.push({ role: "tool", tool_call_id: call.id, content: JSON.stringify(result) }); } return toolResults; } else { // console.log("\nNessun tool call richiesto, conversazione completata."); this.isToolCallsComplete = true; return []; } } async executeToolCall(call) { const tool = this.toolsMap[call.name]; if (!tool) throw new Error(`Tool non trovato: ${call.name}`); try { const result = await tool.call(call.input); return result; } catch (error) { console.error(`Errore durante l'esecuzione del tool ${call.name}: ${error}`); return `@@@@ Errore durante l'esecuzione del tool ${call.name}: ${error} @@@@`; } } }