'Afraid to Ask' implemented
This commit is contained in:
@@ -1,3 +1,6 @@
|
||||
import dotenv from 'dotenv';
|
||||
dotenv.config();
|
||||
|
||||
import express from 'express';
|
||||
import http from 'http';
|
||||
import { createWebSocketServer } from './ws';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import express from 'express';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { sessions, SessionState } from '../ws'; // Import sessions and SessionState from ws/index.ts
|
||||
import { sessions, SessionState, broadcastToSession, handleWebSocketMessage } from '../ws'; // Import sessions, SessionState, broadcastToSession, and handleWebSocketMessage from ws/index.ts
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
@@ -19,4 +19,68 @@ router.post('/sessions', (req, res) => {
|
||||
res.status(201).json({ sessionId });
|
||||
});
|
||||
|
||||
router.post('/sessions/:sessionId/responses', async (req, res) => {
|
||||
const { sessionId } = req.params;
|
||||
const { userId, wants, accepts, afraidToAsk } = req.body;
|
||||
|
||||
if (!sessions.has(sessionId)) {
|
||||
return res.status(404).json({ message: 'Session not found.' });
|
||||
}
|
||||
|
||||
// Create a dummy WebSocket object for the handleWebSocketMessage function.
|
||||
// This is a workaround to reuse the WebSocket message handling logic.
|
||||
// In a real application, consider a more robust event-driven architecture.
|
||||
const dummyWs: any = {
|
||||
send: (message: string) => console.log('Dummy WS send:', message),
|
||||
readyState: 1, // OPEN
|
||||
};
|
||||
|
||||
const message = {
|
||||
type: 'SUBMIT_RESPONSE',
|
||||
clientId: userId, // Using userId as clientId for simplicity in this context
|
||||
payload: {
|
||||
response: { wants, accepts, afraidToAsk },
|
||||
},
|
||||
};
|
||||
|
||||
try {
|
||||
await handleWebSocketMessage(dummyWs, sessionId, message);
|
||||
res.status(202).json({ message: 'Response submission acknowledged and processed.' });
|
||||
} catch (error: any) {
|
||||
console.error('Error processing response via HTTP route:', error);
|
||||
res.status(500).json({ message: 'Error processing response.', error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/sessions/:sessionId/results', (req, res) => {
|
||||
const { sessionId } = req.params;
|
||||
|
||||
if (!sessions.has(sessionId)) {
|
||||
return res.status(404).json({ message: 'Session not found.' });
|
||||
}
|
||||
|
||||
const sessionData = sessions.get(sessionId)!;
|
||||
|
||||
if (sessionData.state !== SessionState.FINAL || !sessionData.finalResult) {
|
||||
return res.status(200).json({ message: 'Session results not yet finalized.', harmonizedIdeas: [] });
|
||||
}
|
||||
|
||||
// Assuming finalResult directly contains the harmonized ideas as per openapi.yaml
|
||||
res.status(200).json({ sessionId, harmonizedIdeas: sessionData.finalResult });
|
||||
});
|
||||
|
||||
router.post('/sessions/:sessionId/terminate', (req, res) => {
|
||||
const { sessionId } = req.params;
|
||||
|
||||
if (!sessions.has(sessionId)) {
|
||||
return res.status(404).json({ message: 'Session not found.' });
|
||||
}
|
||||
|
||||
sessions.delete(sessionId);
|
||||
// Log the purging event
|
||||
// logEvent('session_terminated_and_purged', sessionId);
|
||||
console.log(`Session ${sessionId} terminated and data purged.`);
|
||||
res.status(200).json({ message: 'Session terminated and data purged successfully.' });
|
||||
});
|
||||
|
||||
export default router;
|
||||
38
backend/src/services/EncryptionService.ts
Normal file
38
backend/src/services/EncryptionService.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import crypto from 'crypto';
|
||||
|
||||
const algorithm = 'aes-256-cbc';
|
||||
const ivLength = 16; // For AES, this is always 16
|
||||
|
||||
// Key should be a 32-byte (256-bit) key
|
||||
// In a real application, this would be loaded securely from environment variables
|
||||
// or a key management service.
|
||||
const ENCRYPTION_KEY = process.env.ENCRYPTION_KEY || crypto.randomBytes(32).toString('hex');
|
||||
|
||||
export class EncryptionService {
|
||||
private readonly key: Buffer;
|
||||
|
||||
constructor(encryptionKey: string) {
|
||||
if (!encryptionKey || encryptionKey.length !== 64) { // 32 bytes in hex is 64 chars
|
||||
throw new Error('Encryption key must be a 64-character hex string (32 bytes).');
|
||||
}
|
||||
this.key = Buffer.from(encryptionKey, 'hex');
|
||||
}
|
||||
|
||||
encrypt(text: string): string {
|
||||
const iv = crypto.randomBytes(ivLength);
|
||||
const cipher = crypto.createCipheriv(algorithm, this.key, iv);
|
||||
let encrypted = cipher.update(text);
|
||||
encrypted = Buffer.concat([encrypted, cipher.final()]);
|
||||
return iv.toString('hex') + ':' + encrypted.toString('hex');
|
||||
}
|
||||
|
||||
decrypt(text: string): string {
|
||||
const textParts = text.split(':');
|
||||
const iv = Buffer.from(textParts.shift()!, 'hex');
|
||||
const encryptedText = Buffer.from(textParts.join(':'), 'hex');
|
||||
const decipher = crypto.createDecipheriv(algorithm, this.key, iv);
|
||||
let decrypted = decipher.update(encryptedText);
|
||||
decrypted = Buffer.concat([decrypted, decipher.final()]);
|
||||
return decrypted.toString();
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ interface DesireSet {
|
||||
wants: string[];
|
||||
accepts: string[];
|
||||
noGoes: string[];
|
||||
afraidToAsk: string;
|
||||
}
|
||||
|
||||
|
||||
@@ -28,18 +29,22 @@ export class LLMService {
|
||||
|
||||
async analyzeDesires(desireSets: DesireSet[]): Promise<Decision> {
|
||||
const prompt = `
|
||||
You are an AI assistant that analyzes and synthesizes cooperative decisions from a group's desires. Given a list of desire sets from multiple participants, your task is to generate a concise, synthesized text for each of the following categories, reflecting the collective opinion:
|
||||
You are an AI assistant that analyzes and synthesizes cooperative decisions from a group's desires. Given a list of desire sets from multiple participants, your task is to generate a concise, synthesized text for each of the following categories, reflecting the collective opinion.
|
||||
|
||||
Here are the rules for categorization and synthesis:
|
||||
- "goTo": Synthesize a text describing what ALL participants want without contradictions. This should be a clear, affirmative statement of shared desire.
|
||||
- "alsoGood": Synthesize a text describing what at least one participant wants, and all other participants accept, and is not a "noGoes" for anyone. This should reflect a generally agreeable outcome.
|
||||
- "considerable": Synthesize a text describing what is wanted or accepted by some, but not all, participants, and is not a "noGoes" for anyone. This should highlight areas of partial agreement or options that could be explored.
|
||||
Each participant's desire set includes 'wants', 'accepts', 'noGoes', and an 'afraidToAsk' field. The 'afraidToAsk' field contains a sensitive idea that the participant is hesitant to express publicly.
|
||||
|
||||
Here are the rules for categorization and synthesis, with special handling for 'afraidToAsk' ideas:
|
||||
- "goTo": Synthesize a text describing what ALL participants want without contradictions. This should include 'afraidToAsk' ideas that semantically match all other participant's 'wants' or 'afraidToAsk'. If an 'afraidToAsk' idea matches, it should be treated as a 'want' for the submitting participant.
|
||||
- "alsoGood": Synthesize a text describing what at least one participant wants (including matched 'afraidToAsk' ideas), not everyone wants but all other participants at least accept, and is not a "noGoes" for anyone. This should reflect a generally agreeable outcome.
|
||||
- "considerable": Synthesize a text describing what is wanted or accepted by some, but not all, participants (including matched 'afraidToAsk' ideas), and is not a "noGoes" for anyone. This should highlight areas of partial agreement or options that could be explored.
|
||||
- "noGoes": Synthesize a text describing what at least ONE participant does not want. This should clearly state the collective exclusions.
|
||||
- "needsDiscussion": Synthesize a text describing where there is a direct conflict (e.g., one participant wants it, another does not want it). This should highlight areas requiring further negotiation.
|
||||
- "needsDiscussion": Synthesize a text describing where there is a direct conflict (e.g., one participant wants it, another does not want it). This should highlight areas requiring further negotiation. Do not include 'afraidToAsk' in this category.
|
||||
|
||||
Prioritize the more specific opinions and leave all the specific options if they do not contradict each other drastically.
|
||||
'AfraidToAsk' ideas that do NOT semantically match any other participant's 'wants' or 'accepts' should remain private and NOT be included in any of the synthesized categories.
|
||||
|
||||
The input will be a JSON object containing a list of desire sets. Each desire set has a participantId and three arrays of strings: "wants", "accepts", and "noGoes".
|
||||
Prioritize more specific desires over more broad ones for positive categories ("goTo", "alsoGood", "considerable"). For negative categories ("noGoes", "needsDiscussion"), prioritize more broad ideas over more specific ones. Formulate common ideas from the point of 'us', e.g. "We are going to...", or "We want to...", or "We think..."
|
||||
|
||||
The input will be a JSON object containing a list of desire sets. Each desire set has a participantId (implicitly handled by the array index) and four arrays/strings: "wants", "accepts", "noGoes", and "afraidToAsk".
|
||||
|
||||
The output should be a JSON object with the following structure, where each category contains a single synthesized text:
|
||||
{
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import { WebSocketServer, WebSocket } from 'ws';
|
||||
import { LLMService } from '../services/LLMService';
|
||||
import { EncryptionService } from '../services/EncryptionService';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
// Initialize Encryption Service
|
||||
const encryptionService = new EncryptionService(process.env.ENCRYPTION_KEY || '');
|
||||
|
||||
// Types from the frontend
|
||||
interface Decision {
|
||||
goTo: string;
|
||||
@@ -20,13 +24,20 @@ export enum SessionState {
|
||||
ERROR = 'ERROR',
|
||||
}
|
||||
|
||||
interface EncryptedResponseData {
|
||||
wants: string[];
|
||||
accepts: string[];
|
||||
noGoes: string[];
|
||||
afraidToAsk: string; // Encrypted afraidToAsk idea
|
||||
}
|
||||
|
||||
// A map to hold session data, including clients and the latest state
|
||||
interface SessionData {
|
||||
state: SessionState; // Current phase of the session
|
||||
topic: string | null; // The topic of the session
|
||||
expectedResponses: number; // The number set by the first user in State A.
|
||||
submittedCount: number; // The current count of submitted responses.
|
||||
responses: Map<string, any>; // Stores the submitted desire objects. Map<ClientID, ResponseData>
|
||||
responses: Map<string, EncryptedResponseData>; // Stores the submitted desire objects. Map<ClientID, EncryptedResponseData>
|
||||
clients: Map<string, WebSocket>; // Maps the persistent Client ID to their active WebSocket connection object.
|
||||
finalResult: any | null; // The result returned by the LLM.
|
||||
}
|
||||
@@ -35,11 +46,39 @@ export const sessions = new Map<string, SessionData>();
|
||||
// Initialize LLM Service (API key from environment)
|
||||
const llmService = new LLMService(process.env.GEMINI_API_KEY || '');
|
||||
|
||||
// Structured logging function
|
||||
const logEvent = (eventName: string, sessionId: string, details: object = {}) => {
|
||||
console.log(JSON.stringify({ timestamp: new Date().toISOString(), eventName, sessionId, ...details }));
|
||||
};
|
||||
|
||||
// Metrics recording function
|
||||
const recordMetric = (metricName: string, value: number | string, sessionId: string, details: object = {}) => {
|
||||
console.log(JSON.stringify({ timestamp: new Date().toISOString(), metricName, value, sessionId, ...details }));
|
||||
};
|
||||
|
||||
// Helper to create a serializable version of the session state
|
||||
const getSerializableSession = (sessionData: SessionData) => {
|
||||
const getSerializableSession = (sessionData: SessionData, currentClientId: string | null = null) => {
|
||||
const filteredResponses = new Map<string, any>();
|
||||
sessionData.responses.forEach((response, clientId) => {
|
||||
if (clientId === currentClientId) {
|
||||
// For the current client, decrypt and send their own full response
|
||||
const decryptedWants = response.wants.map((d: string) => encryptionService.decrypt(d));
|
||||
const decryptedAccepts = response.accepts.map((d: string) => encryptionService.decrypt(d));
|
||||
const decryptedNoGoes = response.noGoes.map((d: string) => encryptionService.decrypt(d));
|
||||
const decryptedAfraidToAsk = encryptionService.decrypt(response.afraidToAsk);
|
||||
filteredResponses.set(clientId, { wants: decryptedWants, accepts: decryptedAccepts, noGoes: decryptedNoGoes, afraidToAsk: decryptedAfraidToAsk });
|
||||
} else {
|
||||
// For other clients, only send non-AfraidToAsk parts (wants, accepts, noGoes without AfraidToAsk)
|
||||
const decryptedWants = response.wants.map((d: string) => encryptionService.decrypt(d));
|
||||
const decryptedAccepts = response.accepts.map((d: string) => encryptionService.decrypt(d));
|
||||
const decryptedNoGoes = response.noGoes.map((d: string) => encryptionService.decrypt(d));
|
||||
filteredResponses.set(clientId, { wants: decryptedWants, accepts: decryptedAccepts, noGoes: decryptedNoGoes, afraidToAsk: "" }); // Hide afraidToAsk for other clients
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
...sessionData,
|
||||
responses: Object.fromEntries(sessionData.responses),
|
||||
responses: Object.fromEntries(filteredResponses),
|
||||
clients: Array.from(sessionData.clients.keys()), // Only send client IDs, not the WebSocket objects
|
||||
};
|
||||
};
|
||||
@@ -47,15 +86,15 @@ const getSerializableSession = (sessionData: SessionData) => {
|
||||
export const broadcastToSession = (sessionId: string, message: any, excludeClientId: string | null = null) => {
|
||||
const sessionData = sessions.get(sessionId);
|
||||
if (sessionData) {
|
||||
const serializableMessage = {
|
||||
...message,
|
||||
payload: {
|
||||
...message.payload,
|
||||
session: getSerializableSession(sessionData),
|
||||
},
|
||||
};
|
||||
sessionData.clients.forEach((client, clientId) => {
|
||||
if (clientId !== excludeClientId && client.readyState === WebSocket.OPEN) {
|
||||
const serializableMessage = {
|
||||
...message,
|
||||
payload: {
|
||||
...message.payload,
|
||||
session: getSerializableSession(sessionData, clientId), // Pass clientId to getSerializableSession
|
||||
},
|
||||
};
|
||||
client.send(JSON.stringify(serializableMessage));
|
||||
}
|
||||
});
|
||||
@@ -106,87 +145,7 @@ export const createWebSocketServer = (server: any) => {
|
||||
}
|
||||
|
||||
console.log(`Received message from ${clientId} in session ${sessionId}:`, type);
|
||||
|
||||
switch (type) {
|
||||
case 'REGISTER_CLIENT':
|
||||
break;
|
||||
|
||||
case 'SETUP_SESSION':
|
||||
if (sessionData.state === SessionState.SETUP) {
|
||||
const { expectedResponses, topic } = payload;
|
||||
if (typeof expectedResponses !== 'number' || expectedResponses <= 0) {
|
||||
ws.send(JSON.stringify({ type: 'ERROR', payload: { message: 'Invalid expectedResponses' } }));
|
||||
return;
|
||||
}
|
||||
sessionData.expectedResponses = expectedResponses;
|
||||
sessionData.topic = topic || 'Untitled Session';
|
||||
sessionData.state = SessionState.GATHERING;
|
||||
broadcastToSession(sessionId, { type: 'STATE_UPDATE', payload: {} });
|
||||
console.log(`Session ${sessionId} moved to GATHERING with topic "${sessionData.topic}" and ${expectedResponses} expected responses.`);
|
||||
} else {
|
||||
ws.send(JSON.stringify({ type: 'ERROR', payload: { message: `Session is not in SETUP state. Current state: ${sessionData.state}` } }));
|
||||
}
|
||||
break;
|
||||
|
||||
case 'SUBMIT_RESPONSE':
|
||||
if (sessionData.state === SessionState.GATHERING) {
|
||||
if (sessionData.responses.has(clientId)) {
|
||||
ws.send(JSON.stringify({ type: 'ERROR', payload: { message: 'You have already submitted a response for this session.' } }));
|
||||
return;
|
||||
}
|
||||
|
||||
const { wants, accepts, noGoes } = payload.response;
|
||||
if ([...wants, ...accepts, ...noGoes].some(desire => desire.length > 500)) {
|
||||
ws.send(JSON.stringify({ type: 'ERROR', payload: { message: 'One of your desires exceeds the 500 character limit.' } }));
|
||||
return;
|
||||
}
|
||||
|
||||
const hasContradictionsGist = await llmService.checkForInnerContradictions(payload.response);
|
||||
if (hasContradictionsGist) {
|
||||
ws.send(JSON.stringify({ type: 'ERROR', payload: { message: `Your submission contains inner contradictions: ${hasContradictionsGist} Please resolve them and submit again.` } }));
|
||||
return;
|
||||
}
|
||||
|
||||
sessionData.responses.set(clientId, payload.response);
|
||||
sessionData.submittedCount++;
|
||||
console.log(`Client ${clientId} submitted response. Submitted count: ${sessionData.submittedCount}/${sessionData.expectedResponses}`);
|
||||
|
||||
if (sessionData.submittedCount === sessionData.expectedResponses) {
|
||||
sessionData.state = SessionState.HARMONIZING;
|
||||
broadcastToSession(sessionId, { type: 'STATE_UPDATE', payload: {} });
|
||||
console.log(`Session ${sessionId} moved to HARMONIZING. Triggering LLM analysis.`);
|
||||
|
||||
// Perform LLM analysis asynchronously
|
||||
(async () => {
|
||||
try {
|
||||
const allDesires = Array.from(sessionData.responses.values());
|
||||
const decision = await llmService.analyzeDesires(allDesires);
|
||||
|
||||
sessionData.finalResult = decision;
|
||||
sessionData.state = SessionState.FINAL;
|
||||
broadcastToSession(sessionId, { type: 'STATE_UPDATE', payload: {} });
|
||||
console.log(`Analysis complete for session ${sessionId}. Result:`, decision);
|
||||
|
||||
} catch (error: any) {
|
||||
console.error(`Error during analysis for session ${sessionId}:`, error.message);
|
||||
sessionData.state = SessionState.ERROR;
|
||||
broadcastToSession(sessionId, { type: 'STATE_UPDATE', payload: {} });
|
||||
}
|
||||
})();
|
||||
} else {
|
||||
// Only broadcast the latest count if the session is not yet harmonizing
|
||||
broadcastToSession(sessionId, { type: 'STATE_UPDATE', payload: {} });
|
||||
}
|
||||
} else {
|
||||
ws.send(JSON.stringify({ type: 'ERROR', payload: { message: `Session is not in GATHERING state. Current state: ${sessionData.state}` } }));
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
console.warn(`Unknown message type: ${type} from client ${clientId} in session ${sessionId}`);
|
||||
ws.send(JSON.stringify({ type: 'ERROR', payload: { message: `Unknown message type: ${type}` } }));
|
||||
break;
|
||||
}
|
||||
await handleWebSocketMessage(ws, sessionId, parsedMessage);
|
||||
});
|
||||
|
||||
ws.on('close', () => {
|
||||
@@ -207,6 +166,7 @@ export const createWebSocketServer = (server: any) => {
|
||||
|
||||
if (sessionData.clients.size === 0) {
|
||||
sessions.delete(sessionId);
|
||||
logEvent('session_purged', sessionId);
|
||||
console.log(`Session ${sessionId} closed and state cleared.`);
|
||||
}
|
||||
});
|
||||
@@ -218,3 +178,123 @@ export const createWebSocketServer = (server: any) => {
|
||||
|
||||
return wss;
|
||||
};
|
||||
|
||||
export const handleWebSocketMessage = async (ws: WebSocket, sessionId: string, parsedMessage: any) => {
|
||||
const { type, clientId, payload } = parsedMessage;
|
||||
|
||||
if (!clientId) {
|
||||
console.error(`Received message without clientId in session ${sessionId}. Type: ${type}`);
|
||||
ws.send(JSON.stringify({ type: 'ERROR', payload: { message: 'clientId is required' } }));
|
||||
return;
|
||||
}
|
||||
|
||||
const sessionData = sessions.get(sessionId)!;
|
||||
|
||||
if (!sessionData.clients.has(clientId)) {
|
||||
sessionData.clients.set(clientId, ws);
|
||||
console.log(`Client ${clientId} registered for session: ${sessionId}. Total clients: ${sessionData.clients.size}`);
|
||||
ws.send(JSON.stringify({ type: 'STATE_UPDATE', payload: { session: getSerializableSession(sessionData, clientId) } }));
|
||||
}
|
||||
|
||||
console.log(`Received message from ${clientId} in session ${sessionId}:`, type);
|
||||
|
||||
switch (type) {
|
||||
case 'REGISTER_CLIENT':
|
||||
break;
|
||||
|
||||
case 'SETUP_SESSION':
|
||||
if (sessionData.state === SessionState.SETUP) {
|
||||
const { expectedResponses, topic } = payload;
|
||||
if (typeof expectedResponses !== 'number' || expectedResponses <= 0) {
|
||||
ws.send(JSON.stringify({ type: 'ERROR', payload: { message: 'Invalid expectedResponses' } }));
|
||||
return;
|
||||
}
|
||||
sessionData.expectedResponses = expectedResponses;
|
||||
sessionData.topic = topic || 'Untitled Session';
|
||||
sessionData.state = SessionState.GATHERING;
|
||||
broadcastToSession(sessionId, { type: 'STATE_UPDATE', payload: {} });
|
||||
console.log(`Session ${sessionId} moved to GATHERING with topic "${sessionData.topic}" and ${expectedResponses} expected responses.`);
|
||||
} else {
|
||||
ws.send(JSON.stringify({ type: 'ERROR', payload: { message: `Session is not in SETUP state. Current state: ${sessionData.state}` } }));
|
||||
}
|
||||
break;
|
||||
|
||||
case 'SUBMIT_RESPONSE':
|
||||
if (sessionData.state === SessionState.GATHERING) {
|
||||
if (sessionData.responses.has(clientId)) {
|
||||
ws.send(JSON.stringify({ type: 'ERROR', payload: { message: 'You have already submitted a response for this session.' } }));
|
||||
return;
|
||||
}
|
||||
|
||||
const { wants, accepts, noGoes, afraidToAsk } = payload.response;
|
||||
if ([...wants, ...accepts, ...noGoes].some(desire => desire.length > 500) || afraidToAsk.length > 500) {
|
||||
ws.send(JSON.stringify({ type: 'ERROR', payload: { message: 'One of your desires or afraidToAsk exceeds the 500 character limit.' } }));
|
||||
return;
|
||||
}
|
||||
|
||||
const hasContradictionsGist = await llmService.checkForInnerContradictions(payload.response);
|
||||
if (hasContradictionsGist) {
|
||||
ws.send(JSON.stringify({ type: 'ERROR', payload: { message: `Your submission contains inner contradictions: ${hasContradictionsGist} Please resolve them and submit again.` } }));
|
||||
return;
|
||||
}
|
||||
|
||||
const encryptedWants = wants.map((d: string) => encryptionService.encrypt(d));
|
||||
const encryptedAccepts = accepts.map((d: string) => encryptionService.encrypt(d));
|
||||
const encryptedNoGoes = noGoes.map((d: string) => encryptionService.encrypt(d));
|
||||
const encryptedAfraidToAsk = encryptionService.encrypt(afraidToAsk);
|
||||
|
||||
sessionData.responses.set(clientId, { wants: encryptedWants, accepts: encryptedAccepts, noGoes: encryptedNoGoes, afraidToAsk: encryptedAfraidToAsk });
|
||||
sessionData.submittedCount++;
|
||||
logEvent('response_submitted', sessionId, { clientId, submittedCount: sessionData.submittedCount });
|
||||
console.log(`Client ${clientId} submitted response. Submitted count: ${sessionData.submittedCount}/${sessionData.expectedResponses}`);
|
||||
|
||||
if (sessionData.submittedCount === sessionData.expectedResponses) {
|
||||
sessionData.state = SessionState.HARMONIZING;
|
||||
broadcastToSession(sessionId, { type: 'STATE_UPDATE', payload: {} });
|
||||
logEvent('session_harmonizing', sessionId, { expectedResponses: sessionData.expectedResponses });
|
||||
console.log(`Session ${sessionId} moved to HARMONIZING. Triggering LLM analysis.`);
|
||||
|
||||
// Perform LLM analysis asynchronously
|
||||
(async () => {
|
||||
let durationMs: number = 0; // Declare here
|
||||
try {
|
||||
logEvent('llm_analysis_started', sessionId);
|
||||
const startTime = process.hrtime.bigint(); const allDecryptedDesires = Array.from(sessionData.responses.values()).map(encryptedResponse => {
|
||||
const decryptedWants = encryptedResponse.wants.map((d: string) => encryptionService.decrypt(d));
|
||||
const decryptedAccepts = encryptedResponse.accepts.map((d: string) => encryptionService.decrypt(d));
|
||||
const decryptedNoGoes = encryptedResponse.noGoes.map((d: string) => encryptionService.decrypt(d));
|
||||
const decryptedAfraidToAsk = encryptionService.decrypt(encryptedResponse.afraidToAsk);
|
||||
return { wants: decryptedWants, accepts: decryptedAccepts, noGoes: decryptedNoGoes, afraidToAsk: decryptedAfraidToAsk };
|
||||
});
|
||||
const decision = await llmService.analyzeDesires(allDecryptedDesires);
|
||||
|
||||
sessionData.finalResult = decision;
|
||||
sessionData.state = SessionState.FINAL;
|
||||
broadcastToSession(sessionId, { type: 'STATE_UPDATE', payload: {} });
|
||||
logEvent('llm_analysis_completed', sessionId, { result: decision });
|
||||
recordMetric('llm_analysis_duration', durationMs, sessionId, { status: 'success' }); recordMetric('llm_analysis_availability', 'available', sessionId);
|
||||
console.log(`Analysis complete for session ${sessionId}. Result:`, decision);
|
||||
|
||||
} catch (error: any) {
|
||||
console.error(`Error during analysis for session ${sessionId}:`, error.message);
|
||||
sessionData.state = SessionState.ERROR;
|
||||
broadcastToSession(sessionId, { type: 'STATE_UPDATE', payload: {} });
|
||||
logEvent('llm_analysis_error', sessionId, { error: error.message });
|
||||
recordMetric('llm_analysis_availability', 'unavailable', sessionId, { error: error.message });
|
||||
}
|
||||
})();
|
||||
} else {
|
||||
// Only broadcast the latest count if the session is not yet harmonizing
|
||||
broadcastToSession(sessionId, { type: 'STATE_UPDATE', payload: {} });
|
||||
}
|
||||
} else {
|
||||
ws.send(JSON.stringify({ type: 'ERROR', payload: { message: `Session is not in GATHERING state. Current state: ${sessionData.state}` } }));
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
console.warn(`Unknown message type: ${type} from client ${clientId} in session ${sessionId}`);
|
||||
ws.send(JSON.stringify({ type: 'ERROR', payload: { message: `Unknown message type: ${type}` } }));
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user