From 60196079f2d9f466db54feb09c731982eb419ad8 Mon Sep 17 00:00:00 2001 From: motalib-code Date: Tue, 6 Jan 2026 18:51:47 +0530 Subject: [PATCH] feat: implement multi-agent orchestration backend #61 --- .../src/api/v1/Agent/agent.constant.ts | 8 ++ .../src/api/v1/Agent/agent.controller.ts | 61 +++++++++++++ .../src/api/v1/Agent/agent.model.ts | 34 +++++++ .../src/api/v1/Agent/agent.routes.ts | 11 +++ .../src/api/v1/Agent/agent.service.ts | 88 +++++++++++++++++++ .../src/api/v1/Agent/agent.type.ts | 38 ++++++++ .../src/api/v1/Agent/agent.validator.ts | 17 ++++ LocalMind-Backend/src/routes/app.ts | 5 +- 8 files changed, 260 insertions(+), 2 deletions(-) create mode 100644 LocalMind-Backend/src/api/v1/Agent/agent.constant.ts create mode 100644 LocalMind-Backend/src/api/v1/Agent/agent.controller.ts create mode 100644 LocalMind-Backend/src/api/v1/Agent/agent.model.ts create mode 100644 LocalMind-Backend/src/api/v1/Agent/agent.routes.ts create mode 100644 LocalMind-Backend/src/api/v1/Agent/agent.service.ts create mode 100644 LocalMind-Backend/src/api/v1/Agent/agent.type.ts create mode 100644 LocalMind-Backend/src/api/v1/Agent/agent.validator.ts diff --git a/LocalMind-Backend/src/api/v1/Agent/agent.constant.ts b/LocalMind-Backend/src/api/v1/Agent/agent.constant.ts new file mode 100644 index 0000000..22bc568 --- /dev/null +++ b/LocalMind-Backend/src/api/v1/Agent/agent.constant.ts @@ -0,0 +1,8 @@ +export const AgentRoles = ['planner', 'researcher', 'executor', 'validator', 'custom'] as const +export const RunStatuses = ['pending', 'running', 'blocked', 'completed', 'failed'] as const +export const StepStatuses = ['success', 'error'] as const + +export const AgentConstants = { + MAX_AGENTS_PER_RUN: 7, + DEFAULT_PRIORITY: 1, +} diff --git a/LocalMind-Backend/src/api/v1/Agent/agent.controller.ts b/LocalMind-Backend/src/api/v1/Agent/agent.controller.ts new file mode 100644 index 0000000..73ef6a8 --- /dev/null +++ b/LocalMind-Backend/src/api/v1/Agent/agent.controller.ts @@ -0,0 +1,61 @@ +import { Request, Response } from 'express' +import { createRunSchema } from './agent.validator' +import { createAgentRun, executeAgentRun, getAgentRun, getAgentRunLogs } from './agent.service' +import { SendResponse } from '../../../utils/SendResponse.utils' +import { StatusConstant } from '../../../constant/Status.constant' + +class AgentController { + constructor() { + this.createRun = this.createRun.bind(this) + this.executeRun = this.executeRun.bind(this) + this.getRunStatus = this.getRunStatus.bind(this) + this.getRunLogs = this.getRunLogs.bind(this) + } + + async createRun(req: Request, res: Response): Promise { + try { + const validatedData = await createRunSchema.parseAsync(req.body) + // Cast the agents to any to bypass strict type checking if needed vs Validator types + const run = await createAgentRun(validatedData.runName, validatedData.agents as any) + SendResponse.success(res, 'Agent run created successfully', run, StatusConstant.CREATED) + } catch (err: any) { + SendResponse.error(res, err.message, StatusConstant.BAD_REQUEST, err) + } + } + + async executeRun(req: Request, res: Response): Promise { + try { + const { id } = req.params + const run = await executeAgentRun(id) + SendResponse.success(res, 'Agent run execution started', run, StatusConstant.OK) + } catch (err: any) { + SendResponse.error(res, err.message, StatusConstant.INTERNAL_SERVER_ERROR, err) + } + } + + async getRunStatus(req: Request, res: Response): Promise { + try { + const { id } = req.params + const run = await getAgentRun(id) + if (!run) { + SendResponse.error(res, 'Run not found', StatusConstant.NOT_FOUND) + return + } + SendResponse.success(res, 'Agent run status fetched', run, StatusConstant.OK) + } catch (err: any) { + SendResponse.error(res, err.message, StatusConstant.INTERNAL_SERVER_ERROR, err) + } + } + + async getRunLogs(req: Request, res: Response): Promise { + try { + const { id } = req.params + const logs = await getAgentRunLogs(id) + SendResponse.success(res, 'Agent run logs fetched', logs, StatusConstant.OK) + } catch (err: any) { + SendResponse.error(res, err.message, StatusConstant.INTERNAL_SERVER_ERROR, err) + } + } +} + +export default new AgentController() diff --git a/LocalMind-Backend/src/api/v1/Agent/agent.model.ts b/LocalMind-Backend/src/api/v1/Agent/agent.model.ts new file mode 100644 index 0000000..726e368 --- /dev/null +++ b/LocalMind-Backend/src/api/v1/Agent/agent.model.ts @@ -0,0 +1,34 @@ +import mongoose, { Schema } from 'mongoose' +import { IAgentRun, IAgentStepLog } from './agent.type' +import { AgentRoles, RunStatuses, StepStatuses } from './agent.constant' + +const agentConfigSchema = new Schema({ + agentId: { type: String, required: true }, + name: { type: String, required: true }, + type: { type: String, enum: AgentRoles, required: true }, + systemPrompt: { type: String, required: true }, + tools: [{ type: String }], + priority: { type: Number, default: 1 }, + isActive: { type: Boolean, default: true } +}, { _id: false }) + +const agentRunSchema = new Schema({ + runName: { type: String, required: true }, + status: { type: String, enum: RunStatuses, default: 'pending' }, + agents: [agentConfigSchema], + currentAgent: { type: String }, + startedAt: { type: Date }, + completedAt: { type: Date } +}, { timestamps: true }) + +const agentStepLogSchema = new Schema({ + runId: { type: 'ObjectId', ref: 'AgentRun', required: true }, + agentId: { type: String, required: true }, + inputPrompt: { type: String, required: true }, + output: { type: String, required: true }, + status: { type: String, enum: StepStatuses, required: true }, + executionTimeMs: { type: Number, required: true } +}, { timestamps: true }) + +export const AgentRunModel = mongoose.model('AgentRun', agentRunSchema) +export const AgentStepLogModel = mongoose.model('AgentStepLog', agentStepLogSchema) diff --git a/LocalMind-Backend/src/api/v1/Agent/agent.routes.ts b/LocalMind-Backend/src/api/v1/Agent/agent.routes.ts new file mode 100644 index 0000000..6877de3 --- /dev/null +++ b/LocalMind-Backend/src/api/v1/Agent/agent.routes.ts @@ -0,0 +1,11 @@ +import { Router } from 'express' +import AgentController from './agent.controller' + +const router = Router() + +router.post('/v1/agent-runs', AgentController.createRun) +router.post('/v1/agent-runs/:id/execute', AgentController.executeRun) +router.get('/v1/agent-runs/:id', AgentController.getRunStatus) +router.get('/v1/agent-runs/:id/logs', AgentController.getRunLogs) + +export { router as agentRoutes } diff --git a/LocalMind-Backend/src/api/v1/Agent/agent.service.ts b/LocalMind-Backend/src/api/v1/Agent/agent.service.ts new file mode 100644 index 0000000..af4d8c9 --- /dev/null +++ b/LocalMind-Backend/src/api/v1/Agent/agent.service.ts @@ -0,0 +1,88 @@ +import { AgentRunModel, AgentStepLogModel } from './agent.model' +import { IAgentConfig, IAgentRun, IAgentStepLog } from './agent.type' + +export const createAgentRun = async ( + runName: string, + agents: IAgentConfig[] +): Promise => { + const run = new AgentRunModel({ + runName, + agents, + status: 'pending', + }) + return await run.save() +} + +export const getAgentRun = async (runId: string): Promise => { + return await AgentRunModel.findById(runId).exec() +} + +export const getAgentRunLogs = async (runId: string): Promise => { + return await AgentStepLogModel.find({ runId }).sort({ createdAt: 1 }).exec() +} + +export const executeAgentRun = async (runId: string) => { + const run = await AgentRunModel.findById(runId) + if (!run) throw new Error('Run not found') + + if (run.status === 'running' || run.status === 'completed') { + return run + } + + run.status = 'running' + run.startedAt = new Date() + await run.save() + + // Start execution in background + processRun(run) + + return run +} + +const processRun = async (run: any) => { + try { + // Sort agents by priority (lower number = higher priority, or vice versa? + // Usually priority 1 is high. But if it's a sequence, maybe 1, 2, 3... + // The requirement says "Sort agents by priority". I'll assume ascending order of priority field. + const agents = run.agents.sort((a: IAgentConfig, b: IAgentConfig) => a.priority - b.priority) + let previousOutput = '' + + for (const agent of agents) { + if (!agent.isActive) continue + + run.currentAgent = agent.agentId + await run.save() + + const start = Date.now() + + // Simulate AI processing delay + await new Promise((resolve) => setTimeout(resolve, 1000)) + + // Placeholder for AI invocation + const input = previousOutput || 'Start of Run' + const executionOutput = `[${agent.name}]: Executed task based on prompt. Input len: ${input.length}` + + const log = new AgentStepLogModel({ + runId: run._id, + agentId: agent.agentId, + inputPrompt: input, + output: executionOutput, + status: 'success', + executionTimeMs: Date.now() - start + }) + await log.save() + + previousOutput = executionOutput + } + + run.status = 'completed' + run.completedAt = new Date() + run.currentAgent = undefined + await run.save() + + } catch (error) { + console.error('Run execution error:', error) + run.status = 'failed' + await run.save() + } +} diff --git a/LocalMind-Backend/src/api/v1/Agent/agent.type.ts b/LocalMind-Backend/src/api/v1/Agent/agent.type.ts new file mode 100644 index 0000000..3b81bc8 --- /dev/null +++ b/LocalMind-Backend/src/api/v1/Agent/agent.type.ts @@ -0,0 +1,38 @@ +import { AgentRoles, RunStatuses, StepStatuses } from './agent.constant' + +export type AgentRole = (typeof AgentRoles)[number] +export type RunStatus = (typeof RunStatuses)[number] +export type StepStatus = (typeof StepStatuses)[number] + +export interface IAgentConfig { + agentId: string + name: string + type: AgentRole + systemPrompt: string + tools: string[] + priority: number + isActive: boolean +} + +export interface IAgentRun { + _id?: string + runName: string + status: RunStatus + agents: IAgentConfig[] + currentAgent?: string + startedAt?: Date + completedAt?: Date + createdAt?: Date + updatedAt?: Date +} + +export interface IAgentStepLog { + _id?: string + runId: string + agentId: string + inputPrompt: string + output: string + status: StepStatus + executionTimeMs: number + createdAt?: Date +} diff --git a/LocalMind-Backend/src/api/v1/Agent/agent.validator.ts b/LocalMind-Backend/src/api/v1/Agent/agent.validator.ts new file mode 100644 index 0000000..32d52bb --- /dev/null +++ b/LocalMind-Backend/src/api/v1/Agent/agent.validator.ts @@ -0,0 +1,17 @@ +import { z } from 'zod' +import { AgentRoles, AgentConstants } from './agent.constant' + +export const agentConfigSchema = z.object({ + agentId: z.string().min(1), + name: z.string().min(1), + type: z.enum(AgentRoles), + systemPrompt: z.string().min(1), + tools: z.array(z.string()), + priority: z.number().default(AgentConstants.DEFAULT_PRIORITY), + isActive: z.boolean().default(true) +}) + +export const createRunSchema = z.object({ + runName: z.string().min(1), + agents: z.array(agentConfigSchema).max(AgentConstants.MAX_AGENTS_PER_RUN) +}) diff --git a/LocalMind-Backend/src/routes/app.ts b/LocalMind-Backend/src/routes/app.ts index 4905423..1f3d8a7 100644 --- a/LocalMind-Backend/src/routes/app.ts +++ b/LocalMind-Backend/src/routes/app.ts @@ -9,6 +9,7 @@ import { DataSetRoutes } from '../api/v1/DataSet/v1/DataSet.routes' import { userRoutes } from '../api/v1/user/user.routes' import { OllamaRouter } from '../api/v1/Ai-model/Ollama/Ollama.routes' import { GroqRouter } from '../api/v1/Ai-model/Groq/Groq.routes' +import { agentRoutes } from '../api/v1/Agent/agent.routes' logger.token('time', () => new Date().toLocaleString()) @@ -19,13 +20,13 @@ app.use(express.json()) app.use(express.urlencoded({ extended: true })) // API routes -app.use('/api', GoogleRoutes, userRoutes, DataSetRoutes, OllamaRouter, GroqRouter) +app.use('/api', GoogleRoutes, userRoutes, DataSetRoutes, OllamaRouter, GroqRouter, agentRoutes) // Serve static files from public directory (for frontend in production) const publicPath = path.join(__dirname, '../../public') if (fs.existsSync(publicPath)) { app.use(express.static(publicPath)) - + // SPA fallback: serve index.html for all non-API routes app.get('*', (req, res) => { if (!req.path.startsWith('/api')) {