groq-agents
Version:
Prebuilt task specific AI agents running solely on Groq (Not Grok). *In development
525 lines (445 loc) • 21.3 kB
text/typescript
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}