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:
Augustin ROUX 2025-10-18 23:01:43 +02:00
parent 97c4ad9f6c
commit b566671ea4
3 changed files with 450 additions and 571 deletions

View File

@ -1,7 +1,7 @@
import express from 'express'; import express from 'express'
import collaborativeOrchestrator from '../services/collaborativeOrchestrator.js'; import collaborativeOrchestrator from '../services/collaborativeOrchestrator.js'
const router = express.Router(); const router = express.Router()
/** /**
* POST /api/collaborate * POST /api/collaborate
@ -9,73 +9,74 @@ const router = express.Router();
*/ */
router.post('/', async (req, res) => { router.post('/', async (req, res) => {
try { try {
const { prompt, documentFormat = 'md', agentCount = 7 } = req.body; const { prompt, documentFormat = 'md', agentCount = 7 } = req.body
if (!prompt || prompt.trim().length === 0) { 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)) { 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( const sessionId = collaborativeOrchestrator.createSession(
prompt, prompt,
documentFormat, documentFormat,
Math.min(agentCount, 7) validAgentCount
); )
const session = collaborativeOrchestrator.getSessionDetails(sessionId); const sessionInfo = collaborativeOrchestrator.getSessionInfo(sessionId)
res.json({ res.json({
sessionId, sessionId,
prompt, prompt,
documentFormat, documentFormat,
agentCount: validAgentCount,
status: 'created', status: 'created',
agents: session.agents.map(a => a.role), agents: sessionInfo.agents,
message: 'Collaborative session created. Start the session to begin collaboration.' message: 'Collaborative session created. Start the session to begin collaboration.'
}); })
} catch (error) { } catch (error) {
console.error('Error creating collaborative session:', error); console.error('Error creating collaborative session:', error)
res.status(500).json({ error: 'Failed to create collaborative session' }); res.status(500).json({ error: 'Failed to create collaborative session' })
} }
}); })
/** /**
* POST /api/collaborate/:id/start * 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) => { router.post('/:id/start', async (req, res) => {
try { try {
const sessionId = parseInt(req.params.id); const sessionId = parseInt(req.params.id)
const session = collaborativeOrchestrator.getSession(sessionId); const session = collaborativeOrchestrator.getSession(sessionId)
if (!session) { if (!session) {
return res.status(404).json({ error: 'Session not found' }); return res.status(404).json({ error: 'Session not found' })
} }
if (session.status !== 'created') { 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({ res.json({
sessionId, sessionId,
status: 'starting', status: 'starting',
message: 'Session is starting. Initial document is being created...' 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) { } catch (error) {
console.error('Error starting collaborative session:', error); console.error('Error starting session:', error)
res.status(500).json({ error: 'Failed to start collaborative session' }); res.status(500).json({ error: 'Failed to start session' })
} }
}); })
/** /**
* POST /api/collaborate/:id/round * POST /api/collaborate/:id/round
@ -83,85 +84,80 @@ router.post('/:id/start', async (req, res) => {
*/ */
router.post('/:id/round', async (req, res) => { router.post('/:id/round', async (req, res) => {
try { try {
const sessionId = parseInt(req.params.id); const sessionId = parseInt(req.params.id)
const session = collaborativeOrchestrator.getSession(sessionId); const session = collaborativeOrchestrator.getSession(sessionId)
if (!session) { if (!session) {
return res.status(404).json({ error: 'Session not found' }); return res.status(404).json({ error: 'Session not found' })
} }
if (session.status !== 'ongoing') { 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) { 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({ res.json({
sessionId, sessionId,
roundNumber: activeSession.currentRound + 1, roundNumber,
status: 'running', status: 'running',
message: 'Review round in progress...' message: 'Review round in progress...'
}); })
// Run asynchronously without waiting // Run asynchronously
collaborativeOrchestrator.runRound(sessionId).catch(error => { collaborativeOrchestrator.runRound(sessionId).catch(error => {
console.error('Error running round:', error); console.error('Error running round:', error)
collaborativeOrchestrator.broadcast(sessionId, { collaborativeOrchestrator.broadcast(sessionId, {
type: 'round_error', type: 'round_error',
sessionId, sessionId,
error: error.message error: error.message
}); })
}); })
} catch (error) { } catch (error) {
console.error('Error running round:', error); console.error('Error running round:', error)
res.status(500).json({ error: 'Failed to run round' }); res.status(500).json({ error: 'Failed to run round' })
} }
}); })
/** /**
* GET /api/collaborate/:id * GET /api/collaborate/:id
* Get session details with full history and current document * Get session details
*/ */
router.get('/:id', (req, res) => { router.get('/:id', (req, res) => {
try { try {
const sessionId = parseInt(req.params.id); const sessionId = parseInt(req.params.id)
const session = collaborativeOrchestrator.getSessionDetails(sessionId); const session = collaborativeOrchestrator.getSession(sessionId)
if (!session) { 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({ res.json({
sessionId: session.id, sessionId: sessionInfo.id,
initialPrompt: session.initial_prompt, status: sessionInfo.status,
documentFormat: session.document_format, agents: sessionInfo.agents,
status: session.status, agentCount: sessionInfo.agentCount,
currentRound: sessionInfo.currentRound,
currentDocument: sessionInfo.currentDocument,
versionNumber: sessionInfo.versionNumber,
documentVersionCount: versions.length,
conversationHistory: sessionInfo.conversationHistory,
createdAt: session.created_at, createdAt: session.created_at,
completedAt: session.completed_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
}))
});
} catch (error) { } catch (error) {
console.error('Error fetching session details:', error); console.error('Error fetching session details:', error)
res.status(500).json({ error: 'Failed to fetch session details' }); res.status(500).json({ error: 'Failed to fetch session details' })
} }
}); })
/** /**
* GET /api/collaborate/:id/document * GET /api/collaborate/:id/document
@ -169,29 +165,27 @@ router.get('/:id', (req, res) => {
*/ */
router.get('/:id/document', (req, res) => { router.get('/:id/document', (req, res) => {
try { try {
const sessionId = parseInt(req.params.id); const sessionId = parseInt(req.params.id)
const session = collaborativeOrchestrator.getSession(sessionId); const session = collaborativeOrchestrator.getSession(sessionId)
if (!session) { 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 activeSession = collaborativeOrchestrator.activeSessions.get(sessionId)
const currentDocument = activeSession?.currentDocument || ''; const currentDocument = activeSession?.currentDocument || ''
// Determine content type based on format
const contentType = session.document_format === 'md' const contentType = session.document_format === 'md'
? 'text/markdown; charset=utf-8' ? 'text/markdown; charset=utf-8'
: 'text/plain; charset=utf-8'; : 'text/plain; charset=utf-8'
res.set('Content-Type', contentType);
res.send(currentDocument);
res.set('Content-Type', contentType)
res.send(currentDocument)
} catch (error) { } catch (error) {
console.error('Error fetching document:', error); console.error('Error fetching document:', error)
res.status(500).json({ error: 'Failed to fetch document' }); res.status(500).json({ error: 'Failed to fetch document' })
} }
}); })
/** /**
* GET /api/collaborate/:id/versions/:versionNumber * GET /api/collaborate/:id/versions/:versionNumber
@ -199,26 +193,21 @@ router.get('/:id/document', (req, res) => {
*/ */
router.get('/:id/versions/:versionNumber', (req, res) => { router.get('/:id/versions/:versionNumber', (req, res) => {
try { try {
const sessionId = parseInt(req.params.id); const sessionId = parseInt(req.params.id)
const versionNumber = parseInt(req.params.versionNumber); const versionNumber = parseInt(req.params.versionNumber)
const session = collaborativeOrchestrator.getSession(sessionId); const session = collaborativeOrchestrator.getSession(sessionId)
if (!session) { 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 versions = collaborativeOrchestrator.getDocumentVersions(sessionId)
const version = versions.find(v => v.version_number === versionNumber); const version = versions.find(v => v.version_number === versionNumber)
if (!version) { 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({ res.json({
versionNumber: version.version_number, versionNumber: version.version_number,
modifiedBy: version.modified_by, modifiedBy: version.modified_by,
@ -226,13 +215,12 @@ router.get('/:id/versions/:versionNumber', (req, res) => {
roundNumber: version.round_number, roundNumber: version.round_number,
createdAt: version.created_at, createdAt: version.created_at,
content: version.content content: version.content
}); })
} catch (error) { } catch (error) {
console.error('Error fetching version:', error); console.error('Error fetching version:', error)
res.status(500).json({ error: 'Failed to fetch version' }); res.status(500).json({ error: 'Failed to fetch version' })
} }
}); })
/** /**
* POST /api/collaborate/:id/complete * POST /api/collaborate/:id/complete
@ -240,25 +228,24 @@ router.get('/:id/versions/:versionNumber', (req, res) => {
*/ */
router.post('/:id/complete', (req, res) => { router.post('/:id/complete', (req, res) => {
try { try {
const sessionId = parseInt(req.params.id); const sessionId = parseInt(req.params.id)
const session = collaborativeOrchestrator.getSession(sessionId); const session = collaborativeOrchestrator.getSession(sessionId)
if (!session) { 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({ res.json({
sessionId, sessionId,
status: 'completed', status: 'completed',
message: 'Session completed successfully' message: 'Session completed successfully'
}); })
} catch (error) { } catch (error) {
console.error('Error completing session:', error); console.error('Error completing session:', error)
res.status(500).json({ error: 'Failed to complete session' }); res.status(500).json({ error: 'Failed to complete session' })
} }
}); })
export default router; export default router

View File

@ -1,21 +1,11 @@
import db from '../db/schema.js'; import db from '../db/schema.js'
import { generateAgentResponse } from './mistralClient.js'; import { generateAgentResponseSync, extractSection, extractThinking } from './mistralClient.js'
import { getRandomNames } from './nameGenerator.js'
class CollaborativeOrchestrator { class CollaborativeOrchestrator {
constructor() { constructor() {
this.activeSessions = new Map(); this.activeSessions = new Map()
this.wsClients = new Map(); // sessionId -> Set of WebSocket clients 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 }
];
} }
/** /**
@ -23,9 +13,9 @@ class CollaborativeOrchestrator {
*/ */
registerWSClient(sessionId, ws) { registerWSClient(sessionId, ws) {
if (!this.wsClients.has(sessionId)) { 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) { unregisterWSClient(sessionId, ws) {
if (this.wsClients.has(sessionId)) { 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) { broadcast(sessionId, message) {
if (this.wsClients.has(sessionId)) { if (this.wsClients.has(sessionId)) {
const data = JSON.stringify(message); const data = JSON.stringify(message)
this.wsClients.get(sessionId).forEach(ws => { this.wsClients.get(sessionId).forEach(ws => {
if (ws.readyState === 1) { // OPEN 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) { createSession(initialPrompt, documentFormat = 'md', agentCount = 7) {
const stmt = db.prepare( const stmt = db.prepare(
'INSERT INTO collaborative_sessions (initial_prompt, document_format, status) VALUES (?, ?, ?)' 'INSERT INTO collaborative_sessions (initial_prompt, document_format, status) VALUES (?, ?, ?)'
); )
const result = stmt.run(initialPrompt, documentFormat, 'created'); const result = stmt.run(initialPrompt, documentFormat, 'created')
const sessionId = result.lastInsertRowid; const sessionId = result.lastInsertRowid
// Select the agents to use // Generate random names for agents
const selectedAgents = this.agents.slice(0, Math.min(agentCount, this.agents.length)); const agentNames = getRandomNames(Math.min(agentCount, 50))
this.activeSessions.set(sessionId, { this.activeSessions.set(sessionId, {
id: sessionId, id: sessionId,
initialPrompt, initialPrompt,
documentFormat, documentFormat,
agents: selectedAgents, agents: agentNames, // Array of agent names
currentRound: 0, agentCount,
currentAgentIndex: 0,
currentDocument: null, currentDocument: null,
versionNumber: 0, versionNumber: 0,
conversationHistory: [], conversationHistory: [],
started: false started: false,
}); consecutiveNoChanges: 0, // Counter for convergence
lastModifiedAgent: null
})
return sessionId; return sessionId
} }
/** /**
* Get session by ID * Get session by ID
*/ */
getSession(sessionId) { getSession(sessionId) {
const stmt = db.prepare('SELECT * FROM collaborative_sessions WHERE id = ?'); const stmt = db.prepare('SELECT * FROM collaborative_sessions WHERE id = ?')
return stmt.get(sessionId); return stmt.get(sessionId)
} }
/** /**
* Get all versions of a document * Start a session - create initial document with first agent
*/ */
getDocumentVersions(sessionId) { async startSession(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) {
try { try {
const session = this.getSession(sessionId); const session = this.activeSessions.get(sessionId)
if (!session) { if (!session || session.started) return
throw new Error('Session not found');
}
const activeSession = this.activeSessions.get(sessionId); const firstAgent = session.agents[0]
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);
// Generate initial document // Generate initial document
const response = await generateAgentResponse( const initialResponse = await generateAgentResponseSync(
leadArchitect.role, firstAgent,
`${session.initial_prompt}\n\nYou are the Lead Architect. Create the INITIAL version of a comprehensive project document in ${session.document_format} format. 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: // Save to DB
- Project overview and goals const insertStmt = db.prepare(
- Architecture overview 'INSERT INTO document_versions (session_id, version_number, content, modified_by, modification_reason, round_number) VALUES (?, ?, ?, ?, ?, ?)'
- Technology stack decisions )
- Project structure insertStmt.run(sessionId, 0, initialDocument, firstAgent, 'Initial document creation', 0)
- Key features
- Non-functional requirements
- Timeline and phases
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 // Update DB status
let documentContent = response.proposal || response; const updateStmt = db.prepare('UPDATE collaborative_sessions SET status = ? WHERE id = ?')
if (typeof documentContent === 'object') { updateStmt.run('ongoing', sessionId)
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
});
// Broadcast initial document
this.broadcast(sessionId, { this.broadcast(sessionId, {
type: 'initial_document_created', type: 'initial_document_created',
sessionId, content: initialDocument,
agent: leadArchitect.role, agentName: firstAgent,
documentVersion: 1, thinking,
document: documentContent, roundNumber: 0
message: `Lead Architect (${leadArchitect.role}) created initial document. Starting review rounds...` })
});
return {
sessionId,
documentVersion: 1,
document: documentContent,
agents: activeSession.agents
};
// Auto-start first round
setTimeout(() => this.runRound(sessionId), 2000)
} catch (error) { } catch (error) {
console.error('Error starting collaborative session:', error); console.error('Error starting session:', error)
this.failSession(sessionId);
this.broadcast(sessionId, { this.broadcast(sessionId, {
type: 'session_error', type: 'session_error',
sessionId,
error: error.message 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) { async runRound(sessionId) {
try { try {
const session = this.getSession(sessionId); const session = this.activeSessions.get(sessionId)
if (!session) { if (!session) return
throw new Error('Session not found');
}
const activeSession = this.activeSessions.get(sessionId); const roundNumber = session.conversationHistory.length + 1
if (!activeSession) { const agentsInRound = session.agents
throw new Error('Active session not found'); const modifiedAgents = []
}
if (!activeSession.currentDocument) { // Each agent reviews the document
throw new Error('No document to review'); for (let i = 0; i < agentsInRound.length; i++) {
} const agentName = agentsInRound[i]
activeSession.currentRound += 1;
const roundNumber = activeSession.currentRound;
// Broadcast that this agent is working
this.broadcast(sessionId, { this.broadcast(sessionId, {
type: 'round_start', type: 'agent_working',
sessionId, agentName,
roundNumber, roundNumber
message: `Starting review round ${roundNumber}. Agents will review and modify the document...` })
});
const agentsMadeChanges = []; try {
const roundAgents = activeSession.agents.map(a => a.role).join(','); const response = await generateAgentResponseSync(
agentName,
session.initialPrompt,
session.currentDocument
)
// Process each agent sequentially const thinking = extractThinking(response)
for (const agent of activeSession.agents) { const section = extractSection(response)
// Broadcast agent's thinking in real-time
this.broadcast(sessionId, { this.broadcast(sessionId, {
type: 'agent_reviewing', type: 'agent_thinking',
sessionId, agentName,
agent: agent.role, thinking,
roundNumber, 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);
}
// Check if agent made changes // Check if agent made changes
if (responseText.trim() !== 'NO_CHANGES') { if (section !== 'Section is good, no changes needed' && !section.includes('no changes needed')) {
// Calculate diff // Merge section into document
const diff = this.calculateDiff(activeSession.currentDocument, responseText); const updatedDocument = this.mergeSection(session.currentDocument, section)
// Save new version if (updatedDocument !== session.currentDocument) {
this.saveDocumentVersion( 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, sessionId,
responseText, session.versionNumber,
agent.role, updatedDocument,
diff.summary, agentName,
`Round ${roundNumber} modifications`,
roundNumber roundNumber
); )
activeSession.currentDocument = responseText;
activeSession.versionNumber += 1;
agentsMadeChanges.push(agent.role);
// Broadcast modification
this.broadcast(sessionId, { this.broadcast(sessionId, {
type: 'document_modified', type: 'document_modified',
sessionId, content: updatedDocument,
agent: agent.role, modifiedBy: agentName,
roundNumber, section,
documentVersion: activeSession.versionNumber, roundNumber
document: responseText, })
changeSummary: diff.summary, }
message: `${agent.role} modified the document` }
}); } catch (error) {
} else { console.error(`Error with agent ${agentName}:`, error)
this.broadcast(sessionId, {
type: 'agent_no_changes',
sessionId,
agent: agent.role,
roundNumber,
message: `${agent.role} reviewed the document and found no changes needed`
});
} }
} }
// Save round completion // Track convergence
if (modifiedAgents.length === 0) {
session.consecutiveNoChanges++
} else {
session.consecutiveNoChanges = 0
}
// Save round to DB
const roundStmt = db.prepare( const roundStmt = db.prepare(
`INSERT INTO document_rounds 'INSERT INTO document_rounds (session_id, round_number, agents_in_round, agents_made_changes) VALUES (?, ?, ?, ?)'
(session_id, round_number, agents_in_round, agents_made_changes, completed_at) )
VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP)`
);
roundStmt.run( roundStmt.run(
sessionId, sessionId,
roundNumber, roundNumber,
roundAgents, JSON.stringify(agentsInRound),
agentsMadeChanges.join(',') JSON.stringify(modifiedAgents)
); )
const conversationEntry = { // Add to history
session.conversationHistory.push({
roundNumber, roundNumber,
agentsMadeChanges: agentsMadeChanges.length > 0 ? agentsMadeChanges : 'none' agentsMadeChanges: modifiedAgents,
}; timestamp: Date.now()
})
activeSession.conversationHistory.push(conversationEntry);
// Check for convergence
const hasConverged = agentsMadeChanges.length === 0;
// Broadcast round complete
const hasConverged = session.consecutiveNoChanges >= session.agentCount
this.broadcast(sessionId, { this.broadcast(sessionId, {
type: 'round_complete', type: 'round_complete',
sessionId,
roundNumber, roundNumber,
agentsMadeChanges: agentsMadeChanges.length, agentsMadeChanges: modifiedAgents,
agentsWhoModified: agentsMadeChanges,
hasConverged, hasConverged,
message: hasConverged consecutiveNoChanges: session.consecutiveNoChanges
? 'Convergence reached! No more changes needed.' })
: `Round ${roundNumber} complete. ${agentsMadeChanges.length} agent(s) made changes.`
});
return {
roundNumber,
agentsMadeChanges,
hasConverged,
documentVersion: activeSession.versionNumber
};
// 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) { } catch (error) {
console.error('Error running round:', error); console.error('Error running round:', error)
throw 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) { mergeSection(document, newSection) {
const focusAreas = { if (!document || !newSection) return document
lead_architect: 'High-level architecture, technology stack, system design, scalability',
backend_engineer: 'APIs, databases, backend services, data models, performance, security', // Extract header from new section
frontend_engineer: 'UI components, state management, performance, user interactions, frameworks', const headerMatch = newSection.match(/^(#{1,4})\s+(.+)/)
ui_designer: 'User experience, accessibility, visual design, UI patterns, usability', if (!headerMatch) return document
devops_engineer: 'Deployment, infrastructure, CI/CD, monitoring, containers, scalability',
product_manager: 'Features, user needs, roadmap, priorities, market fit, business goals', const headerLevel = headerMatch[1].length
security_specialist: 'Security architecture, data protection, authentication, compliance' const headerText = headerMatch[2]
}; const headerRegex = new RegExp(`^${'#'.repeat(headerLevel)}\\s+${headerText.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}$`, 'gm')
return focusAreas[agentRole] || 'General improvements';
// 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 * Complete a session
*/ */
completeSession(sessionId) { async completeSession(sessionId) {
const session = this.activeSessions.get(sessionId); try {
const document = session?.currentDocument || ''; 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( this.broadcast(sessionId, {
'UPDATE collaborative_sessions SET status = ?, completed_at = CURRENT_TIMESTAMP, final_document = ? WHERE id = ?' type: 'session_completed',
); finalDocument: session?.currentDocument
stmt.run('completed', document, sessionId); })
} catch (error) {
this.activeSessions.delete(sessionId); console.error('Error completing session:', error)
}
} }
/** /**
* Fail a session * Get document versions
*/ */
failSession(sessionId) { getDocumentVersions(sessionId) {
const stmt = db.prepare( const stmt = db.prepare(
'UPDATE collaborative_sessions SET status = ? WHERE id = ?' 'SELECT * FROM document_versions WHERE session_id = ? ORDER BY version_number ASC'
); )
stmt.run('failed', sessionId); return stmt.all(sessionId)
this.activeSessions.delete(sessionId);
} }
/** /**
* Get session details with full history * Get session info for API response
*/ */
getSessionDetails(sessionId) { getSessionInfo(sessionId) {
const session = this.getSession(sessionId); const session = this.activeSessions.get(sessionId)
const versions = this.getDocumentVersions(sessionId); const dbSession = this.getSession(sessionId)
const activeSession = this.activeSessions.get(sessionId);
if (!session) return null
return { return {
...session, id: sessionId,
versions, status: dbSession?.status,
currentRound: activeSession?.currentRound || 0, agents: session.agents,
currentDocument: activeSession?.currentDocument || null, agentCount: session.agentCount,
agents: activeSession?.agents || [], currentRound: session.conversationHistory.length,
conversationHistory: activeSession?.conversationHistory || [] currentDocument: session.currentDocument,
}; versionNumber: session.versionNumber,
conversationHistory: session.conversationHistory
}
} }
} }
export default new CollaborativeOrchestrator(); export default new CollaborativeOrchestrator()

View File

@ -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_KEY = process.env.MISTRAL_API_KEY
const MISTRAL_API_URL = 'https://api.mistral.ai/v1/chat/completions'; 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 = { function getAgentPrompt(agentName) {
architect: `You are a Software Architect AI. Your role is to: return `You are an AI assistant named ${agentName} collaborating on a technical document design.
- Design high-level system architecture
- Make technology stack decisions
- Define project structure and modules
- Consider scalability and maintainability
- Provide clear technical justifications
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: IMPORTANT RULES:
- Design API endpoints and data models - Only modify ONE section header and its content
- Suggest backend technologies and frameworks - Never modify the entire document
- Plan database schema - Return only the section you're working on, not the whole document
- Consider performance and security - If section is good, respond: "Section is good, no changes needed"
- Provide implementation guidelines - Think step-by-step about what could be improved
- Share your reasoning process
Output format: JSON with fields {proposal, justification, confidence (0-1), dependencies: []}`, Format your response as:
THINKING: [Your analysis and reasoning]
frontend_engineer: `You are a Frontend Engineer AI. Your role is to: DECISION: [What you'll modify or if keeping as-is]
- Design user interface structure SECTION:
- Suggest frontend frameworks and libraries [The modified or confirmed section with header]`
- 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: []}`
};
/** /**
* Call Mistral AI API * Call Mistral AI API with streaming
*/ */
async function callMistralAPI(messages, options = {}) { async function callMistralAPI(messages, options = {}) {
const { maxTokens, ...otherOptions } = options; const { maxTokens, stream = false, ...otherOptions } = options
const response = await fetch(MISTRAL_API_URL, { const response = await fetch(MISTRAL_API_URL, {
method: 'POST', method: 'POST',
@ -63,108 +50,132 @@ async function callMistralAPI(messages, options = {}) {
messages, messages,
temperature: options.temperature || 0.7, temperature: options.temperature || 0.7,
max_tokens: maxTokens || 2048, max_tokens: maxTokens || 2048,
stream,
...otherOptions ...otherOptions
}) })
}); })
if (!response.ok) { if (!response.ok) {
const error = await response.text(); const error = await response.text()
throw new Error(`Mistral API error: ${error}`); 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 = []) { async function* parseStreamResponse(reader) {
const systemPrompt = AGENT_PROMPTS[agentRole] || AGENT_PROMPTS.architect; 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 = [ const messages = [
{ role: 'system', content: systemPrompt }, { role: 'system', content: systemPrompt },
{ role: 'user', content: `Project prompt: ${prompt}` } { role: 'user', content: `Project description: ${prompt}\n\nCurrent document:\n${currentDocument}` }
]; ]
// 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.`
});
}
try { try {
const result = await callMistralAPI(messages, { const response = await callMistralAPI(messages, { stream: true })
temperature: 0.7, const reader = response.body.getReader()
maxTokens: 2048 let fullContent = ''
});
const content = result.choices[0].message.content; for await (const chunk of parseStreamResponse(reader)) {
fullContent += chunk
// Try to parse as JSON if (onThought) {
let parsedContent; onThought(chunk)
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);
} }
} catch (parseError) { yield chunk
// If not valid JSON, create structured response
parsedContent = {
proposal: content,
justification: `Analysis from ${agentRole}`,
confidence: 0.7,
dependencies: []
};
} }
// Ensure required fields return fullContent
return {
proposal: parsedContent.proposal || content,
justification: parsedContent.justification || '',
confidence: parsedContent.confidence || 0.7,
dependencies: parsedContent.dependencies || [],
mermaid: parsedContent.mermaid || null
};
} catch (error) { } catch (error) {
console.error(`Error generating response for ${agentRole}:`, error); console.error(`Error generating response from ${agentName}:`, error)
return `Error: ${error.message}`
// Return mock response on error
return {
proposal: `Error generating response: ${error.message}`,
justification: 'Failed to get AI response',
confidence: 0.5,
dependencies: [],
error: true
};
} }
} }
/** /**
* Generate responses from multiple agents in parallel * Generate agent response (non-streaming version for simpler integration)
*/ */
export async function generateMultiAgentResponses(agents, prompt, context = []) { export async function generateAgentResponseSync(agentName, prompt, currentDocument = '') {
const promises = agents.map(agent => const systemPrompt = getAgentPrompt(agentName)
generateAgentResponse(agent, prompt, context)
.then(response => ({ agent, response }))
);
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')
} }
export default { return data.choices[0].message.content
generateAgentResponse, } catch (error) {
generateMultiAgentResponses 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
}
}
}
/**
* 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 ''
}