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 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' })
|
||||||
}
|
}
|
||||||
|
|
||||||
res.json({
|
const sessionInfo = collaborativeOrchestrator.getSessionInfo(sessionId)
|
||||||
sessionId: session.id,
|
const versions = collaborativeOrchestrator.getDocumentVersions(sessionId)
|
||||||
initialPrompt: session.initial_prompt,
|
|
||||||
documentFormat: session.document_format,
|
|
||||||
status: session.status,
|
|
||||||
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
|
|
||||||
}))
|
|
||||||
});
|
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
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
|
||||||
|
})
|
||||||
} 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
|
||||||
|
|||||||
@ -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;
|
// Broadcast that this agent is working
|
||||||
const roundNumber = activeSession.currentRound;
|
|
||||||
|
|
||||||
this.broadcast(sessionId, {
|
|
||||||
type: 'round_start',
|
|
||||||
sessionId,
|
|
||||||
roundNumber,
|
|
||||||
message: `Starting review round ${roundNumber}. Agents will review and modify the document...`
|
|
||||||
});
|
|
||||||
|
|
||||||
const agentsMadeChanges = [];
|
|
||||||
const roundAgents = activeSession.agents.map(a => a.role).join(',');
|
|
||||||
|
|
||||||
// Process each agent sequentially
|
|
||||||
for (const agent of activeSession.agents) {
|
|
||||||
this.broadcast(sessionId, {
|
this.broadcast(sessionId, {
|
||||||
type: 'agent_reviewing',
|
type: 'agent_working',
|
||||||
sessionId,
|
agentName,
|
||||||
agent: agent.role,
|
roundNumber
|
||||||
roundNumber,
|
})
|
||||||
message: `${agent.role} is reviewing the document...`
|
|
||||||
});
|
|
||||||
|
|
||||||
// Call agent to review and potentially modify document
|
try {
|
||||||
const response = await generateAgentResponse(
|
const response = await generateAgentResponseSync(
|
||||||
agent.role,
|
agentName,
|
||||||
`${session.initial_prompt}\n\n
|
session.initialPrompt,
|
||||||
CURRENT DOCUMENT (${session.document_format} format):
|
session.currentDocument
|
||||||
\`\`\`${session.document_format}
|
)
|
||||||
${activeSession.currentDocument}
|
|
||||||
\`\`\`
|
|
||||||
|
|
||||||
You are the ${agent.role}. Review this document from your perspective. Your task is to:
|
const thinking = extractThinking(response)
|
||||||
1. Read and understand the entire document
|
const section = extractSection(response)
|
||||||
2. Identify areas that need improvement or modifications from your expertise area
|
|
||||||
3. Provide improvements, additions, or modifications
|
|
||||||
|
|
||||||
IMPORTANT:
|
// Broadcast agent's thinking in real-time
|
||||||
- If you decide to modify the document, output ONLY the complete modified document in ${session.document_format} format
|
this.broadcast(sessionId, {
|
||||||
- If the document is already excellent and needs no changes, output: NO_CHANGES
|
type: 'agent_thinking',
|
||||||
- Do not include explanations, just the document or NO_CHANGES
|
agentName,
|
||||||
|
thinking,
|
||||||
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
|
|
||||||
if (responseText.trim() !== 'NO_CHANGES') {
|
|
||||||
// Calculate diff
|
|
||||||
const diff = this.calculateDiff(activeSession.currentDocument, responseText);
|
|
||||||
|
|
||||||
// Save new version
|
|
||||||
this.saveDocumentVersion(
|
|
||||||
sessionId,
|
|
||||||
responseText,
|
|
||||||
agent.role,
|
|
||||||
diff.summary,
|
|
||||||
roundNumber
|
roundNumber
|
||||||
);
|
})
|
||||||
|
|
||||||
activeSession.currentDocument = responseText;
|
// Check if agent made changes
|
||||||
activeSession.versionNumber += 1;
|
if (section !== 'Section is good, no changes needed' && !section.includes('no changes needed')) {
|
||||||
agentsMadeChanges.push(agent.role);
|
// Merge section into document
|
||||||
|
const updatedDocument = this.mergeSection(session.currentDocument, section)
|
||||||
|
|
||||||
this.broadcast(sessionId, {
|
if (updatedDocument !== session.currentDocument) {
|
||||||
type: 'document_modified',
|
session.currentDocument = updatedDocument
|
||||||
sessionId,
|
session.versionNumber++
|
||||||
agent: agent.role,
|
modifiedAgents.push(agentName)
|
||||||
roundNumber,
|
|
||||||
documentVersion: activeSession.versionNumber,
|
// Save version to DB
|
||||||
document: responseText,
|
const insertStmt = db.prepare(
|
||||||
changeSummary: diff.summary,
|
'INSERT INTO document_versions (session_id, version_number, content, modified_by, modification_reason, round_number) VALUES (?, ?, ?, ?, ?, ?)'
|
||||||
message: `${agent.role} modified the document`
|
)
|
||||||
});
|
insertStmt.run(
|
||||||
} else {
|
sessionId,
|
||||||
this.broadcast(sessionId, {
|
session.versionNumber,
|
||||||
type: 'agent_no_changes',
|
updatedDocument,
|
||||||
sessionId,
|
agentName,
|
||||||
agent: agent.role,
|
`Round ${roundNumber} modifications`,
|
||||||
roundNumber,
|
roundNumber
|
||||||
message: `${agent.role} reviewed the document and found no changes needed`
|
)
|
||||||
});
|
|
||||||
|
// Broadcast modification
|
||||||
|
this.broadcast(sessionId, {
|
||||||
|
type: 'document_modified',
|
||||||
|
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(
|
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',
|
|
||||||
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';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
// Extract header from new section
|
||||||
* Calculate diff between two document versions (simplified)
|
const headerMatch = newSection.match(/^(#{1,4})\s+(.+)/)
|
||||||
*/
|
if (!headerMatch) return document
|
||||||
calculateDiff(oldContent, newContent) {
|
|
||||||
const oldLines = oldContent.split('\n');
|
|
||||||
const newLines = newContent.split('\n');
|
|
||||||
|
|
||||||
const linesAdded = Math.max(0, newLines.length - oldLines.length);
|
const headerLevel = headerMatch[1].length
|
||||||
const linesRemoved = Math.max(0, oldLines.length - newLines.length);
|
const headerText = headerMatch[2]
|
||||||
const changes = Math.abs(linesAdded) + Math.abs(linesRemoved);
|
const headerRegex = new RegExp(`^${'#'.repeat(headerLevel)}\\s+${headerText.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}$`, 'gm')
|
||||||
|
|
||||||
const summary = `Modified document: +${linesAdded} lines, -${linesRemoved} lines (${changes} total changes)`;
|
// Find and replace the section
|
||||||
|
if (headerRegex.test(document)) {
|
||||||
return { summary, linesAdded, linesRemoved };
|
// 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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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()
|
||||||
|
|||||||
@ -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')
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
* Extract section from AI response
|
||||||
generateMultiAgentResponses
|
*/
|
||||||
};
|
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