UNPKG

loom-agents

Version:

A lightweight, composable framework for building hierarchical AI agent systems using OpenAI's API.

544 lines 24.2 kB
import { Loom } from "../Loom/Loom.js"; import { v4 } from "uuid"; import OpenAI from "openai"; const valueToString = (value) => { if (value === null || value === undefined) { return String(value); } if (typeof value === "object") { try { return JSON.stringify(value); } catch (error) { return `[Object: Serialization Error - ${error.message}]`; } } return String(value); }; export class Agent { uuid; config; defaultModel = "gpt-4o"; defaultTimeout = 60000; _client; _api; constructor(config) { if (!config.purpose) throw new Error("Agent purpose is required"); if (!config.name) throw new Error("Agent name is required"); if (config.client_config) this._client = new OpenAI(config.client_config); if (config.api) this._api = config.api; this.uuid = `agent.${v4()}`; this.config = { ...config, model: config.model || this.defaultModel, timeout_ms: config.timeout_ms || this.defaultTimeout, }; } get client() { if (!this._client) return Loom.openai; return this._client; } async prepareTools() { const toolsArray = []; if (this.config.mcp_servers) { for (const mcp of this.config.mcp_servers) { const server_tools = await mcp.getTools(); for (const tool of server_tools.tools) { toolsArray.push({ type: "function", name: `mcp_${tool.name}`, description: `MCP Tool: ${tool.name}`, parameters: { type: "object", properties: tool.inputSchema.properties, required: tool.inputSchema.required, additionalProperties: false, }, strict: true, }); } } } if (this.config.tools && this.config.tools.length > 0) { toolsArray.push(...this.config.tools.map((tool) => ({ type: "function", name: tool.name, description: tool.description, parameters: { type: "object", properties: tool.parameters, required: Object.keys(tool.parameters), additionalProperties: false, }, strict: true, }))); } if (this.config.sub_agents && this.config.sub_agents.length > 0) { toolsArray.push({ type: "function", name: "CallSubAgent", description: "Pass an input to a sub agent for processing and return the output", parameters: { type: "object", properties: { sub_agent: { type: "string", description: "The name of the sub agent to call", enum: this.config.sub_agents.map((agent) => agent.config.name), }, request: { type: "string", description: "The request to send to the sub agent", }, }, required: ["sub_agent", "request"], additionalProperties: false, }, strict: true, }); } if (this.config.web_search?.enabled) { toolsArray.push({ type: "web_search_preview", ...this.config.web_search.config, }); } return toolsArray; } ToolsToCompletionTools(tools) { if (!tools || tools.length === 0) return undefined; const toolsArray = []; toolsArray.push(...tools .filter((tool) => tool.type === "function") .map((tool) => { return { type: "function", function: { name: tool.name, description: tool.description, parameters: tool.parameters, strict: tool.strict, }, }; })); return toolsArray; } async run_completions(input, trace) { const run_trace = trace?.start("run_completions", {}); const context = typeof input === "string" ? [{ content: input, role: "user" }] : input.context; const response = await this.client.chat.completions.create({ model: this.config.model, // metadata: { // loom: "powered", // agent: this.config.name, // }, // store: true, // meta data isn't enabled unless you store responses, so.. TODO: Completion mode add `store` config variable messages: [ { role: "system", content: `You are an AI Agent, your purpose is to (${this.config.purpose}). ${this.config.sub_agents && this.config.sub_agents.length > 0 ? `You can query the following 'sub_agents' with the 'CallSubAgent' tool: {${this.config.sub_agents .map((agent) => `${agent.config.name}`) .join(", ")}}` : ""} Consider using all the tools available to you to achieve this. Start acting immediately.`, }, ...context, ], tools: this.ToolsToCompletionTools(await this.prepareTools()), web_search_options: this.config.web_search?.enabled ? { search_context_size: this.config.web_search.config?.search_context_size, user_location: { type: this.config.web_search.config?.user_location ?.type, approximate: { city: this.config.web_search.config?.user_location?.city, country: this.config.web_search.config?.user_location?.country, region: this.config.web_search.config?.user_location?.region, }, }, } : undefined, }); const hasToolCalls = response.choices.some((item) => item.finish_reason === "function_call" || item.finish_reason === "tool_calls"); if (response.choices[0].finish_reason === "stop" && !hasToolCalls) { return { status: "completed", final_message: response.choices[0].message.content, context: [ ...context, ...response.choices.map((choice) => choice.message), ], }; } if (response.choices[0].finish_reason === "content_filter") { return { status: "error", final_message: "[Content Filter] " + response.choices[0].message.content, context: [ ...context, ...response.choices.map((choice) => choice.message), ], }; } if (response.choices[0].finish_reason === "length") { return { status: "error", final_message: "[Length] " + response.choices[0].message.content, context: [ ...context, ...response.choices.map((choice) => choice.message), ], }; } if (response.choices[0].finish_reason === "function_call") { return { status: "error", final_message: "[Function Call] Not implemented", context: [ ...context, ...response.choices.map((choice) => choice.message), ], }; } const call_results = []; if (response.choices[0].finish_reason === "tool_calls") { const tool_calls = response.choices[0].message.tool_calls; if (tool_calls && tool_calls.length > 0) { for (const tool_call of tool_calls) { if (tool_call.function.name.startsWith("mcp_")) { trace?.start("mcp_tool_call", { tool_call, }); const mcp_tool_name = tool_call.function.name.replace("mcp_", ""); const mcpResult = await this.config.mcp_servers?.reduce(async (accPromise, server) => { const acc = await accPromise; if (acc.mcp && acc.tool) return acc; const { tools } = await server.getTools(); const tool = tools.find((tool) => tool.name === mcp_tool_name); return tool ? { mcp: server, tool } : acc; }, Promise.resolve({ mcp: undefined, tool: undefined, })); const { mcp, tool } = mcpResult || {}; if (!mcp || !tool) { call_results.push({ role: "tool", tool_call_id: tool_call.id, content: `[MCP Tool Call Error] ${mcp_tool_name} - Tool not found`, }); continue; } try { const result = await mcp.callTool({ name: mcp_tool_name, arguments: JSON.parse(tool_call.function.arguments), }); if (result.isError) { call_results.push({ role: "tool", tool_call_id: tool_call.id, content: `[MCP Tool Call Error] ${mcp_tool_name} - ${valueToString(result.content)}`, }); continue; } call_results.push({ role: "tool", tool_call_id: tool_call.id, content: valueToString(result.content), }); } catch (error) { call_results.push({ role: "tool", tool_call_id: tool_call.id, content: `[MCP Tool Call Error] ${mcp_tool_name} - ${error.message}`, }); } finally { } } else if (tool_call.function.name === "CallSubAgent") { trace?.start("call_sub_agent", { tool_call, }); const args = JSON.parse(tool_call.function.arguments); const sub_agent = this.config.sub_agents?.find((agent) => agent.config.name === args.sub_agent); if (!sub_agent) { call_results.push({ role: "tool", tool_call_id: tool_call.id, content: `[Sub Agent Error] ${args.sub_agent} - Sub Agent not found`, }); continue; } const result = await sub_agent.run({ context: [ ...context, { role: "user", content: args.request, }, ], }, trace); call_results.push({ role: "tool", tool_call_id: tool_call.id, content: result.final_message, }); } else { trace?.start("tool_call", { tool_call, }); const tool = this.config.tools?.find((tool) => tool.name === tool_call.function.name); if (!tool) { call_results.push({ role: "tool", tool_call_id: tool_call.id, content: `[Tool Call Error] ${tool_call.function.name} - Tool not found`, }); continue; } try { const result = await tool.callback(JSON.parse(tool_call.function.arguments)); call_results.push({ role: "tool", tool_call_id: tool_call.id, content: valueToString(result), }); } catch (error) { call_results.push({ role: "tool", tool_call_id: tool_call.id, content: `[Tool Call Error] ${tool_call.function.name} - ${error.message}`, }); } finally { } } } } } return this.run_completions({ context: [ ...context, ...response.choices.map((choice) => choice.message), ...call_results, ], }, trace).finally(() => { }); } async run_responses(input, trace) { const run_trace = trace?.start("run_responses", {}); const context = typeof input === "string" ? [{ content: input, role: "user" }] : input.context; const response = await this.client.responses.create({ model: this.config.model, metadata: { loom: "powered", agent: this.config.name, }, input: [ { role: "system", content: `You are an AI Agent, your purpose is to (${this.config.purpose}). ${this.config.sub_agents && this.config.sub_agents.length > 0 ? `You can query the following 'sub_agents' with the 'CallSubAgent' tool: {${this.config.sub_agents .map((agent) => `${agent.config.name}`) .join(", ")}}` : ""} Consider using all the tools available to you to achieve this. Start acting immediately.`, }, ...context, ], tools: await this.prepareTools(), }); const hasToolCalls = response.output.some((item) => item.type === "function_call"); if (response.status === "completed" && response.output_text && !hasToolCalls) { return { status: "completed", final_message: response.output_text || "[Unknown] Something went wrong", context: [...context, ...response.output], }; } if (response.status === "failed") { return { status: "error", final_message: `[Failed] ${response.output[0].content} ${response.error?.message}`, context: [...context, ...response.output], }; } if (response.status === "incomplete") { return { status: "error", final_message: `[Incomplete] ${response.output[0].content} ${response.incomplete_details?.reason}`, context: [...context, ...response.output], }; } const call_results = []; if (response.output[0].type === "function_call") { const tool_calls = response.output; if (tool_calls && tool_calls.length > 0) { for (const tool_call of tool_calls) { if (tool_call.name.startsWith("mcp_")) { trace?.start("mcp_tool_call", { tool_call, }); const mcp_tool_name = tool_call.name.replace("mcp_", ""); const mcpResult = await this.config.mcp_servers?.reduce(async (accPromise, server) => { const acc = await accPromise; if (acc.mcp && acc.tool) return acc; const { tools } = await server.getTools(); const tool = tools.find((tool) => tool.name === mcp_tool_name); return tool ? { mcp: server, tool } : acc; }, Promise.resolve({ mcp: undefined, tool: undefined, })); const { mcp, tool } = mcpResult || {}; if (!mcp || !tool) { call_results.push({ type: "function_call_output", call_id: tool_call.call_id, output: `[MCP Tool Call Error] ${mcp_tool_name} - Tool not found`, }); continue; } try { const result = await mcp.callTool({ name: mcp_tool_name, arguments: JSON.parse(tool_call.arguments), }); if (result.isError) { call_results.push({ type: "function_call_output", call_id: tool_call.call_id, output: `[MCP Tool Call Error] ${mcp_tool_name} - ${valueToString(result.content)}`, }); continue; } call_results.push({ type: "function_call_output", call_id: tool_call.call_id, output: valueToString(result.content), }); } catch (error) { call_results.push({ type: "function_call_output", call_id: tool_call.call_id, output: `[MCP Tool Call Error] ${mcp_tool_name} - ${error.message}`, }); } finally { } } else if (tool_call.name === "CallSubAgent") { trace?.start("call_sub_agent", { tool_call, }); const args = JSON.parse(tool_call.arguments); const sub_agent = this.config.sub_agents?.find((agent) => agent.config.name === args.sub_agent); if (!sub_agent) { call_results.push({ type: "function_call_output", call_id: tool_call.call_id, output: `[Sub Agent Error] ${args.sub_agent} - Sub Agent not found`, }); continue; } const result = await sub_agent.run_responses({ context: [ ...context, { role: "user", content: args.request, }, ], }, trace); call_results.push({ type: "function_call_output", call_id: tool_call.call_id, output: result.final_message, }); } else { trace?.start("tool_call", { tool_call, }); const tool = this.config.tools?.find((tool) => tool.name === tool_call.name); if (!tool) { call_results.push({ type: "function_call_output", call_id: tool_call.call_id, output: `[Tool Call Error] ${tool_call.name} - Tool not found`, }); continue; } try { const result = await tool.callback(JSON.parse(tool_call.arguments)); call_results.push({ type: "function_call_output", call_id: tool_call.call_id, output: valueToString(result), }); } catch (error) { call_results.push({ type: "function_call_output", call_id: tool_call.call_id, output: `[Tool Call Error] ${tool_call.name} - ${error.message}`, }); } finally { } } } } } return this.run_responses({ context: [...context, ...response.output, ...call_results], }, trace).finally(() => { }); } async run(input, trace) { const content = typeof input === "string" ? [{ content: input, role: "user" }] : input.context; const api = this._api ? this._api : Loom.api; if (api === "responses") return this.run_responses({ context: content }, trace); return this.run_completions({ context: content }, trace); } asTool(parameters) { return { name: this.config.name.replace(/[^a-zA-Z0-9_-]/g, ""), parameters: parameters ? parameters : { request: { type: "string", description: `Request to send to the ${this.config.name.replace(/[^a-zA-Z0-9_-]/g, "")} agent`, }, }, callback: async (...args) => { return await this.run(`You were invoked as a tool with the following request - ${JSON.stringify(args)}`); }, description: this.config.purpose, }; } } //# sourceMappingURL=Agent.js.map