Major refactor: Replace fixed roles with N named AI agents
Backend changes: - Refactor mistralClient to generic agent prompts (not role-based) - Implement streaming responses with thinking extraction - Create nameGenerator service for random AI names - Refactor collaborativeOrchestrator for N agents - Implement true convergence (N agents with no changes) - Add section merging for partial document updates - Each AI modifies only ONE section, not entire document - Broadcast agent_working and agent_thinking events in real-time - Update routes for new orchestrator API Features: - Support 3-50 AI agents instead of fixed 7 roles - Real-time thinking/reasoning streaming - Partial document updates (section-based) - True convergence tracking - Automatic round progression - Section extraction and merging Next: Frontend enhancements for visualization
This commit is contained in:
parent
97c4ad9f6c
commit
b566671ea4
@ -1,7 +1,7 @@
|
||||
import express from 'express';
|
||||
import collaborativeOrchestrator from '../services/collaborativeOrchestrator.js';
|
||||
import express from 'express'
|
||||
import collaborativeOrchestrator from '../services/collaborativeOrchestrator.js'
|
||||
|
||||
const router = express.Router();
|
||||
const router = express.Router()
|
||||
|
||||
/**
|
||||
* POST /api/collaborate
|
||||
@ -9,73 +9,74 @@ const router = express.Router();
|
||||
*/
|
||||
router.post('/', async (req, res) => {
|
||||
try {
|
||||
const { prompt, documentFormat = 'md', agentCount = 7 } = req.body;
|
||||
const { prompt, documentFormat = 'md', agentCount = 7 } = req.body
|
||||
|
||||
if (!prompt || prompt.trim().length === 0) {
|
||||
return res.status(400).json({ error: 'Prompt is required' });
|
||||
return res.status(400).json({ error: 'Prompt is required' })
|
||||
}
|
||||
|
||||
if (!['md', 'txt'].includes(documentFormat)) {
|
||||
return res.status(400).json({ error: 'Document format must be "md" or "txt"' });
|
||||
return res.status(400).json({ error: 'Document format must be "md" or "txt"' })
|
||||
}
|
||||
|
||||
// Validate agent count
|
||||
const validAgentCount = Math.min(Math.max(agentCount, 3), 50)
|
||||
|
||||
const sessionId = collaborativeOrchestrator.createSession(
|
||||
prompt,
|
||||
documentFormat,
|
||||
Math.min(agentCount, 7)
|
||||
);
|
||||
validAgentCount
|
||||
)
|
||||
|
||||
const session = collaborativeOrchestrator.getSessionDetails(sessionId);
|
||||
const sessionInfo = collaborativeOrchestrator.getSessionInfo(sessionId)
|
||||
|
||||
res.json({
|
||||
sessionId,
|
||||
prompt,
|
||||
documentFormat,
|
||||
agentCount: validAgentCount,
|
||||
status: 'created',
|
||||
agents: session.agents.map(a => a.role),
|
||||
agents: sessionInfo.agents,
|
||||
message: 'Collaborative session created. Start the session to begin collaboration.'
|
||||
});
|
||||
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Error creating collaborative session:', error);
|
||||
res.status(500).json({ error: 'Failed to create collaborative session' });
|
||||
console.error('Error creating collaborative session:', error)
|
||||
res.status(500).json({ error: 'Failed to create collaborative session' })
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
/**
|
||||
* POST /api/collaborate/:id/start
|
||||
* Start the collaborative session (Lead Architect creates initial document)
|
||||
* Start the collaborative session
|
||||
*/
|
||||
router.post('/:id/start', async (req, res) => {
|
||||
try {
|
||||
const sessionId = parseInt(req.params.id);
|
||||
const session = collaborativeOrchestrator.getSession(sessionId);
|
||||
const sessionId = parseInt(req.params.id)
|
||||
const session = collaborativeOrchestrator.getSession(sessionId)
|
||||
|
||||
if (!session) {
|
||||
return res.status(404).json({ error: 'Session not found' });
|
||||
return res.status(404).json({ error: 'Session not found' })
|
||||
}
|
||||
|
||||
if (session.status !== 'created') {
|
||||
return res.status(400).json({ error: 'Session has already been started or is no longer available' });
|
||||
return res.status(400).json({ error: 'Session has already been started' })
|
||||
}
|
||||
|
||||
// Start the session asynchronously
|
||||
res.json({
|
||||
sessionId,
|
||||
status: 'starting',
|
||||
message: 'Session is starting. Initial document is being created...'
|
||||
});
|
||||
|
||||
// Start asynchronously without waiting
|
||||
collaborativeOrchestrator.startCollaborativeSession(sessionId).catch(error => {
|
||||
console.error('Error starting collaborative session:', error);
|
||||
});
|
||||
})
|
||||
|
||||
// Start asynchronously
|
||||
collaborativeOrchestrator.startSession(sessionId).catch(error => {
|
||||
console.error('Error starting session:', error)
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Error starting collaborative session:', error);
|
||||
res.status(500).json({ error: 'Failed to start collaborative session' });
|
||||
console.error('Error starting session:', error)
|
||||
res.status(500).json({ error: 'Failed to start session' })
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
/**
|
||||
* POST /api/collaborate/:id/round
|
||||
@ -83,85 +84,80 @@ router.post('/:id/start', async (req, res) => {
|
||||
*/
|
||||
router.post('/:id/round', async (req, res) => {
|
||||
try {
|
||||
const sessionId = parseInt(req.params.id);
|
||||
const session = collaborativeOrchestrator.getSession(sessionId);
|
||||
const sessionId = parseInt(req.params.id)
|
||||
const session = collaborativeOrchestrator.getSession(sessionId)
|
||||
|
||||
if (!session) {
|
||||
return res.status(404).json({ error: 'Session not found' });
|
||||
return res.status(404).json({ error: 'Session not found' })
|
||||
}
|
||||
|
||||
if (session.status !== 'ongoing') {
|
||||
return res.status(400).json({ error: 'Session is not in ongoing status' });
|
||||
return res.status(400).json({ error: 'Session is not in ongoing status' })
|
||||
}
|
||||
|
||||
const activeSession = collaborativeOrchestrator.activeSessions.get(sessionId);
|
||||
const activeSession = collaborativeOrchestrator.activeSessions.get(sessionId)
|
||||
if (!activeSession?.started) {
|
||||
return res.status(400).json({ error: 'Session has not been started yet' });
|
||||
return res.status(400).json({ error: 'Session has not been started yet' })
|
||||
}
|
||||
|
||||
// Run round asynchronously
|
||||
const roundNumber = activeSession.conversationHistory.length + 1
|
||||
|
||||
res.json({
|
||||
sessionId,
|
||||
roundNumber: activeSession.currentRound + 1,
|
||||
roundNumber,
|
||||
status: 'running',
|
||||
message: 'Review round in progress...'
|
||||
});
|
||||
})
|
||||
|
||||
// Run asynchronously without waiting
|
||||
// Run asynchronously
|
||||
collaborativeOrchestrator.runRound(sessionId).catch(error => {
|
||||
console.error('Error running round:', error);
|
||||
console.error('Error running round:', error)
|
||||
collaborativeOrchestrator.broadcast(sessionId, {
|
||||
type: 'round_error',
|
||||
sessionId,
|
||||
error: error.message
|
||||
});
|
||||
});
|
||||
|
||||
})
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Error running round:', error);
|
||||
res.status(500).json({ error: 'Failed to run round' });
|
||||
console.error('Error running round:', error)
|
||||
res.status(500).json({ error: 'Failed to run round' })
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
/**
|
||||
* GET /api/collaborate/:id
|
||||
* Get session details with full history and current document
|
||||
* Get session details
|
||||
*/
|
||||
router.get('/:id', (req, res) => {
|
||||
try {
|
||||
const sessionId = parseInt(req.params.id);
|
||||
const session = collaborativeOrchestrator.getSessionDetails(sessionId);
|
||||
const sessionId = parseInt(req.params.id)
|
||||
const session = collaborativeOrchestrator.getSession(sessionId)
|
||||
|
||||
if (!session) {
|
||||
return res.status(404).json({ error: 'Session not found' });
|
||||
return res.status(404).json({ error: 'Session not found' })
|
||||
}
|
||||
|
||||
const sessionInfo = collaborativeOrchestrator.getSessionInfo(sessionId)
|
||||
const versions = collaborativeOrchestrator.getDocumentVersions(sessionId)
|
||||
|
||||
res.json({
|
||||
sessionId: session.id,
|
||||
initialPrompt: session.initial_prompt,
|
||||
documentFormat: session.document_format,
|
||||
status: session.status,
|
||||
sessionId: sessionInfo.id,
|
||||
status: sessionInfo.status,
|
||||
agents: sessionInfo.agents,
|
||||
agentCount: sessionInfo.agentCount,
|
||||
currentRound: sessionInfo.currentRound,
|
||||
currentDocument: sessionInfo.currentDocument,
|
||||
versionNumber: sessionInfo.versionNumber,
|
||||
documentVersionCount: versions.length,
|
||||
conversationHistory: sessionInfo.conversationHistory,
|
||||
createdAt: session.created_at,
|
||||
completedAt: session.completed_at,
|
||||
currentRound: session.currentRound,
|
||||
agents: session.agents.map(a => a.role),
|
||||
currentDocument: session.currentDocument,
|
||||
documentVersionCount: session.versions.length,
|
||||
conversationHistory: session.conversationHistory,
|
||||
versions: session.versions.map(v => ({
|
||||
versionNumber: v.version_number,
|
||||
modifiedBy: v.modified_by,
|
||||
modificationReason: v.modification_reason,
|
||||
roundNumber: v.round_number,
|
||||
createdAt: v.created_at
|
||||
}))
|
||||
});
|
||||
|
||||
completedAt: session.completed_at
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Error fetching session details:', error);
|
||||
res.status(500).json({ error: 'Failed to fetch session details' });
|
||||
console.error('Error fetching session details:', error)
|
||||
res.status(500).json({ error: 'Failed to fetch session details' })
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
/**
|
||||
* GET /api/collaborate/:id/document
|
||||
@ -169,29 +165,27 @@ router.get('/:id', (req, res) => {
|
||||
*/
|
||||
router.get('/:id/document', (req, res) => {
|
||||
try {
|
||||
const sessionId = parseInt(req.params.id);
|
||||
const session = collaborativeOrchestrator.getSession(sessionId);
|
||||
const sessionId = parseInt(req.params.id)
|
||||
const session = collaborativeOrchestrator.getSession(sessionId)
|
||||
|
||||
if (!session) {
|
||||
return res.status(404).json({ error: 'Session not found' });
|
||||
return res.status(404).json({ error: 'Session not found' })
|
||||
}
|
||||
|
||||
const activeSession = collaborativeOrchestrator.activeSessions.get(sessionId);
|
||||
const currentDocument = activeSession?.currentDocument || '';
|
||||
const activeSession = collaborativeOrchestrator.activeSessions.get(sessionId)
|
||||
const currentDocument = activeSession?.currentDocument || ''
|
||||
|
||||
// Determine content type based on format
|
||||
const contentType = session.document_format === 'md'
|
||||
? 'text/markdown; charset=utf-8'
|
||||
: 'text/plain; charset=utf-8';
|
||||
|
||||
res.set('Content-Type', contentType);
|
||||
res.send(currentDocument);
|
||||
: 'text/plain; charset=utf-8'
|
||||
|
||||
res.set('Content-Type', contentType)
|
||||
res.send(currentDocument)
|
||||
} catch (error) {
|
||||
console.error('Error fetching document:', error);
|
||||
res.status(500).json({ error: 'Failed to fetch document' });
|
||||
console.error('Error fetching document:', error)
|
||||
res.status(500).json({ error: 'Failed to fetch document' })
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
/**
|
||||
* GET /api/collaborate/:id/versions/:versionNumber
|
||||
@ -199,26 +193,21 @@ router.get('/:id/document', (req, res) => {
|
||||
*/
|
||||
router.get('/:id/versions/:versionNumber', (req, res) => {
|
||||
try {
|
||||
const sessionId = parseInt(req.params.id);
|
||||
const versionNumber = parseInt(req.params.versionNumber);
|
||||
const sessionId = parseInt(req.params.id)
|
||||
const versionNumber = parseInt(req.params.versionNumber)
|
||||
|
||||
const session = collaborativeOrchestrator.getSession(sessionId);
|
||||
const session = collaborativeOrchestrator.getSession(sessionId)
|
||||
if (!session) {
|
||||
return res.status(404).json({ error: 'Session not found' });
|
||||
return res.status(404).json({ error: 'Session not found' })
|
||||
}
|
||||
|
||||
const versions = collaborativeOrchestrator.getDocumentVersions(sessionId);
|
||||
const version = versions.find(v => v.version_number === versionNumber);
|
||||
const versions = collaborativeOrchestrator.getDocumentVersions(sessionId)
|
||||
const version = versions.find(v => v.version_number === versionNumber)
|
||||
|
||||
if (!version) {
|
||||
return res.status(404).json({ error: 'Version not found' });
|
||||
return res.status(404).json({ error: 'Version not found' })
|
||||
}
|
||||
|
||||
const contentType = session.document_format === 'md'
|
||||
? 'text/markdown; charset=utf-8'
|
||||
: 'text/plain; charset=utf-8';
|
||||
|
||||
res.set('Content-Type', contentType);
|
||||
res.json({
|
||||
versionNumber: version.version_number,
|
||||
modifiedBy: version.modified_by,
|
||||
@ -226,13 +215,12 @@ router.get('/:id/versions/:versionNumber', (req, res) => {
|
||||
roundNumber: version.round_number,
|
||||
createdAt: version.created_at,
|
||||
content: version.content
|
||||
});
|
||||
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Error fetching version:', error);
|
||||
res.status(500).json({ error: 'Failed to fetch version' });
|
||||
console.error('Error fetching version:', error)
|
||||
res.status(500).json({ error: 'Failed to fetch version' })
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
/**
|
||||
* POST /api/collaborate/:id/complete
|
||||
@ -240,25 +228,24 @@ router.get('/:id/versions/:versionNumber', (req, res) => {
|
||||
*/
|
||||
router.post('/:id/complete', (req, res) => {
|
||||
try {
|
||||
const sessionId = parseInt(req.params.id);
|
||||
const session = collaborativeOrchestrator.getSession(sessionId);
|
||||
const sessionId = parseInt(req.params.id)
|
||||
const session = collaborativeOrchestrator.getSession(sessionId)
|
||||
|
||||
if (!session) {
|
||||
return res.status(404).json({ error: 'Session not found' });
|
||||
return res.status(404).json({ error: 'Session not found' })
|
||||
}
|
||||
|
||||
collaborativeOrchestrator.completeSession(sessionId);
|
||||
collaborativeOrchestrator.completeSession(sessionId)
|
||||
|
||||
res.json({
|
||||
sessionId,
|
||||
status: 'completed',
|
||||
message: 'Session completed successfully'
|
||||
});
|
||||
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Error completing session:', error);
|
||||
res.status(500).json({ error: 'Failed to complete session' });
|
||||
console.error('Error completing session:', error)
|
||||
res.status(500).json({ error: 'Failed to complete session' })
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
export default router;
|
||||
export default router
|
||||
|
||||
@ -1,21 +1,11 @@
|
||||
import db from '../db/schema.js';
|
||||
import { generateAgentResponse } from './mistralClient.js';
|
||||
import db from '../db/schema.js'
|
||||
import { generateAgentResponseSync, extractSection, extractThinking } from './mistralClient.js'
|
||||
import { getRandomNames } from './nameGenerator.js'
|
||||
|
||||
class CollaborativeOrchestrator {
|
||||
constructor() {
|
||||
this.activeSessions = new Map();
|
||||
this.wsClients = new Map(); // sessionId -> Set of WebSocket clients
|
||||
|
||||
// Define collaborative agents with specialized roles
|
||||
this.agents = [
|
||||
{ role: 'lead_architect', isLead: true },
|
||||
{ role: 'backend_engineer', isLead: false },
|
||||
{ role: 'frontend_engineer', isLead: false },
|
||||
{ role: 'ui_designer', isLead: false },
|
||||
{ role: 'devops_engineer', isLead: false },
|
||||
{ role: 'product_manager', isLead: false },
|
||||
{ role: 'security_specialist', isLead: false }
|
||||
];
|
||||
this.activeSessions = new Map()
|
||||
this.wsClients = new Map() // sessionId -> Set of WebSocket clients
|
||||
}
|
||||
|
||||
/**
|
||||
@ -23,9 +13,9 @@ class CollaborativeOrchestrator {
|
||||
*/
|
||||
registerWSClient(sessionId, ws) {
|
||||
if (!this.wsClients.has(sessionId)) {
|
||||
this.wsClients.set(sessionId, new Set());
|
||||
this.wsClients.set(sessionId, new Set())
|
||||
}
|
||||
this.wsClients.get(sessionId).add(ws);
|
||||
this.wsClients.get(sessionId).add(ws)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -33,7 +23,7 @@ class CollaborativeOrchestrator {
|
||||
*/
|
||||
unregisterWSClient(sessionId, ws) {
|
||||
if (this.wsClients.has(sessionId)) {
|
||||
this.wsClients.get(sessionId).delete(ws);
|
||||
this.wsClients.get(sessionId).delete(ws)
|
||||
}
|
||||
}
|
||||
|
||||
@ -42,432 +32,323 @@ class CollaborativeOrchestrator {
|
||||
*/
|
||||
broadcast(sessionId, message) {
|
||||
if (this.wsClients.has(sessionId)) {
|
||||
const data = JSON.stringify(message);
|
||||
const data = JSON.stringify(message)
|
||||
this.wsClients.get(sessionId).forEach(ws => {
|
||||
if (ws.readyState === 1) { // OPEN
|
||||
ws.send(data);
|
||||
ws.send(data)
|
||||
}
|
||||
});
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new collaborative session
|
||||
* Create a new collaborative session with N random-named agents
|
||||
*/
|
||||
createSession(initialPrompt, documentFormat = 'md', agentCount = 7) {
|
||||
const stmt = db.prepare(
|
||||
'INSERT INTO collaborative_sessions (initial_prompt, document_format, status) VALUES (?, ?, ?)'
|
||||
);
|
||||
const result = stmt.run(initialPrompt, documentFormat, 'created');
|
||||
const sessionId = result.lastInsertRowid;
|
||||
)
|
||||
const result = stmt.run(initialPrompt, documentFormat, 'created')
|
||||
const sessionId = result.lastInsertRowid
|
||||
|
||||
// Select the agents to use
|
||||
const selectedAgents = this.agents.slice(0, Math.min(agentCount, this.agents.length));
|
||||
// Generate random names for agents
|
||||
const agentNames = getRandomNames(Math.min(agentCount, 50))
|
||||
|
||||
this.activeSessions.set(sessionId, {
|
||||
id: sessionId,
|
||||
initialPrompt,
|
||||
documentFormat,
|
||||
agents: selectedAgents,
|
||||
currentRound: 0,
|
||||
agents: agentNames, // Array of agent names
|
||||
agentCount,
|
||||
currentAgentIndex: 0,
|
||||
currentDocument: null,
|
||||
versionNumber: 0,
|
||||
conversationHistory: [],
|
||||
started: false
|
||||
});
|
||||
started: false,
|
||||
consecutiveNoChanges: 0, // Counter for convergence
|
||||
lastModifiedAgent: null
|
||||
})
|
||||
|
||||
return sessionId;
|
||||
return sessionId
|
||||
}
|
||||
|
||||
/**
|
||||
* Get session by ID
|
||||
*/
|
||||
getSession(sessionId) {
|
||||
const stmt = db.prepare('SELECT * FROM collaborative_sessions WHERE id = ?');
|
||||
return stmt.get(sessionId);
|
||||
const stmt = db.prepare('SELECT * FROM collaborative_sessions WHERE id = ?')
|
||||
return stmt.get(sessionId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all versions of a document
|
||||
* Start a session - create initial document with first agent
|
||||
*/
|
||||
getDocumentVersions(sessionId) {
|
||||
const stmt = db.prepare(
|
||||
`SELECT * FROM document_versions
|
||||
WHERE session_id = ?
|
||||
ORDER BY version_number ASC`
|
||||
);
|
||||
return stmt.all(sessionId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get latest document version
|
||||
*/
|
||||
getLatestDocument(sessionId) {
|
||||
const stmt = db.prepare(
|
||||
`SELECT * FROM document_versions
|
||||
WHERE session_id = ?
|
||||
ORDER BY version_number DESC
|
||||
LIMIT 1`
|
||||
);
|
||||
return stmt.get(sessionId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Save a document version
|
||||
*/
|
||||
saveDocumentVersion(sessionId, content, modifiedBy, reason, roundNumber) {
|
||||
// Get current version number
|
||||
const lastVersion = this.getLatestDocument(sessionId);
|
||||
const versionNumber = (lastVersion?.version_number || 0) + 1;
|
||||
|
||||
const stmt = db.prepare(
|
||||
`INSERT INTO document_versions
|
||||
(session_id, version_number, content, modified_by, modification_reason, round_number)
|
||||
VALUES (?, ?, ?, ?, ?, ?)`
|
||||
);
|
||||
|
||||
const result = stmt.run(sessionId, versionNumber, content, modifiedBy, reason, roundNumber);
|
||||
return result.lastInsertRowid;
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the collaborative session - Lead Architect creates first version
|
||||
*/
|
||||
async startCollaborativeSession(sessionId) {
|
||||
async startSession(sessionId) {
|
||||
try {
|
||||
const session = this.getSession(sessionId);
|
||||
if (!session) {
|
||||
throw new Error('Session not found');
|
||||
}
|
||||
const session = this.activeSessions.get(sessionId)
|
||||
if (!session || session.started) return
|
||||
|
||||
const activeSession = this.activeSessions.get(sessionId);
|
||||
if (!activeSession) {
|
||||
throw new Error('Active session not found');
|
||||
}
|
||||
|
||||
// Update session status from 'created' to 'ongoing'
|
||||
const updateStmt = db.prepare('UPDATE collaborative_sessions SET status = ? WHERE id = ?');
|
||||
updateStmt.run('ongoing', sessionId);
|
||||
|
||||
activeSession.started = true;
|
||||
|
||||
// Broadcast session start
|
||||
this.broadcast(sessionId, {
|
||||
type: 'session_start',
|
||||
sessionId,
|
||||
message: 'Collaborative session started. Lead Architect is creating initial document...'
|
||||
});
|
||||
|
||||
// Get Lead Architect (first agent)
|
||||
const leadArchitect = activeSession.agents.find(a => a.isLead);
|
||||
const firstAgent = session.agents[0]
|
||||
|
||||
// Generate initial document
|
||||
const response = await generateAgentResponse(
|
||||
leadArchitect.role,
|
||||
`${session.initial_prompt}\n\nYou are the Lead Architect. Create the INITIAL version of a comprehensive project document in ${session.document_format} format.
|
||||
const initialResponse = await generateAgentResponseSync(
|
||||
firstAgent,
|
||||
session.initialPrompt,
|
||||
''
|
||||
)
|
||||
|
||||
This document will be reviewed and modified by other team members (Backend Engineer, Frontend Engineer, UI Designer, DevOps Engineer, Product Manager, Security Specialist).
|
||||
const initialDocument = extractSection(initialResponse)
|
||||
const thinking = extractThinking(initialResponse)
|
||||
|
||||
Create a structured, complete document that outlines:
|
||||
- Project overview and goals
|
||||
- Architecture overview
|
||||
- Technology stack decisions
|
||||
- Project structure
|
||||
- Key features
|
||||
- Non-functional requirements
|
||||
- Timeline and phases
|
||||
// Save to DB
|
||||
const insertStmt = db.prepare(
|
||||
'INSERT INTO document_versions (session_id, version_number, content, modified_by, modification_reason, round_number) VALUES (?, ?, ?, ?, ?, ?)'
|
||||
)
|
||||
insertStmt.run(sessionId, 0, initialDocument, firstAgent, 'Initial document creation', 0)
|
||||
|
||||
Output ONLY the raw document content in ${session.document_format} format, nothing else.`
|
||||
);
|
||||
// Update session
|
||||
session.currentDocument = initialDocument
|
||||
session.versionNumber = 0
|
||||
session.started = true
|
||||
session.consecutiveNoChanges = 0
|
||||
|
||||
// Extract document from response
|
||||
let documentContent = response.proposal || response;
|
||||
if (typeof documentContent === 'object') {
|
||||
documentContent = JSON.stringify(documentContent, null, 2);
|
||||
}
|
||||
|
||||
// Save as first version
|
||||
this.saveDocumentVersion(
|
||||
sessionId,
|
||||
documentContent,
|
||||
leadArchitect.role,
|
||||
'Initial document creation',
|
||||
1
|
||||
);
|
||||
|
||||
// Update in-memory session
|
||||
activeSession.currentDocument = documentContent;
|
||||
activeSession.versionNumber = 1;
|
||||
activeSession.currentRound = 1;
|
||||
activeSession.conversationHistory.push({
|
||||
agent: leadArchitect.role,
|
||||
action: 'created_initial_document',
|
||||
documentVersion: 1
|
||||
});
|
||||
// Update DB status
|
||||
const updateStmt = db.prepare('UPDATE collaborative_sessions SET status = ? WHERE id = ?')
|
||||
updateStmt.run('ongoing', sessionId)
|
||||
|
||||
// Broadcast initial document
|
||||
this.broadcast(sessionId, {
|
||||
type: 'initial_document_created',
|
||||
sessionId,
|
||||
agent: leadArchitect.role,
|
||||
documentVersion: 1,
|
||||
document: documentContent,
|
||||
message: `Lead Architect (${leadArchitect.role}) created initial document. Starting review rounds...`
|
||||
});
|
||||
|
||||
return {
|
||||
sessionId,
|
||||
documentVersion: 1,
|
||||
document: documentContent,
|
||||
agents: activeSession.agents
|
||||
};
|
||||
content: initialDocument,
|
||||
agentName: firstAgent,
|
||||
thinking,
|
||||
roundNumber: 0
|
||||
})
|
||||
|
||||
// Auto-start first round
|
||||
setTimeout(() => this.runRound(sessionId), 2000)
|
||||
} catch (error) {
|
||||
console.error('Error starting collaborative session:', error);
|
||||
this.failSession(sessionId);
|
||||
|
||||
console.error('Error starting session:', error)
|
||||
this.broadcast(sessionId, {
|
||||
type: 'session_error',
|
||||
sessionId,
|
||||
error: error.message
|
||||
});
|
||||
|
||||
throw error;
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Run one round of review (each agent reviews and potentially modifies)
|
||||
* Run a round of collaborative review
|
||||
*/
|
||||
async runRound(sessionId) {
|
||||
try {
|
||||
const session = this.getSession(sessionId);
|
||||
if (!session) {
|
||||
throw new Error('Session not found');
|
||||
}
|
||||
const session = this.activeSessions.get(sessionId)
|
||||
if (!session) return
|
||||
|
||||
const activeSession = this.activeSessions.get(sessionId);
|
||||
if (!activeSession) {
|
||||
throw new Error('Active session not found');
|
||||
}
|
||||
const roundNumber = session.conversationHistory.length + 1
|
||||
const agentsInRound = session.agents
|
||||
const modifiedAgents = []
|
||||
|
||||
if (!activeSession.currentDocument) {
|
||||
throw new Error('No document to review');
|
||||
}
|
||||
|
||||
activeSession.currentRound += 1;
|
||||
const roundNumber = activeSession.currentRound;
|
||||
// Each agent reviews the document
|
||||
for (let i = 0; i < agentsInRound.length; i++) {
|
||||
const agentName = agentsInRound[i]
|
||||
|
||||
// Broadcast that this agent is working
|
||||
this.broadcast(sessionId, {
|
||||
type: 'round_start',
|
||||
sessionId,
|
||||
roundNumber,
|
||||
message: `Starting review round ${roundNumber}. Agents will review and modify the document...`
|
||||
});
|
||||
type: 'agent_working',
|
||||
agentName,
|
||||
roundNumber
|
||||
})
|
||||
|
||||
const agentsMadeChanges = [];
|
||||
const roundAgents = activeSession.agents.map(a => a.role).join(',');
|
||||
try {
|
||||
const response = await generateAgentResponseSync(
|
||||
agentName,
|
||||
session.initialPrompt,
|
||||
session.currentDocument
|
||||
)
|
||||
|
||||
// Process each agent sequentially
|
||||
for (const agent of activeSession.agents) {
|
||||
const thinking = extractThinking(response)
|
||||
const section = extractSection(response)
|
||||
|
||||
// Broadcast agent's thinking in real-time
|
||||
this.broadcast(sessionId, {
|
||||
type: 'agent_reviewing',
|
||||
sessionId,
|
||||
agent: agent.role,
|
||||
roundNumber,
|
||||
message: `${agent.role} is reviewing the document...`
|
||||
});
|
||||
|
||||
// Call agent to review and potentially modify document
|
||||
const response = await generateAgentResponse(
|
||||
agent.role,
|
||||
`${session.initial_prompt}\n\n
|
||||
CURRENT DOCUMENT (${session.document_format} format):
|
||||
\`\`\`${session.document_format}
|
||||
${activeSession.currentDocument}
|
||||
\`\`\`
|
||||
|
||||
You are the ${agent.role}. Review this document from your perspective. Your task is to:
|
||||
1. Read and understand the entire document
|
||||
2. Identify areas that need improvement or modifications from your expertise area
|
||||
3. Provide improvements, additions, or modifications
|
||||
|
||||
IMPORTANT:
|
||||
- If you decide to modify the document, output ONLY the complete modified document in ${session.document_format} format
|
||||
- If the document is already excellent and needs no changes, output: NO_CHANGES
|
||||
- Do not include explanations, just the document or NO_CHANGES
|
||||
|
||||
Modification focus for ${agent.role}:
|
||||
${this.getAgentFocusArea(agent.role)}`
|
||||
);
|
||||
|
||||
let responseText = response.proposal || response;
|
||||
if (typeof responseText === 'object') {
|
||||
responseText = JSON.stringify(responseText, null, 2);
|
||||
}
|
||||
type: 'agent_thinking',
|
||||
agentName,
|
||||
thinking,
|
||||
roundNumber
|
||||
})
|
||||
|
||||
// Check if agent made changes
|
||||
if (responseText.trim() !== 'NO_CHANGES') {
|
||||
// Calculate diff
|
||||
const diff = this.calculateDiff(activeSession.currentDocument, responseText);
|
||||
if (section !== 'Section is good, no changes needed' && !section.includes('no changes needed')) {
|
||||
// Merge section into document
|
||||
const updatedDocument = this.mergeSection(session.currentDocument, section)
|
||||
|
||||
// Save new version
|
||||
this.saveDocumentVersion(
|
||||
if (updatedDocument !== session.currentDocument) {
|
||||
session.currentDocument = updatedDocument
|
||||
session.versionNumber++
|
||||
modifiedAgents.push(agentName)
|
||||
|
||||
// Save version to DB
|
||||
const insertStmt = db.prepare(
|
||||
'INSERT INTO document_versions (session_id, version_number, content, modified_by, modification_reason, round_number) VALUES (?, ?, ?, ?, ?, ?)'
|
||||
)
|
||||
insertStmt.run(
|
||||
sessionId,
|
||||
responseText,
|
||||
agent.role,
|
||||
diff.summary,
|
||||
session.versionNumber,
|
||||
updatedDocument,
|
||||
agentName,
|
||||
`Round ${roundNumber} modifications`,
|
||||
roundNumber
|
||||
);
|
||||
|
||||
activeSession.currentDocument = responseText;
|
||||
activeSession.versionNumber += 1;
|
||||
agentsMadeChanges.push(agent.role);
|
||||
)
|
||||
|
||||
// Broadcast modification
|
||||
this.broadcast(sessionId, {
|
||||
type: 'document_modified',
|
||||
sessionId,
|
||||
agent: agent.role,
|
||||
roundNumber,
|
||||
documentVersion: activeSession.versionNumber,
|
||||
document: responseText,
|
||||
changeSummary: diff.summary,
|
||||
message: `${agent.role} modified the document`
|
||||
});
|
||||
} else {
|
||||
this.broadcast(sessionId, {
|
||||
type: 'agent_no_changes',
|
||||
sessionId,
|
||||
agent: agent.role,
|
||||
roundNumber,
|
||||
message: `${agent.role} reviewed the document and found no changes needed`
|
||||
});
|
||||
content: updatedDocument,
|
||||
modifiedBy: agentName,
|
||||
section,
|
||||
roundNumber
|
||||
})
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error with agent ${agentName}:`, error)
|
||||
}
|
||||
}
|
||||
|
||||
// Save round completion
|
||||
// Track convergence
|
||||
if (modifiedAgents.length === 0) {
|
||||
session.consecutiveNoChanges++
|
||||
} else {
|
||||
session.consecutiveNoChanges = 0
|
||||
}
|
||||
|
||||
// Save round to DB
|
||||
const roundStmt = db.prepare(
|
||||
`INSERT INTO document_rounds
|
||||
(session_id, round_number, agents_in_round, agents_made_changes, completed_at)
|
||||
VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP)`
|
||||
);
|
||||
'INSERT INTO document_rounds (session_id, round_number, agents_in_round, agents_made_changes) VALUES (?, ?, ?, ?)'
|
||||
)
|
||||
roundStmt.run(
|
||||
sessionId,
|
||||
roundNumber,
|
||||
roundAgents,
|
||||
agentsMadeChanges.join(',')
|
||||
);
|
||||
JSON.stringify(agentsInRound),
|
||||
JSON.stringify(modifiedAgents)
|
||||
)
|
||||
|
||||
const conversationEntry = {
|
||||
// Add to history
|
||||
session.conversationHistory.push({
|
||||
roundNumber,
|
||||
agentsMadeChanges: agentsMadeChanges.length > 0 ? agentsMadeChanges : 'none'
|
||||
};
|
||||
|
||||
activeSession.conversationHistory.push(conversationEntry);
|
||||
|
||||
// Check for convergence
|
||||
const hasConverged = agentsMadeChanges.length === 0;
|
||||
agentsMadeChanges: modifiedAgents,
|
||||
timestamp: Date.now()
|
||||
})
|
||||
|
||||
// Broadcast round complete
|
||||
const hasConverged = session.consecutiveNoChanges >= session.agentCount
|
||||
this.broadcast(sessionId, {
|
||||
type: 'round_complete',
|
||||
sessionId,
|
||||
roundNumber,
|
||||
agentsMadeChanges: agentsMadeChanges.length,
|
||||
agentsWhoModified: agentsMadeChanges,
|
||||
agentsMadeChanges: modifiedAgents,
|
||||
hasConverged,
|
||||
message: hasConverged
|
||||
? 'Convergence reached! No more changes needed.'
|
||||
: `Round ${roundNumber} complete. ${agentsMadeChanges.length} agent(s) made changes.`
|
||||
});
|
||||
|
||||
return {
|
||||
roundNumber,
|
||||
agentsMadeChanges,
|
||||
hasConverged,
|
||||
documentVersion: activeSession.versionNumber
|
||||
};
|
||||
consecutiveNoChanges: session.consecutiveNoChanges
|
||||
})
|
||||
|
||||
// Auto-schedule next round if not converged
|
||||
if (!hasConverged && session.consecutiveNoChanges < session.agentCount) {
|
||||
setTimeout(() => this.runRound(sessionId), 2000)
|
||||
} else if (hasConverged) {
|
||||
// All agents agreed, auto-complete
|
||||
setTimeout(() => this.completeSession(sessionId), 2000)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error running round:', error);
|
||||
throw error;
|
||||
console.error('Error running round:', error)
|
||||
this.broadcast(sessionId, {
|
||||
type: 'session_error',
|
||||
error: error.message
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get focus area for each agent
|
||||
* Merge a modified section into the document
|
||||
*/
|
||||
getAgentFocusArea(agentRole) {
|
||||
const focusAreas = {
|
||||
lead_architect: 'High-level architecture, technology stack, system design, scalability',
|
||||
backend_engineer: 'APIs, databases, backend services, data models, performance, security',
|
||||
frontend_engineer: 'UI components, state management, performance, user interactions, frameworks',
|
||||
ui_designer: 'User experience, accessibility, visual design, UI patterns, usability',
|
||||
devops_engineer: 'Deployment, infrastructure, CI/CD, monitoring, containers, scalability',
|
||||
product_manager: 'Features, user needs, roadmap, priorities, market fit, business goals',
|
||||
security_specialist: 'Security architecture, data protection, authentication, compliance'
|
||||
};
|
||||
return focusAreas[agentRole] || 'General improvements';
|
||||
mergeSection(document, newSection) {
|
||||
if (!document || !newSection) return document
|
||||
|
||||
// Extract header from new section
|
||||
const headerMatch = newSection.match(/^(#{1,4})\s+(.+)/)
|
||||
if (!headerMatch) return document
|
||||
|
||||
const headerLevel = headerMatch[1].length
|
||||
const headerText = headerMatch[2]
|
||||
const headerRegex = new RegExp(`^${'#'.repeat(headerLevel)}\\s+${headerText.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}$`, 'gm')
|
||||
|
||||
// Find and replace the section
|
||||
if (headerRegex.test(document)) {
|
||||
// Replace existing section
|
||||
const sections = document.split(/\n(?=^#{1,4}\s+)/m)
|
||||
let merged = sections
|
||||
.map(section => {
|
||||
if (headerRegex.test(section)) {
|
||||
return newSection
|
||||
}
|
||||
return section
|
||||
})
|
||||
.join('\n')
|
||||
return merged
|
||||
} else {
|
||||
// Append new section
|
||||
return document + '\n\n' + newSection
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate diff between two document versions (simplified)
|
||||
*/
|
||||
calculateDiff(oldContent, newContent) {
|
||||
const oldLines = oldContent.split('\n');
|
||||
const newLines = newContent.split('\n');
|
||||
|
||||
const linesAdded = Math.max(0, newLines.length - oldLines.length);
|
||||
const linesRemoved = Math.max(0, oldLines.length - newLines.length);
|
||||
const changes = Math.abs(linesAdded) + Math.abs(linesRemoved);
|
||||
|
||||
const summary = `Modified document: +${linesAdded} lines, -${linesRemoved} lines (${changes} total changes)`;
|
||||
|
||||
return { summary, linesAdded, linesRemoved };
|
||||
}
|
||||
|
||||
/**
|
||||
* Complete a session
|
||||
*/
|
||||
completeSession(sessionId) {
|
||||
const session = this.activeSessions.get(sessionId);
|
||||
const document = session?.currentDocument || '';
|
||||
async completeSession(sessionId) {
|
||||
try {
|
||||
const stmt = db.prepare('UPDATE collaborative_sessions SET status = ?, completed_at = CURRENT_TIMESTAMP, final_document = ? WHERE id = ?')
|
||||
const session = this.activeSessions.get(sessionId)
|
||||
if (session) {
|
||||
stmt.run('completed', session.currentDocument, sessionId)
|
||||
}
|
||||
|
||||
const stmt = db.prepare(
|
||||
'UPDATE collaborative_sessions SET status = ?, completed_at = CURRENT_TIMESTAMP, final_document = ? WHERE id = ?'
|
||||
);
|
||||
stmt.run('completed', document, sessionId);
|
||||
|
||||
this.activeSessions.delete(sessionId);
|
||||
this.broadcast(sessionId, {
|
||||
type: 'session_completed',
|
||||
finalDocument: session?.currentDocument
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Error completing session:', error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fail a session
|
||||
* Get document versions
|
||||
*/
|
||||
failSession(sessionId) {
|
||||
getDocumentVersions(sessionId) {
|
||||
const stmt = db.prepare(
|
||||
'UPDATE collaborative_sessions SET status = ? WHERE id = ?'
|
||||
);
|
||||
stmt.run('failed', sessionId);
|
||||
|
||||
this.activeSessions.delete(sessionId);
|
||||
'SELECT * FROM document_versions WHERE session_id = ? ORDER BY version_number ASC'
|
||||
)
|
||||
return stmt.all(sessionId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get session details with full history
|
||||
* Get session info for API response
|
||||
*/
|
||||
getSessionDetails(sessionId) {
|
||||
const session = this.getSession(sessionId);
|
||||
const versions = this.getDocumentVersions(sessionId);
|
||||
const activeSession = this.activeSessions.get(sessionId);
|
||||
getSessionInfo(sessionId) {
|
||||
const session = this.activeSessions.get(sessionId)
|
||||
const dbSession = this.getSession(sessionId)
|
||||
|
||||
if (!session) return null
|
||||
|
||||
return {
|
||||
...session,
|
||||
versions,
|
||||
currentRound: activeSession?.currentRound || 0,
|
||||
currentDocument: activeSession?.currentDocument || null,
|
||||
agents: activeSession?.agents || [],
|
||||
conversationHistory: activeSession?.conversationHistory || []
|
||||
};
|
||||
id: sessionId,
|
||||
status: dbSession?.status,
|
||||
agents: session.agents,
|
||||
agentCount: session.agentCount,
|
||||
currentRound: session.conversationHistory.length,
|
||||
currentDocument: session.currentDocument,
|
||||
versionNumber: session.versionNumber,
|
||||
conversationHistory: session.conversationHistory
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default new CollaborativeOrchestrator();
|
||||
export default new CollaborativeOrchestrator()
|
||||
|
||||
@ -1,56 +1,43 @@
|
||||
import dotenv from 'dotenv';
|
||||
import dotenv from 'dotenv'
|
||||
import { Readable } from 'stream'
|
||||
|
||||
dotenv.config();
|
||||
dotenv.config()
|
||||
|
||||
const MISTRAL_API_KEY = process.env.MISTRAL_API_KEY;
|
||||
const MISTRAL_API_URL = 'https://api.mistral.ai/v1/chat/completions';
|
||||
const MISTRAL_API_KEY = process.env.MISTRAL_API_KEY
|
||||
const MISTRAL_API_URL = 'https://api.mistral.ai/v1/chat/completions'
|
||||
|
||||
/**
|
||||
* Agent role system prompts
|
||||
* Generic AI prompt for collaborative document editing
|
||||
*/
|
||||
const AGENT_PROMPTS = {
|
||||
architect: `You are a Software Architect AI. Your role is to:
|
||||
- Design high-level system architecture
|
||||
- Make technology stack decisions
|
||||
- Define project structure and modules
|
||||
- Consider scalability and maintainability
|
||||
- Provide clear technical justifications
|
||||
function getAgentPrompt(agentName) {
|
||||
return `You are an AI assistant named ${agentName} collaborating on a technical document design.
|
||||
|
||||
Output format: JSON with fields {proposal, justification, confidence (0-1), dependencies: []}`,
|
||||
Your responsibilities:
|
||||
1. Review the current document structure
|
||||
2. Select ONE section to improve or modify (identified by #, ##, ###, #### headers)
|
||||
3. Provide your thinking process and reasoning
|
||||
4. Return ONLY the modified section with its header, or confirm it's good as-is
|
||||
|
||||
backend_engineer: `You are a Backend Engineer AI. Your role is to:
|
||||
- Design API endpoints and data models
|
||||
- Suggest backend technologies and frameworks
|
||||
- Plan database schema
|
||||
- Consider performance and security
|
||||
- Provide implementation guidelines
|
||||
IMPORTANT RULES:
|
||||
- Only modify ONE section header and its content
|
||||
- Never modify the entire document
|
||||
- Return only the section you're working on, not the whole document
|
||||
- If section is good, respond: "Section is good, no changes needed"
|
||||
- Think step-by-step about what could be improved
|
||||
- Share your reasoning process
|
||||
|
||||
Output format: JSON with fields {proposal, justification, confidence (0-1), dependencies: []}`,
|
||||
|
||||
frontend_engineer: `You are a Frontend Engineer AI. Your role is to:
|
||||
- Design user interface structure
|
||||
- Suggest frontend frameworks and libraries
|
||||
- Plan component architecture
|
||||
- Consider UX and performance
|
||||
- Provide implementation guidelines
|
||||
|
||||
Output format: JSON with fields {proposal, justification, confidence (0-1), dependencies: []}`,
|
||||
|
||||
designer: `You are a UI/UX Designer AI. Your role is to:
|
||||
- Design user experience flows
|
||||
- Suggest UI patterns and layouts
|
||||
- Consider accessibility and usability
|
||||
- Provide visual design guidelines
|
||||
- Think about user interactions
|
||||
|
||||
Output format: JSON with fields {proposal, justification, confidence (0-1), dependencies: []}`
|
||||
};
|
||||
Format your response as:
|
||||
THINKING: [Your analysis and reasoning]
|
||||
DECISION: [What you'll modify or if keeping as-is]
|
||||
SECTION:
|
||||
[The modified or confirmed section with header]`
|
||||
}
|
||||
|
||||
/**
|
||||
* Call Mistral AI API
|
||||
* Call Mistral AI API with streaming
|
||||
*/
|
||||
async function callMistralAPI(messages, options = {}) {
|
||||
const { maxTokens, ...otherOptions } = options;
|
||||
const { maxTokens, stream = false, ...otherOptions } = options
|
||||
|
||||
const response = await fetch(MISTRAL_API_URL, {
|
||||
method: 'POST',
|
||||
@ -63,108 +50,132 @@ async function callMistralAPI(messages, options = {}) {
|
||||
messages,
|
||||
temperature: options.temperature || 0.7,
|
||||
max_tokens: maxTokens || 2048,
|
||||
stream,
|
||||
...otherOptions
|
||||
})
|
||||
});
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.text();
|
||||
throw new Error(`Mistral API error: ${error}`);
|
||||
const error = await response.text()
|
||||
throw new Error(`Mistral API error: ${error}`)
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
return response
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate agent response for a debate
|
||||
* Parse streaming response
|
||||
*/
|
||||
export async function generateAgentResponse(agentRole, prompt, context = []) {
|
||||
const systemPrompt = AGENT_PROMPTS[agentRole] || AGENT_PROMPTS.architect;
|
||||
async function* parseStreamResponse(reader) {
|
||||
const decoder = new TextDecoder()
|
||||
let buffer = ''
|
||||
|
||||
while (true) {
|
||||
const { done, value } = await reader.read()
|
||||
if (done) break
|
||||
|
||||
buffer += decoder.decode(value, { stream: true })
|
||||
const lines = buffer.split('\n')
|
||||
buffer = lines.pop() || ''
|
||||
|
||||
for (const line of lines) {
|
||||
if (line.startsWith('data: ')) {
|
||||
const data = line.slice(6)
|
||||
if (data === '[DONE]') continue
|
||||
try {
|
||||
const json = JSON.parse(data)
|
||||
if (json.choices?.[0]?.delta?.content) {
|
||||
yield json.choices[0].delta.content
|
||||
}
|
||||
} catch (e) {
|
||||
// Skip invalid JSON
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate agent response with streaming thoughts
|
||||
*/
|
||||
export async function generateAgentResponse(agentName, prompt, currentDocument = '', onThought = null) {
|
||||
const systemPrompt = getAgentPrompt(agentName)
|
||||
|
||||
const messages = [
|
||||
{ role: 'system', content: systemPrompt },
|
||||
{ role: 'user', content: `Project prompt: ${prompt}` }
|
||||
];
|
||||
|
||||
// Add context from previous responses
|
||||
if (context.length > 0) {
|
||||
const contextStr = context
|
||||
.slice(-3) // Last 3 responses to avoid token bloat
|
||||
.map(r => `${r.agent_role}: ${JSON.stringify(r.content)}`)
|
||||
.join('\n');
|
||||
|
||||
messages.push({
|
||||
role: 'user',
|
||||
content: `Previous discussion:\n${contextStr}\n\nProvide your analysis and proposal.`
|
||||
});
|
||||
}
|
||||
{ role: 'user', content: `Project description: ${prompt}\n\nCurrent document:\n${currentDocument}` }
|
||||
]
|
||||
|
||||
try {
|
||||
const result = await callMistralAPI(messages, {
|
||||
temperature: 0.7,
|
||||
maxTokens: 2048
|
||||
});
|
||||
const response = await callMistralAPI(messages, { stream: true })
|
||||
const reader = response.body.getReader()
|
||||
let fullContent = ''
|
||||
|
||||
const content = result.choices[0].message.content;
|
||||
|
||||
// Try to parse as JSON
|
||||
let parsedContent;
|
||||
try {
|
||||
// Extract JSON from markdown code blocks if present
|
||||
const jsonMatch = content.match(/```(?:json)?\s*(\{[\s\S]*\})\s*```/) ||
|
||||
content.match(/(\{[\s\S]*\})/);
|
||||
|
||||
if (jsonMatch) {
|
||||
parsedContent = JSON.parse(jsonMatch[1]);
|
||||
} else {
|
||||
parsedContent = JSON.parse(content);
|
||||
for await (const chunk of parseStreamResponse(reader)) {
|
||||
fullContent += chunk
|
||||
if (onThought) {
|
||||
onThought(chunk)
|
||||
}
|
||||
} catch (parseError) {
|
||||
// If not valid JSON, create structured response
|
||||
parsedContent = {
|
||||
proposal: content,
|
||||
justification: `Analysis from ${agentRole}`,
|
||||
confidence: 0.7,
|
||||
dependencies: []
|
||||
};
|
||||
yield chunk
|
||||
}
|
||||
|
||||
// Ensure required fields
|
||||
return {
|
||||
proposal: parsedContent.proposal || content,
|
||||
justification: parsedContent.justification || '',
|
||||
confidence: parsedContent.confidence || 0.7,
|
||||
dependencies: parsedContent.dependencies || [],
|
||||
mermaid: parsedContent.mermaid || null
|
||||
};
|
||||
|
||||
return fullContent
|
||||
} catch (error) {
|
||||
console.error(`Error generating response for ${agentRole}:`, error);
|
||||
|
||||
// Return mock response on error
|
||||
return {
|
||||
proposal: `Error generating response: ${error.message}`,
|
||||
justification: 'Failed to get AI response',
|
||||
confidence: 0.5,
|
||||
dependencies: [],
|
||||
error: true
|
||||
};
|
||||
console.error(`Error generating response from ${agentName}:`, error)
|
||||
return `Error: ${error.message}`
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate responses from multiple agents in parallel
|
||||
* Generate agent response (non-streaming version for simpler integration)
|
||||
*/
|
||||
export async function generateMultiAgentResponses(agents, prompt, context = []) {
|
||||
const promises = agents.map(agent =>
|
||||
generateAgentResponse(agent, prompt, context)
|
||||
.then(response => ({ agent, response }))
|
||||
);
|
||||
export async function generateAgentResponseSync(agentName, prompt, currentDocument = '') {
|
||||
const systemPrompt = getAgentPrompt(agentName)
|
||||
|
||||
return await Promise.all(promises);
|
||||
const messages = [
|
||||
{ role: 'system', content: systemPrompt },
|
||||
{ role: 'user', content: `Project description: ${prompt}\n\nCurrent document:\n${currentDocument}` }
|
||||
]
|
||||
|
||||
try {
|
||||
const response = await callMistralAPI(messages)
|
||||
const data = await response.json()
|
||||
|
||||
if (!data.choices?.[0]?.message?.content) {
|
||||
throw new Error('Invalid response from Mistral API')
|
||||
}
|
||||
|
||||
return data.choices[0].message.content
|
||||
} catch (error) {
|
||||
console.error(`Error generating response from ${agentName}:`, error)
|
||||
return {
|
||||
proposal: `Error generating response: ${error.message}`,
|
||||
justification: 'Failed to get AI response',
|
||||
confidence: 0,
|
||||
dependencies: [],
|
||||
error: true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
generateAgentResponse,
|
||||
generateMultiAgentResponses
|
||||
};
|
||||
/**
|
||||
* Extract section from AI response
|
||||
*/
|
||||
export function extractSection(aiResponse) {
|
||||
const sectionMatch = aiResponse.match(/SECTION:\s*([\s\S]*?)(?:$|THINKING:|DECISION:)/)
|
||||
if (sectionMatch) {
|
||||
return sectionMatch[1].trim()
|
||||
}
|
||||
return aiResponse
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract thinking from AI response
|
||||
*/
|
||||
export function extractThinking(aiResponse) {
|
||||
const thinkingMatch = aiResponse.match(/THINKING:\s*([\s\S]*?)(?:DECISION:|SECTION:)/)
|
||||
if (thinkingMatch) {
|
||||
return thinkingMatch[1].trim()
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user