diff --git a/backend/src/index.js b/backend/src/index.js index f096d74..e6023c6 100644 --- a/backend/src/index.js +++ b/backend/src/index.js @@ -5,8 +5,10 @@ import dotenv from 'dotenv'; import { createServer } from 'http'; import { WebSocketServer } from 'ws'; import rateLimit from 'express-rate-limit'; +import { parse } from 'url'; import db from './db/schema.js'; import debateRoutes from './routes/debate.js'; +import orchestrator from './services/orchestrator.js'; dotenv.config(); @@ -32,15 +34,44 @@ const limiter = rateLimit({ app.use('/api', limiter); // WebSocket connection handling -wss.on('connection', (ws) => { - console.log('New WebSocket connection established'); +wss.on('connection', (ws, req) => { + const { query } = parse(req.url, true); + const debateId = query.debateId ? parseInt(query.debateId) : null; + + console.log('New WebSocket connection established', debateId ? `for debate ${debateId}` : ''); + + if (debateId) { + orchestrator.registerWSClient(debateId, ws); + + ws.send(JSON.stringify({ + type: 'connected', + debateId, + message: 'Connected to debate updates' + })); + } ws.on('message', (message) => { - console.log('Received:', message.toString()); - // Handle incoming messages + try { + const data = JSON.parse(message.toString()); + console.log('Received:', data); + + // Handle subscribe to debate + if (data.type === 'subscribe' && data.debateId) { + orchestrator.registerWSClient(parseInt(data.debateId), ws); + ws.send(JSON.stringify({ + type: 'subscribed', + debateId: data.debateId + })); + } + } catch (error) { + console.error('WebSocket message error:', error); + } }); ws.on('close', () => { + if (debateId) { + orchestrator.unregisterWSClient(debateId, ws); + } console.log('WebSocket connection closed'); }); }); diff --git a/backend/src/routes/debate.js b/backend/src/routes/debate.js index 098bdc1..7555db3 100644 --- a/backend/src/routes/debate.js +++ b/backend/src/routes/debate.js @@ -5,7 +5,7 @@ const router = express.Router(); /** * POST /api/debate - * Create a new debate + * Create a new debate and start AI discussion */ router.post('/', async (req, res) => { try { @@ -18,12 +18,19 @@ router.post('/', async (req, res) => { const debateId = orchestrator.createDebate(prompt); const agents = orchestrator.selectAgents(prompt); + // Send immediate response res.json({ debateId, prompt, agents, status: 'ongoing' }); + + // Start debate asynchronously (don't wait for response) + orchestrator.startDebate(debateId, agents).catch(error => { + console.error('Debate failed:', error); + }); + } catch (error) { console.error('Error creating debate:', error); res.status(500).json({ error: 'Failed to create debate' }); diff --git a/backend/src/services/mistralClient.js b/backend/src/services/mistralClient.js new file mode 100644 index 0000000..e95aefa --- /dev/null +++ b/backend/src/services/mistralClient.js @@ -0,0 +1,168 @@ +import dotenv from 'dotenv'; + +dotenv.config(); + +const MISTRAL_API_KEY = process.env.MISTRAL_API_KEY; +const MISTRAL_API_URL = 'https://api.mistral.ai/v1/chat/completions'; + +/** + * Agent role system prompts + */ +const AGENT_PROMPTS = { + architect: `You are a Software Architect AI. Your role is to: +- Design high-level system architecture +- Make technology stack decisions +- Define project structure and modules +- Consider scalability and maintainability +- Provide clear technical justifications + +Output format: JSON with fields {proposal, justification, confidence (0-1), dependencies: []}`, + + backend_engineer: `You are a Backend Engineer AI. Your role is to: +- Design API endpoints and data models +- Suggest backend technologies and frameworks +- Plan database schema +- Consider performance and security +- Provide implementation guidelines + +Output format: JSON with fields {proposal, justification, confidence (0-1), dependencies: []}`, + + frontend_engineer: `You are a Frontend Engineer AI. Your role is to: +- Design user interface structure +- Suggest frontend frameworks and libraries +- Plan component architecture +- Consider UX and performance +- Provide implementation guidelines + +Output format: JSON with fields {proposal, justification, confidence (0-1), dependencies: []}`, + + designer: `You are a UI/UX Designer AI. Your role is to: +- Design user experience flows +- Suggest UI patterns and layouts +- Consider accessibility and usability +- Provide visual design guidelines +- Think about user interactions + +Output format: JSON with fields {proposal, justification, confidence (0-1), dependencies: []}` +}; + +/** + * Call Mistral AI API + */ +async function callMistralAPI(messages, options = {}) { + const response = await fetch(MISTRAL_API_URL, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${MISTRAL_API_KEY}` + }, + body: JSON.stringify({ + model: options.model || 'mistral-small-latest', + messages, + temperature: options.temperature || 0.7, + max_tokens: options.maxTokens || 2048, + ...options + }) + }); + + if (!response.ok) { + const error = await response.text(); + throw new Error(`Mistral API error: ${error}`); + } + + return await response.json(); +} + +/** + * Generate agent response for a debate + */ +export async function generateAgentResponse(agentRole, prompt, context = []) { + const systemPrompt = AGENT_PROMPTS[agentRole] || AGENT_PROMPTS.architect; + + const messages = [ + { role: 'system', content: systemPrompt }, + { role: 'user', content: `Project prompt: ${prompt}` } + ]; + + // Add context from previous responses + if (context.length > 0) { + const contextStr = context + .slice(-3) // Last 3 responses to avoid token bloat + .map(r => `${r.agent_role}: ${JSON.stringify(r.content)}`) + .join('\n'); + + messages.push({ + role: 'user', + content: `Previous discussion:\n${contextStr}\n\nProvide your analysis and proposal.` + }); + } + + try { + const result = await callMistralAPI(messages, { + temperature: 0.7, + maxTokens: 2048 + }); + + const content = result.choices[0].message.content; + + // Try to parse as JSON + let parsedContent; + try { + // Extract JSON from markdown code blocks if present + const jsonMatch = content.match(/```(?:json)?\s*(\{[\s\S]*\})\s*```/) || + content.match(/(\{[\s\S]*\})/); + + if (jsonMatch) { + parsedContent = JSON.parse(jsonMatch[1]); + } else { + parsedContent = JSON.parse(content); + } + } catch (parseError) { + // If not valid JSON, create structured response + parsedContent = { + proposal: content, + justification: `Analysis from ${agentRole}`, + confidence: 0.7, + dependencies: [] + }; + } + + // Ensure required fields + return { + proposal: parsedContent.proposal || content, + justification: parsedContent.justification || '', + confidence: parsedContent.confidence || 0.7, + dependencies: parsedContent.dependencies || [], + mermaid: parsedContent.mermaid || null + }; + + } catch (error) { + console.error(`Error generating response for ${agentRole}:`, error); + + // Return mock response on error + return { + proposal: `Error generating response: ${error.message}`, + justification: 'Failed to get AI response', + confidence: 0.5, + dependencies: [], + error: true + }; + } +} + +/** + * Generate responses from multiple agents in parallel + */ +export async function generateMultiAgentResponses(agents, prompt, context = []) { + const promises = agents.map(agent => + generateAgentResponse(agent, prompt, context) + .then(response => ({ agent, response })) + ); + + return await Promise.all(promises); +} + +export default { + generateAgentResponse, + generateMultiAgentResponses +}; diff --git a/backend/src/services/orchestrator.js b/backend/src/services/orchestrator.js index 82b59e0..8a35aaa 100644 --- a/backend/src/services/orchestrator.js +++ b/backend/src/services/orchestrator.js @@ -1,8 +1,43 @@ import db from '../db/schema.js'; +import { generateMultiAgentResponses } from './mistralClient.js'; class Orchestrator { constructor() { this.activeDebates = new Map(); + this.wsClients = new Map(); // debateId -> Set of WebSocket clients + } + + /** + * Register WebSocket client for a debate + */ + registerWSClient(debateId, ws) { + if (!this.wsClients.has(debateId)) { + this.wsClients.set(debateId, new Set()); + } + this.wsClients.get(debateId).add(ws); + } + + /** + * Unregister WebSocket client + */ + unregisterWSClient(debateId, ws) { + if (this.wsClients.has(debateId)) { + this.wsClients.get(debateId).delete(ws); + } + } + + /** + * Broadcast message to all clients watching a debate + */ + broadcast(debateId, message) { + if (this.wsClients.has(debateId)) { + const data = JSON.stringify(message); + this.wsClients.get(debateId).forEach(ws => { + if (ws.readyState === 1) { // OPEN + ws.send(data); + } + }); + } } /** @@ -102,6 +137,100 @@ class Orchestrator { return agents; } + + /** + * Start AI debate - trigger agents and collect responses + */ + async startDebate(debateId, agents) { + try { + const debate = this.getDebate(debateId); + if (!debate) { + throw new Error('Debate not found'); + } + + const prompt = debate.prompt; + const context = this.getDebateResponses(debateId); + + // Broadcast debate start + this.broadcast(debateId, { + type: 'debate_start', + debateId, + agents, + message: 'AI agents are analyzing your project...' + }); + + // Generate responses from all agents in parallel + const agentResponses = await generateMultiAgentResponses(agents, prompt, context); + + // Store responses and broadcast each one + for (const { agent, response } of agentResponses) { + const responseId = this.addResponse(debateId, agent, response); + + this.broadcast(debateId, { + type: 'agent_response', + debateId, + responseId, + agent, + response + }); + } + + // Calculate consensus + const consensus = this.calculateConsensus(agentResponses); + + // Complete debate + this.completeDebate(debateId); + + this.broadcast(debateId, { + type: 'debate_complete', + debateId, + consensus, + message: 'Debate completed successfully' + }); + + return { + responses: agentResponses, + consensus + }; + + } catch (error) { + console.error('Error in debate:', error); + this.failDebate(debateId); + + this.broadcast(debateId, { + type: 'debate_error', + debateId, + error: error.message + }); + + throw error; + } + } + + /** + * Calculate consensus from agent responses + */ + calculateConsensus(agentResponses) { + const proposals = agentResponses.map(({ agent, response }) => ({ + agent, + proposal: response.proposal, + confidence: response.confidence || 0.5 + })); + + // Weight by confidence and architect gets 1.5x weight + const totalWeight = proposals.reduce((sum, p) => { + const weight = p.agent === 'architect' ? 1.5 : 1.0; + return sum + (p.confidence * weight); + }, 0); + + const avgConfidence = totalWeight / proposals.length; + + return { + proposals, + averageConfidence: avgConfidence, + status: avgConfidence >= 0.6 ? 'consensus_reached' : 'needs_discussion' + }; + } } export default new Orchestrator(); diff --git a/frontend/.env.example b/frontend/.env.example index 5317fce..27e71be 100644 --- a/frontend/.env.example +++ b/frontend/.env.example @@ -1 +1,2 @@ VITE_API_URL=http://localhost:3000 +VITE_WS_URL=ws://localhost:3000 diff --git a/frontend/src/components/DebateThread.vue b/frontend/src/components/DebateThread.vue index 616355a..077bbe1 100644 --- a/frontend/src/components/DebateThread.vue +++ b/frontend/src/components/DebateThread.vue @@ -2,7 +2,7 @@
Proposal: {{ response.response.proposal }}
+Justification: {{ response.response.justification }}
+Confidence: {{ Math.round(response.response.confidence * 100) }}%
+{{ response.response.mermaid.code || response.response.mermaid }}
+ Waiting for AI agents to respond...
+ +AI agents are analyzing your project...
+Status: {{ consensus.status }}
+Average Confidence: {{ Math.round(consensus.averageConfidence * 100) }}%
+