UNPKG

openai-swarmjs

Version:

Agentic framework inspired from OpenAI's swarm framework for TS, JS

240 lines (239 loc) 10.7 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.Swarm = void 0; const openai_1 = __importDefault(require("openai")); const debug_1 = require("../utils/debug"); const function_parser_1 = require("../utils/function-parser"); const merge_1 = require("../utils/merge"); const CTX_VARS_NAME = 'contextVariables'; const DEBUG = process.env.DEBUG === 'true'; class Swarm { constructor(client) { this.client = client || new openai_1.default(); } async getChatCompletion(agent, history, contextVariables, modelOverride, stream, debug) { const instructions = typeof agent.instructions === 'function' ? agent.instructions(contextVariables) : agent.instructions; const messages = [ { role: 'system', content: instructions }, ...history ]; (0, debug_1.debugPrint)(debug, 'Getting chat completion for...:', messages); const tools = agent.functions.map(f => { // Get the tool with guaranteed parameter structure const rawTool = (0, function_parser_1.functionToJson)(f); const tool = rawTool; // Type-safe parameter manipulation const properties = tool.function.parameters.properties; // Remove context variables if they exist if (CTX_VARS_NAME in properties) { delete properties[CTX_VARS_NAME]; } // Handle required parameters if (Array.isArray(tool.function.parameters.required)) { const required = tool.function.parameters.required; const contextVarIndex = required.indexOf(CTX_VARS_NAME); if (contextVarIndex !== -1) { required.splice(contextVarIndex, 1); if (required.length === 0) { delete tool.function.parameters.required; } } } return tool; }); const createParams = { model: modelOverride || agent.model, messages, tools: tools.length > 0 ? tools : undefined, stream }; // Handle tool_choice based on OpenAI's expected types if (tools.length > 0) { createParams.tool_choice = (agent.toolChoice ?? 'auto'); } else { createParams.tool_choice = 'none'; } return this.client.chat.completions.create(createParams); } handleFunctionResult(result, debug) { // Case 1: Already a Result object if (result && typeof result === 'object' && 'value' in result && 'agent' in result && 'contextVariables' in result) { return result; } // Case 2: Agent object if (result && typeof result === 'object' && 'name' in result && 'model' in result) { return { value: JSON.stringify({ assistant: result.name }), agent: result, contextVariables: {} }; } // Case 3: Handle other types with error checking try { return { value: String(result), agent: null, contextVariables: {} }; } catch (e) { const errorMessage = `Failed to cast response to string: ${result}. Make sure agent functions return a string or Result object. Error: ${e}`; (0, debug_1.debugPrint)(debug, errorMessage); throw new TypeError(errorMessage); } } async handleToolCalls(toolCalls, functions, contextVariables, debug) { const functionMap = new Map(functions.map(f => [f.name, f])); const partialResponse = { messages: [], agent: null, contextVariables: {} }; for (const toolCall of toolCalls) { toolCall.id = toolCall.id.substring(0, 40); console.log(`Tool call: ${JSON.stringify(toolCall)}`); // Add safe parsing of arguments const funName = toolCall.function.name; //debugPrint(debug, `Tool call name: ${funName}`); console.log(`Tool call name: ${funName}`); if (!functionMap.has(funName)) { (0, debug_1.debugPrint)(debug, `Tool ${funName} not found in function map.`); partialResponse.messages.push({ role: 'tool', tool_call_id: toolCall.id.substring(0, 40), name: funName, content: `Error: Tool ${funName} not found.` }); continue; } const args = JSON.parse(toolCall.function.arguments); //debugPrint(debug, `Processing tool call: ${funName} with arguments`, args); console.log(`Processing tool call: ${funName} with arguments`, args); const func = functionMap.get(funName); if (func.toString().includes(CTX_VARS_NAME)) { args[CTX_VARS_NAME] = contextVariables; } console.log(`Calling function: ${func.name} with arguments: ${JSON.stringify(args)}`); const rawResult = await Promise.resolve(func(...Object.values(args))); const result = this.handleFunctionResult(rawResult, debug); console.log(`Tool call result: ${JSON.stringify(result)}`); partialResponse.messages.push({ role: 'tool', tool_call_id: toolCall.id.substring(0, 40), name: funName, content: result.value, }); partialResponse.contextVariables = { ...partialResponse.contextVariables, ...result.contextVariables }; if (result.agent) { partialResponse.agent = result.agent; } } return partialResponse; } async *runAndStream(agent, messages, contextVariables = {}, modelOverride = null, debug = false, maxTurns = Infinity, executeTools = true) { let activeAgent = agent; const ctxVars = { ...contextVariables }; const history = [...messages]; const initLen = messages.length; while (history.length - initLen < maxTurns) { const message = { content: '', sender: agent.name, role: 'assistant', function_call: null, tool_calls: {} }; const completion = await this.getChatCompletion(activeAgent, history, ctxVars, modelOverride, true, debug); yield { delim: 'start' }; for await (const chunk of completion) { const delta = chunk.choices[0].delta; if (delta.role === 'assistant') { delta.sender = activeAgent.name; } yield delta; (0, merge_1.mergeChunk)(message, delta); } yield { delim: 'end' }; message.tool_calls = Object.values(message.tool_calls); if (!message.tool_calls.length) { message.tool_calls = null; } (0, debug_1.debugPrint)(debug, 'Received completion:', message); history.push(message); if (!message.tool_calls || !executeTools) { if ('shouldTransferManually' in activeAgent) { const agent = activeAgent; if (agent.updateLastResponse) { agent.updateLastResponse(message.content || ''); } if (agent.shouldTransferManually()) { (0, debug_1.debugPrint)(debug, 'No tool calls, but manual transfer is required'); const nextAgent = await agent.nextAgent(); if (nextAgent) { (0, debug_1.debugPrint)(debug, 'Transferring to next agent manually'); activeAgent = nextAgent; continue; } } } (0, debug_1.debugPrint)(debug, 'Ending turn.'); break; } console.log(`Raw tool calls>>>>>>>: ${JSON.stringify(message.tool_calls)}`); const partialResponse = await this.handleToolCalls(message.tool_calls, activeAgent.functions, ctxVars, debug); history.push(...partialResponse.messages); Object.assign(ctxVars, partialResponse.contextVariables); if (partialResponse.agent) { activeAgent = partialResponse.agent; } } yield { response: { messages: history.slice(initLen), agent: activeAgent, contextVariables: ctxVars } }; } async run(agent, messages, contextVariables = {}, modelOverride = null, stream = false, debug = false, maxTurns = Infinity, executeTools = true) { if (stream) { return this.runAndStream(agent, messages, contextVariables, modelOverride, debug, maxTurns, executeTools); } let activeAgent = agent; const ctxVars = { ...contextVariables }; const history = [...messages]; const initLen = messages.length; while (history.length - initLen < maxTurns && activeAgent) { const completion = await this.getChatCompletion(activeAgent, history, ctxVars, modelOverride, false, debug); const message = completion.choices[0].message; (0, debug_1.debugPrint)(debug, 'Received completion:', message); message.sender = activeAgent.name; history.push(JSON.parse(JSON.stringify(message))); if (!message.tool_calls || !executeTools) { (0, debug_1.debugPrint)(debug, 'Ending turn.'); break; } const partialResponse = await this.handleToolCalls(message.tool_calls, activeAgent.functions, ctxVars, debug); history.push(...partialResponse.messages); Object.assign(ctxVars, partialResponse.contextVariables); if (partialResponse.agent) { activeAgent = partialResponse.agent; } } return { messages: history.slice(initLen), agent: activeAgent, contextVariables: ctxVars }; } } exports.Swarm = Swarm;