feat: Refactor result preparation and add contradiction checks

This commit is contained in:
AG
2025-10-11 17:03:13 +03:00
parent 42303b1fc3
commit fd2676dd5f
14 changed files with 436 additions and 211 deletions

View File

@@ -6,6 +6,17 @@ interface DesireSet {
noGoes: string[];
}
interface Decision {
goTo: string[];
alsoGood: string[];
considerable: string[];
noGoes: string[];
needsDiscussion: string[];
}
export class LLMService {
private genAI: GoogleGenerativeAI;
private model: GenerativeModel;
@@ -15,27 +26,30 @@ export class LLMService {
this.model = this.genAI.getGenerativeModel({ model: "gemini-2.5-flash-lite" });
}
async analyzeDesires(desireSets: DesireSet[]): Promise<Record<string, string>> {
const allDesires: string[] = [];
desireSets.forEach(set => {
allDesires.push(...set.wants, ...set.accepts, ...set.noGoes);
});
async analyzeDesires(desireSets: DesireSet[]): Promise<Decision> {
const prompt = `
You are an AI assistant that analyzes and categorizes user desires from a session. Given a list of desire sets from multiple participants, your task is to categorize them into the following groups: "goTo", "alsoGood", "considerable", "noGoes", and "needsDiscussion".
const uniqueDesires = Array.from(new Set(allDesires.filter(d => d.trim() !== '')));
Here are the rules for categorization:
- "goTo": Items that ALL participants want.
- "alsoGood": Items that at least one participant wants, and all other participants accept.
- "considerable": Items that are wanted or accepted by some, but not all, participants, and are not "noGoes" for anyone.
- "noGoes": Items that at least ONE participant does not want. These items must be excluded from all other categories.
- "needsDiscussion": Items where there is a direct conflict (e.g., one participant wants it, another does not want it).
if (uniqueDesires.length === 0) {
return {};
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".
The output should be a JSON object with the following structure:
{
"goTo": ["item1", "item2"],
"alsoGood": ["item3"],
"considerable": ["item4"],
"noGoes": ["item5"],
"needsDiscussion": ["item6"]
}
const prompt = `
You are an AI assistant that groups similar desires. Given a list of desires, identify semantically equivalent or very similar items and group them under a single, concise canonical name. Return the output as a JSON object where keys are the original desire strings and values are their canonical group names.
Example:
Input: ["go for a walk", "walking", "stroll in the park", "eat pizza", "pizza for dinner"]
Output: {"go for a walk": "Go for a walk", "walking": "Go for a walk", "stroll in the park": "Go for a walk", "eat pizza": "Eat pizza", "pizza for dinner": "Eat pizza"}
Here is the list of desires to group:
${JSON.stringify(uniqueDesires)}
Here is the input data:
${JSON.stringify(desireSets)}
`;
try {
@@ -43,9 +57,8 @@ export class LLMService {
const response = result.response;
let text = response.text();
const newLocal = text.match(/\{.*?\}/s);
// Clean the response to ensure it is valid JSON
const jsonMatch = newLocal;
const jsonMatch = text.match(/\{.*?\}/s);
if (jsonMatch) {
text = jsonMatch[0];
} else {
@@ -60,4 +73,24 @@ export class LLMService {
throw error;
}
}
}
async checkForInnerContradictions(desireSet: DesireSet): Promise<boolean> {
const prompt = `
You are an AI assistant that detects contradictions in a list of desires. Given a JSON object with three lists of desires (wants, accepts, noGoes), determine if there are any contradictions WITHIN each list. For example, "I want a dog" and "I don't want any pets" in the same "wants" list is a contradiction. Respond with only "true" if a contradiction is found, and "false" otherwise.
Here is the desire set:
${JSON.stringify(desireSet)}
`;
try {
const result = await this.model.generateContent(prompt);
const response = result.response;
const text = response.text().trim().toLowerCase();
return text === 'true';
} catch (error) {
console.error("Error calling Gemini API for contradiction check:", error);
throw error;
}
}
}

View File

@@ -3,16 +3,12 @@ import { LLMService } from '../services/LLMService';
import { v4 as uuidv4 } from 'uuid';
// Types from the frontend
interface SemanticDesire {
title: string;
rawInputs: string[];
}
interface Decision {
goTos: SemanticDesire[];
alsoGoods: SemanticDesire[];
considerables: SemanticDesire[];
noGoes: SemanticDesire[];
goTo: string[];
alsoGood: string[];
considerable: string[];
noGoes: string[];
needsDiscussion: string[];
}
// Define the SessionState enum
@@ -138,6 +134,19 @@ export const createWebSocketServer = (server: any) => {
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 hasContradictions = await llmService.checkForInnerContradictions(payload.response);
if (hasContradictions) {
ws.send(JSON.stringify({ type: 'ERROR', payload: { message: 'Your submission contains inner contradictions. 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}`);
@@ -151,71 +160,7 @@ export const createWebSocketServer = (server: any) => {
(async () => {
try {
const allDesires = Array.from(sessionData.responses.values());
const canonicalMap = await llmService.analyzeDesires(allDesires);
const semanticDesiresMap = new Map<string, SemanticDesire>();
for (const originalDesire in canonicalMap) {
const canonicalName = canonicalMap[originalDesire];
if (!semanticDesiresMap.has(canonicalName)) {
semanticDesiresMap.set(canonicalName, { title: canonicalName, rawInputs: [] });
}
semanticDesiresMap.get(canonicalName)?.rawInputs.push(originalDesire);
}
const decision: Decision = {
goTos: [],
alsoGoods: [],
considerables: [],
noGoes: [],
};
const participantIds = Array.from(sessionData.responses.keys());
semanticDesiresMap.forEach(semanticDesire => {
let isNoGo = false;
let allWant = true;
let atLeastOneWant = false;
let allAcceptOrWant = true;
for (const pId of participantIds) {
const participantDesireSet = sessionData.responses.get(pId);
if (!participantDesireSet) continue;
const participantWants = new Set(participantDesireSet.wants.map((d: string) => canonicalMap[d] || d));
const participantAccepts = new Set(participantDesireSet.accepts.map((d: string) => canonicalMap[d] || d));
const participantNoGoes = new Set(participantDesireSet.noGoes.map((d: string) => canonicalMap[d] || d));
const canonicalTitle = semanticDesire.title;
if (participantNoGoes.has(canonicalTitle)) {
isNoGo = true;
break;
}
if (!participantWants.has(canonicalTitle)) {
allWant = false;
}
if (participantWants.has(canonicalTitle)) {
atLeastOneWant = true;
}
if (!participantWants.has(canonicalTitle) && !participantAccepts.has(canonicalTitle)) {
allAcceptOrWant = false;
}
}
if (isNoGo) {
decision.noGoes.push(semanticDesire);
} else if (allWant) {
decision.goTos.push(semanticDesire);
} else if (atLeastOneWant && allAcceptOrWant) {
decision.alsoGoods.push(semanticDesire);
} else if (atLeastOneWant || !allAcceptOrWant) {
decision.considerables.push(semanticDesire);
}
});
const decision = await llmService.analyzeDesires(allDesires);
sessionData.finalResult = decision;
sessionData.state = SessionState.FINAL;