agora-ai/backend/src/routes/collaborate.js
Muyue b566671ea4 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
2025-10-18 23:01:43 +02:00

252 lines
7.1 KiB
JavaScript

import express from 'express'
import collaborativeOrchestrator from '../services/collaborativeOrchestrator.js'
const router = express.Router()
/**
* POST /api/collaborate
* Create a new collaborative session
*/
router.post('/', async (req, res) => {
try {
const { prompt, documentFormat = 'md', agentCount = 7 } = req.body
if (!prompt || prompt.trim().length === 0) {
return res.status(400).json({ error: 'Prompt is required' })
}
if (!['md', 'txt'].includes(documentFormat)) {
return res.status(400).json({ error: 'Document format must be "md" or "txt"' })
}
// Validate agent count
const validAgentCount = Math.min(Math.max(agentCount, 3), 50)
const sessionId = collaborativeOrchestrator.createSession(
prompt,
documentFormat,
validAgentCount
)
const sessionInfo = collaborativeOrchestrator.getSessionInfo(sessionId)
res.json({
sessionId,
prompt,
documentFormat,
agentCount: validAgentCount,
status: 'created',
agents: sessionInfo.agents,
message: 'Collaborative session created. Start the session to begin collaboration.'
})
} catch (error) {
console.error('Error creating collaborative session:', error)
res.status(500).json({ error: 'Failed to create collaborative session' })
}
})
/**
* POST /api/collaborate/:id/start
* Start the collaborative session
*/
router.post('/:id/start', async (req, res) => {
try {
const sessionId = parseInt(req.params.id)
const session = collaborativeOrchestrator.getSession(sessionId)
if (!session) {
return res.status(404).json({ error: 'Session not found' })
}
if (session.status !== 'created') {
return res.status(400).json({ error: 'Session has already been started' })
}
res.json({
sessionId,
status: 'starting',
message: 'Session is starting. Initial document is being created...'
})
// Start asynchronously
collaborativeOrchestrator.startSession(sessionId).catch(error => {
console.error('Error starting session:', error)
})
} catch (error) {
console.error('Error starting session:', error)
res.status(500).json({ error: 'Failed to start session' })
}
})
/**
* POST /api/collaborate/:id/round
* Run the next round of reviews
*/
router.post('/:id/round', async (req, res) => {
try {
const sessionId = parseInt(req.params.id)
const session = collaborativeOrchestrator.getSession(sessionId)
if (!session) {
return res.status(404).json({ error: 'Session not found' })
}
if (session.status !== 'ongoing') {
return res.status(400).json({ error: 'Session is not in ongoing status' })
}
const activeSession = collaborativeOrchestrator.activeSessions.get(sessionId)
if (!activeSession?.started) {
return res.status(400).json({ error: 'Session has not been started yet' })
}
const roundNumber = activeSession.conversationHistory.length + 1
res.json({
sessionId,
roundNumber,
status: 'running',
message: 'Review round in progress...'
})
// Run asynchronously
collaborativeOrchestrator.runRound(sessionId).catch(error => {
console.error('Error running round:', error)
collaborativeOrchestrator.broadcast(sessionId, {
type: 'round_error',
sessionId,
error: error.message
})
})
} catch (error) {
console.error('Error running round:', error)
res.status(500).json({ error: 'Failed to run round' })
}
})
/**
* GET /api/collaborate/:id
* Get session details
*/
router.get('/:id', (req, res) => {
try {
const sessionId = parseInt(req.params.id)
const session = collaborativeOrchestrator.getSession(sessionId)
if (!session) {
return res.status(404).json({ error: 'Session not found' })
}
const sessionInfo = collaborativeOrchestrator.getSessionInfo(sessionId)
const versions = collaborativeOrchestrator.getDocumentVersions(sessionId)
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) {
console.error('Error fetching session details:', error)
res.status(500).json({ error: 'Failed to fetch session details' })
}
})
/**
* GET /api/collaborate/:id/document
* Get the current document
*/
router.get('/:id/document', (req, res) => {
try {
const sessionId = parseInt(req.params.id)
const session = collaborativeOrchestrator.getSession(sessionId)
if (!session) {
return res.status(404).json({ error: 'Session not found' })
}
const activeSession = collaborativeOrchestrator.activeSessions.get(sessionId)
const currentDocument = activeSession?.currentDocument || ''
const contentType = session.document_format === 'md'
? 'text/markdown; charset=utf-8'
: 'text/plain; charset=utf-8'
res.set('Content-Type', contentType)
res.send(currentDocument)
} catch (error) {
console.error('Error fetching document:', error)
res.status(500).json({ error: 'Failed to fetch document' })
}
})
/**
* GET /api/collaborate/:id/versions/:versionNumber
* Get a specific document version
*/
router.get('/:id/versions/:versionNumber', (req, res) => {
try {
const sessionId = parseInt(req.params.id)
const versionNumber = parseInt(req.params.versionNumber)
const session = collaborativeOrchestrator.getSession(sessionId)
if (!session) {
return res.status(404).json({ error: 'Session not found' })
}
const versions = collaborativeOrchestrator.getDocumentVersions(sessionId)
const version = versions.find(v => v.version_number === versionNumber)
if (!version) {
return res.status(404).json({ error: 'Version not found' })
}
res.json({
versionNumber: version.version_number,
modifiedBy: version.modified_by,
modificationReason: version.modification_reason,
roundNumber: version.round_number,
createdAt: version.created_at,
content: version.content
})
} catch (error) {
console.error('Error fetching version:', error)
res.status(500).json({ error: 'Failed to fetch version' })
}
})
/**
* POST /api/collaborate/:id/complete
* Complete the collaborative session
*/
router.post('/:id/complete', (req, res) => {
try {
const sessionId = parseInt(req.params.id)
const session = collaborativeOrchestrator.getSession(sessionId)
if (!session) {
return res.status(404).json({ error: 'Session not found' })
}
collaborativeOrchestrator.completeSession(sessionId)
res.json({
sessionId,
status: 'completed',
message: 'Session completed successfully'
})
} catch (error) {
console.error('Error completing session:', error)
res.status(500).json({ error: 'Failed to complete session' })
}
})
export default router