UNPKG

groq-agents

Version:

Prebuilt task specific AI agents running solely on Groq (Not Grok). *In development

525 lines (445 loc) 21.3 kB
import { z } from "zod"; import { ToolUseModels, MessageType, DependencyTypeSchema } from "./types"; import { tool, generateText, CoreTool } from "ai"; import { createGroq } from "@ai-sdk/groq"; import {Logger } from "./utils"; import WebSocket from 'ws'; import Groq from 'groq-sdk'; import * as http from "http" import JSON5 from 'json5'; import * as Master from "../core/memory/code/agentDumps/Master_Agent_000000000000" type ParamsSchemaType<P extends z.ZodTypeAny = z.ZodTypeAny> = { description?: string; parameters: P; }; const ParamsSchema = <P extends z.ZodTypeAny>(schema: P): ParamsSchemaType<P> => ({ parameters: schema, }); const BaseParamsSchema = z.record(z.any()); const ExecutionResponseSchema = z.object({ status: z.enum(["200", "400", "500"]), message: z.string(), }); const RetrievalResponseSchema = z.object({ status: z.enum(["200", "400", "500"]), responseBody: z.record(z.any()), }); const CustomResponseSchema = z.object({ status: z.enum(["200", "400", "500"]), other: z.any(), }); const ExecutionActionSchema = <P extends z.ZodTypeAny>(params: P) => z.object({ name: z.string(), type: z.literal("Execution"), description: z.string(), params: params, handlerKey: z.string(), // Reference to the function in your registry or file }); const RetrievalActionSchema = <P extends z.ZodTypeAny>(params: P) => z.object({ name: z.string(), type: z.literal("Retrieval"), description: z.string(), params: params, handlerKey: z.string(), }); const CustomActionSchema = <P extends z.ZodTypeAny>(params: P) => z.object({ name: z.string(), type: z.literal("Custom"), description: z.string(), params: params, handlerKey: z.string(), }); const DiscriminatedActionSchema = z.discriminatedUnion("type", [ ExecutionActionSchema(BaseParamsSchema), RetrievalActionSchema(BaseParamsSchema), CustomActionSchema(BaseParamsSchema), ]); const AgentSchema = z.object({ /** * Unique Agent ID */ id: z.string(), /** * Name of the Agent */ name: z.string(), /** * Simple description of the agent's capabilities and areas of use */ description: z.string(), /** * A list of dependencies for packages that an agent requires to work */ dependency: z.array(DependencyTypeSchema).optional(), /** * Array of actions (tools) that the agent can interface with */ actions: z.array(DiscriminatedActionSchema), }); interface ServerConfig { port: number; host: string; enableWebsocket: boolean; } class Agent { private system: string; private agentBody: AgentType; public model: ToolUseModels private task: string; private api_key: string private server: http.Server | null = null; private wss: WebSocket.Server | null = null; private readonly serverConfig: ServerConfig = { port: 9090, host: 'localhost', enableWebsocket: true }; private readonly TIMEOUT_MS:number private readonly useStreamLogging = true; messages: MessageType[]; constructor(system: string|undefined = Master.SystemPrompt, agentBody: AgentType|undefined, model: ToolUseModels, task: string, api_key: string, timer:number|undefined) { this.system = system || Master.SystemPrompt; this.agentBody = agentBody || Master.AgentBody; this.model = model; this.messages = []; this.task = task; this.api_key = api_key this.TIMEOUT_MS = timer || 1 * 60 * 1000 if (this.system) { this.messages.push({ role: "system", content: this.system }); } Logger( "Info",`Agent ${this.agentBody.name} initialized with task: ${this.task}`, this.useStreamLogging); } public async work() { Logger("Action",`Starting work on task: ${this.task}`, this.useStreamLogging); this.messages.push({ role: "user", content: `${this.task}` }); const result = await this.execute(); this.messages.push({ role: "assistant", content: result || "" }); Logger("Response", `Completed work on task: ${this.task}`, this.useStreamLogging); return result; } private async watcher(): Promise<{ iscomplete: boolean, assesment: string }> { Logger("Info",`Starting watcher agent`, this.useStreamLogging) const groq = new Groq({ apiKey: this.api_key, }); const schema = { type: "object", properties: { iscomplete: { type: "boolean", description: "True if the task is complete, false otherwise" }, assesment: { type: "string", description: "A detailed explanation of the assessment" } }, required: ["iscomplete", "assesment"], description: "Assessment of task completion" }; const jsonSchema = JSON.stringify(schema, null, 2); const context = { messages: this.messages, agent: this.agentBody, task: this.task }; Logger("[WA]",`Provided Context: ${JSON.stringify(context)}`, this.useStreamLogging) const prompt = `As an AI task assessor, evaluate if the following task has been completed successfully. Task: ${this.task} Context: ${JSON.stringify(context, null, 2)} Provide your assessment in JSON format, strictly adhering to the following schema: ${jsonSchema}. Ensure the response is ONLY the JSON object.`; try { const completion = await groq.chat.completions.create({ messages: [{ role: "user", content: prompt }], model: this.model, temperature: 0, stream: false, response_format: { type: "json_object" } }); const response = completion.choices[0]?.message?.content; console.log("Watcher Raw response: ", response); Logger("[WA]",`Watcher Raw Response: ${JSON.stringify(response)}`, this.useStreamLogging); if (!response) { Logger("[WA]", `No response from model`, this.useStreamLogging); return { iscomplete: false, assesment: "No response from model" }; } let parsedResponse; try { parsedResponse = JSON5.parse(response); } catch (parseError) { Logger("[WA]",`JSON5 Parse Error: ${parseError}. Raw response: ${response}`, this.useStreamLogging); return { iscomplete: false, assesment: `JSON5 Parse Error: ${parseError}` }; } // Validate response structure if (typeof parsedResponse.iscomplete !== 'boolean' || typeof parsedResponse.assesment !== 'string') { Logger("[WA]", `Invalid response format`, this.useStreamLogging); return { iscomplete: false, assesment: "Invalid response format from model" }; } Logger("[WA]",`\n Task: ${this.task} assessment complete. \n Status: ${parsedResponse.iscomplete ? 'Complete' : 'Incomplete'} \n Assessment: ${parsedResponse.assesment}`, this.useStreamLogging); return { iscomplete: parsedResponse.iscomplete, assesment: parsedResponse.assesment }; } catch (error) { const errorMessage = error instanceof Error ? error.message : "Error parsing assessment response"; Logger("Error", `[WA] Error: ${errorMessage}.`, this.useStreamLogging); // Log the raw response return { iscomplete: false, assesment: errorMessage }; } } private getLogicFilePath(agent: AgentType): string { const sanitizedAgentName = agent.name.replace(/\s+/g, "_"); const sanitizedID = agent.id.replace(/\s+/g, "").trim(); return `../core/memory/code/functionDumps/${sanitizedAgentName}_${sanitizedID}.ts`; } private async execute() { try { const startTime = Date.now(); let isTaskComplete = false; while (!isTaskComplete && Date.now() - startTime < this.TIMEOUT_MS) { Logger("Action",`Execution attempt at ${new Date().toISOString()}`, this.useStreamLogging); const formattedMessages = this.messages.map(message => ({ role: message.role, content: message.content, })); await this.startServer(); const groq = createGroq({ apiKey: this.api_key }); Logger("Info",`Generating Output`, this.useStreamLogging); console.log("Model: ", this.model); console.log("Sys Msg: ", this.system); console.log("Messages: ", formattedMessages); const logicFilePath = this.getLogicFilePath(this.agentBody); const agentLogic = await import(logicFilePath); // Build the tools map by using handlerKey to look up logic functions from agentLogic const toolsMap = this.agentBody.actions.reduce((acc, action) => { const paramsSchema = action.params instanceof z.ZodType ? action.params : z.object(action.params || {}); acc[action.name] = tool({ description: action.description, parameters: paramsSchema, execute: async (params) => { const handler = agentLogic[action.handlerKey]; if (typeof handler !== "function") { throw new Error(`Handler for key ${action.handlerKey} not found in ${logicFilePath}`); } const result = await handler(params); this.messages.push({ role: "system", content: `Tool ${action.name}, result: ${JSON.stringify(result)}` }); return result; }, }); return acc; }, {} as Record<string, CoreTool>); const completion = await generateText({ model: groq(this.model), system: this.system, messages: formattedMessages, tools: toolsMap, maxSteps: 10, }); this.messages.push({ role: "assistant", content: completion.text }); Logger("Info",`completion messages: ${JSON.stringify(this.messages)}`, this.useStreamLogging); const { iscomplete, assesment } = await this.watcher(); if (iscomplete) { await this.stopServer(); Logger("Response",`Task completed successfully: ${completion.text}`, this.useStreamLogging); return completion.text; } this.messages.push({ role: "system", content: `Previous attempt assessment: ${assesment}. Please try again with remaining time: ${ Math.round((this.TIMEOUT_MS - (Date.now() - startTime)) / 1000) } seconds.` }); } Logger("Error",`Task timed out after ${this.TIMEOUT_MS}ms`, this.useStreamLogging); throw new Error('Task incomplete after timeout'); } catch (error: unknown) { await this.stopServer(); let errorMessage = 'Unknown error occurred'; if (error instanceof Error) { errorMessage = error.message; } else if (typeof error === 'string') { errorMessage = error; } else if (error && typeof error === 'object' && 'message' in error) { errorMessage = String(error.message); } Logger("Error",`Task execution failed: ${errorMessage}`, this.useStreamLogging); return `Task execution failed: ${errorMessage}`; } } private async startServer(): Promise<void> { try{ if (this.server) return; Logger("Info",`Starting server on ${this.serverConfig.host}:${this.serverConfig.port}`, this.useStreamLogging); this.server = http.createServer(async (req, res) => { // Add CORS headers res.setHeader('Access-Control-Allow-Origin', '*'); res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); // Basic request logging console.log(`${new Date().toISOString()} ${req.method} ${req.url}`); try { switch(req.url) { case '/health': res.writeHead(200, { "Content-Type": "application/json" }); res.end(JSON.stringify({ status: 'healthy' })); break; case '/status': res.writeHead(200, { "Content-Type": "application/json" }); res.end(JSON.stringify({ agent: this.agentBody.name, task: this.task, model: this.model, status: 'running', uptime: process.uptime(), messages: this.messages.length })); break; case '/messages': res.writeHead(200, { "Content-Type": "application/json" }); res.end(JSON.stringify(this.messages)); break; case '/metrics': res.writeHead(200, { "Content-Type": "application/json" }); res.end(JSON.stringify({ messageCount: this.messages.length, taskCompletionRate: this.getTaskCompletionRate(), averageResponseTime: this.getAverageResponseTime(), memory: process.memoryUsage() })); break; case '/tools': res.writeHead(200, { "Content-Type": "application/json" }); res.end(JSON.stringify(this.agentBody.actions)); break; case '/docs': res.writeHead(200, { "Content-Type": "application/json" }); res.end(JSON.stringify({ endpoints: [ { path: '/health', method: 'GET', description: 'Health check endpoint' }, { path: '/status', method: 'GET', description: 'Current agent status' }, { path: '/messages', method: 'GET', description: 'Message history' }, { path: '/metrics', method: 'GET', description: 'Performance metrics' }, { path: '/tools', method: 'GET', description: 'Available tools' }, { path: '/docs', method: 'GET', description: 'API documentation' } ] })); break; default: res.writeHead(404); res.end(); } } catch (error: unknown) { const errorMessage = error instanceof Error ? error.message : 'Internal server error'; res.writeHead(500); res.end(JSON.stringify({ error: errorMessage })); } }); // Setup WebSocket for real-time updates if (this.serverConfig.enableWebsocket) { try { this.wss = new WebSocket.Server({ server: this.server }); this.wss.on('error', (error) => { Logger("Error",`WebSocket error: ${error.message}`, this.useStreamLogging); }); this.wss.on('connection', (ws) => { ws.send(JSON.stringify({ type: 'connected', message: 'Connected to agent server' })); // Create a message handler function const handleMessage = (message: any) => { ws.send(JSON.stringify({ type: 'message', data: message })); }; // Store the message handler for cleanup const messageHandlers = new Set(); messageHandlers.add(handleMessage); // Clean up on client disconnect ws.on('close', () => { messageHandlers.delete(handleMessage); }); }); } catch (wsError: unknown) { // Explicitly type the error const errorMessage = wsError instanceof Error ? wsError.message : 'Unknown WebSocket setup error'; Logger("Error",`WebSocket setup failed: ${errorMessage}`, this.useStreamLogging); } } return new Promise((resolve, reject) => { this.server!.listen(this.serverConfig.port, this.serverConfig.host, () => { Logger("Info",`Server running at http://${this.serverConfig.host}:${this.serverConfig.port}`, this.useStreamLogging); resolve(); }); this.server!.on('error', (err) => { Logger("Error",`Server error: ${err.message}`, this.useStreamLogging); reject(err); }); }); } catch (error: unknown) { const errorMessage = error instanceof Error ? error.message : 'Unknown server error'; Logger("Error",`Failed to start server: ${errorMessage}`, this.useStreamLogging); throw error; } } public async stopServer(): Promise<void> { Logger("Info",`Stopping server`, this.useStreamLogging); if (this.wss) { this.wss.close(); this.wss = null; } if (!this.server) { return; } return new Promise((resolve) => { this.server!.close(() => { console.log('Server stopped'); this.server = null; resolve(); }); }); } public getTaskCompletionRate(): number { const completedTasks = this.messages.filter(m => m.role === "assistant" && !m.content.includes("Task execution failed") ).length; return completedTasks / Math.max(1, this.messages.length); } public getAverageResponseTime(): number { const timestamps: number[] = []; let prevTimestamp = Date.now(); this.messages.forEach(message => { if (message.role === "assistant") { const currentTimestamp = Date.now(); timestamps.push(currentTimestamp - prevTimestamp); prevTimestamp = currentTimestamp; } }); return timestamps.length ? timestamps.reduce((acc, time) => acc + time, 0) / timestamps.length : 0; } } type AgentType = z.infer<typeof AgentSchema>; type Action = z.infer<typeof DiscriminatedActionSchema>; export {ParamsSchema, Agent, AgentType, Action, AgentSchema, DiscriminatedActionSchema}