diff --git a/backend/jest.config.js b/backend/jest.config.js new file mode 100644 index 0000000..6d6062b --- /dev/null +++ b/backend/jest.config.js @@ -0,0 +1,4 @@ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', +}; diff --git a/backend/package-lock.json b/backend/package-lock.json index 7f5613a..db32725 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -16,13 +16,17 @@ "ws": "^8.4.0" }, "devDependencies": { + "@types/cors": "^2.8.19", + "@types/dotenv": "^6.1.1", "@types/express": "^4.17.13", "@types/jest": "^27.0.3", "@types/node": "^16.11.12", + "@types/supertest": "^6.0.3", "@types/uuid": "^8.3.4", "@types/ws": "^8.2.2", "jest": "^27.4.3", "nodemon": "^2.0.15", + "supertest": "^7.1.4", "ts-jest": "^27.1.0", "ts-node": "^10.4.0", "typescript": "^4.5.2" @@ -926,6 +930,29 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@paralleldrive/cuid2": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/@paralleldrive/cuid2/-/cuid2-2.2.2.tgz", + "integrity": "sha512-ZOBkgDwEdoYVlSeRbYYXs0S9MejQofiVYoTbKzy/6GQa39/q5tQU2IX46+shYnUkpEl3wc+J6wRlar7r2EK2xA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@noble/hashes": "^1.1.5" + } + }, "node_modules/@sinonjs/commons": { "version": "1.8.6", "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.6.tgz", @@ -1050,6 +1077,33 @@ "@types/node": "*" } }, + "node_modules/@types/cookiejar": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.5.tgz", + "integrity": "sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/cors": { + "version": "2.8.19", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz", + "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/dotenv": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/@types/dotenv/-/dotenv-6.1.1.tgz", + "integrity": "sha512-ftQl3DtBvqHl9L16tpqqzA4YzCSXZfi7g8cQceTz5rOlYtk/IZbFjAv3mLOQlNIgOaylCQWQoBdDQHPgEBJPHg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/express": { "version": "4.17.23", "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.23.tgz", @@ -1131,6 +1185,13 @@ "pretty-format": "^27.0.0" } }, + "node_modules/@types/methods": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@types/methods/-/methods-1.1.4.tgz", + "integrity": "sha512-ymXWVrDiCxTBE3+RIrrP533E70eA+9qu7zdWoHuOmGujkYtzf4HQF96b8nwHLqhuf4ykX61IGRIB38CC6/sImQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/mime": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", @@ -1206,6 +1267,47 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/superagent": { + "version": "8.1.9", + "resolved": "https://registry.npmjs.org/@types/superagent/-/superagent-8.1.9.tgz", + "integrity": "sha512-pTVjI73witn+9ILmoJdajHGW2jkSaOzhiFYF1Rd3EQ94kymLqB9PjD9ISg7WaALC7+dCHT0FGe9T2LktLq/3GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/cookiejar": "^2.1.5", + "@types/methods": "^1.1.4", + "@types/node": "*", + "form-data": "^4.0.0" + } + }, + "node_modules/@types/superagent/node_modules/form-data": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", + "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@types/supertest": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@types/supertest/-/supertest-6.0.3.tgz", + "integrity": "sha512-8WzXq62EXFhJ7QsH3Ocb/iKQ/Ty9ZVWnVzoTKc9tyyFRRF3a74Tk2+TLFgaFFw364Ere+npzHKEJ6ga2LzIL7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/methods": "^1.1.4", + "@types/superagent": "^8.1.0" + } + }, "node_modules/@types/uuid": { "version": "8.3.4", "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-8.3.4.tgz", @@ -1425,6 +1527,13 @@ "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", "license": "MIT" }, + "node_modules/asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", + "dev": true, + "license": "MIT" + }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -1898,6 +2007,16 @@ "node": ">= 0.8" } }, + "node_modules/component-emitter": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.1.tgz", + "integrity": "sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -1948,6 +2067,13 @@ "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", "license": "MIT" }, + "node_modules/cookiejar": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.4.tgz", + "integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==", + "dev": true, + "license": "MIT" + }, "node_modules/cors": { "version": "2.8.5", "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", @@ -2097,6 +2223,17 @@ "node": ">=8" } }, + "node_modules/dezalgo": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz", + "integrity": "sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==", + "dev": true, + "license": "ISC", + "dependencies": { + "asap": "^2.0.0", + "wrappy": "1" + } + }, "node_modules/diff": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", @@ -2446,6 +2583,13 @@ "dev": true, "license": "MIT" }, + "node_modules/fast-safe-stringify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", + "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", + "dev": true, + "license": "MIT" + }, "node_modules/fb-watchman": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", @@ -2518,6 +2662,24 @@ "node": ">= 6" } }, + "node_modules/formidable": { + "version": "3.5.4", + "resolved": "https://registry.npmjs.org/formidable/-/formidable-3.5.4.tgz", + "integrity": "sha512-YikH+7CUTOtP44ZTnUhR7Ic2UASBPOqmaRkRKxRbywPTe5VxF7RRCck4af9wutiZ/QKM5nME9Bie2fFaPz5Gug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@paralleldrive/cuid2": "^2.2.2", + "dezalgo": "^1.0.4", + "once": "^1.4.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "url": "https://ko-fi.com/tunnckoCore/commissions" + } + }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -5153,6 +5315,96 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/superagent": { + "version": "10.2.3", + "resolved": "https://registry.npmjs.org/superagent/-/superagent-10.2.3.tgz", + "integrity": "sha512-y/hkYGeXAj7wUMjxRbB21g/l6aAEituGXM9Rwl4o20+SX3e8YOSV6BxFXl+dL3Uk0mjSL3kCbNkwURm8/gEDig==", + "dev": true, + "license": "MIT", + "dependencies": { + "component-emitter": "^1.3.1", + "cookiejar": "^2.1.4", + "debug": "^4.3.7", + "fast-safe-stringify": "^2.1.1", + "form-data": "^4.0.4", + "formidable": "^3.5.4", + "methods": "^1.1.2", + "mime": "2.6.0", + "qs": "^6.11.2" + }, + "engines": { + "node": ">=14.18.0" + } + }, + "node_modules/superagent/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/superagent/node_modules/form-data": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", + "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/superagent/node_modules/mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/superagent/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/supertest": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/supertest/-/supertest-7.1.4.tgz", + "integrity": "sha512-tjLPs7dVyqgItVFirHYqe2T+MfWc2VOBQ8QFKKbWTA3PU7liZR8zoSpAi/C1k1ilm9RsXIKYf197oap9wXGVYg==", + "dev": true, + "license": "MIT", + "dependencies": { + "methods": "^1.1.2", + "superagent": "^10.2.3" + }, + "engines": { + "node": ">=14.18.0" + } + }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", diff --git a/backend/package.json b/backend/package.json index 9f0a783..cec4d15 100644 --- a/backend/package.json +++ b/backend/package.json @@ -13,17 +13,20 @@ "author": "", "license": "ISC", "devDependencies": { + "@types/cors": "^2.8.19", + "@types/dotenv": "^6.1.1", "@types/express": "^4.17.13", "@types/jest": "^27.0.3", "@types/node": "^16.11.12", + "@types/supertest": "^6.0.3", "@types/uuid": "^8.3.4", "@types/ws": "^8.2.2", "jest": "^27.4.3", "nodemon": "^2.0.15", + "supertest": "^7.1.4", "ts-jest": "^27.1.0", "ts-node": "^10.4.0", - "typescript": "^4.5.2", - "@types/cors": "^2.8.19" + "typescript": "^4.5.2" }, "dependencies": { "@google/generative-ai": "^0.1.0", diff --git a/backend/src/services/LLMService.ts b/backend/src/services/LLMService.ts index dfad7d2..158022d 100644 --- a/backend/src/services/LLMService.ts +++ b/backend/src/services/LLMService.ts @@ -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> { - const allDesires: string[] = []; - desireSets.forEach(set => { - allDesires.push(...set.wants, ...set.accepts, ...set.noGoes); - }); + async analyzeDesires(desireSets: DesireSet[]): Promise { + 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 { + 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; + } + } + } + diff --git a/backend/src/ws/index.ts b/backend/src/ws/index.ts index 8c90f28..eab49cb 100644 --- a/backend/src/ws/index.ts +++ b/backend/src/ws/index.ts @@ -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(); - - 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; diff --git a/backend/tests/LLMService.refactor.test.ts b/backend/tests/LLMService.refactor.test.ts new file mode 100644 index 0000000..8de7baf --- /dev/null +++ b/backend/tests/LLMService.refactor.test.ts @@ -0,0 +1,33 @@ +import { LLMService } from '../src/services/LLMService'; +require('dotenv').config(); + +describe('LLMService Refactor', () => { + let llmService: LLMService; + + beforeAll(() => { + const apiKey = process.env.GEMINI_API_KEY; + if (!apiKey) { + throw new Error('GEMINI_API_KEY is not defined in .env file'); + } + llmService = new LLMService(apiKey); + }); + + it('should correctly categorize desires based on the rules', async () => { + const desireSets = [ + { participantId: '1', wants: ['Pizza'], accepts: ['Pasta'], noGoes: ['Salad'] }, + { participantId: '2', wants: ['Pizza'], accepts: ['Pasta'], noGoes: ['Tacos'] }, + ]; + + const result = await llmService.analyzeDesires(desireSets as any); + + expect(result.goTo).toContain('Pizza'); + expect(result.alsoGood).toContain('Pasta'); + expect(result.noGoes).toEqual(expect.arrayContaining(['Salad', 'Tacos'])); + }); + + it('should detect inner contradictions in a desire set', async () => { + const desireSet = { wants: ['Ice Cream', 'No desserts'], accepts: [], noGoes: [] }; + const hasContradictions = await llmService.checkForInnerContradictions(desireSet as any); + expect(hasContradictions).toBe(true); + }); +}); diff --git a/backend/tests/llmService.test.ts b/backend/tests/llmService.test.ts deleted file mode 100644 index 620fea4..0000000 --- a/backend/tests/llmService.test.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { GoogleGenerativeAI } from '@google/generative-ai'; -import { LLMService } from '../src/services/LLMService'; - -// Mock the GoogleGenerativeAI class and its methods -jest.mock('@google/generative-ai', () => ({ - GoogleGenerativeAI: jest.fn().mockImplementation(() => ({ - getGenerativeModel: jest.fn().mockReturnValue({ - generateContent: jest.fn().mockResolvedValue({ - response: { - text: jest.fn().mockReturnValue( - JSON.stringify({ - "item1": "Concept A", - "item2": "Concept A", - "item3": "Concept B" - }) - ), - }, - }), - }), - })), -})); - -describe('LLMService', () => { - let llmService: LLMService; - const mockApiKey = 'test-api-key'; - - beforeEach(() => { - llmService = new LLMService(mockApiKey); - }); - - it('should call the Gemini API with the correct prompt and return parsed content', async () => { - const desires = [ - { wants: ['item1'], accepts: [], noGoes: [] }, - { wants: ['item2'], accepts: [], noGoes: [] }, - { wants: [], accepts: ['item3'], noGoes: [] }, - ]; - - const result = await llmService.analyzeDesires(desires); - - expect(GoogleGenerativeAI).toHaveBeenCalledWith(mockApiKey); - expect(llmService['model'].generateContent).toHaveBeenCalled(); - expect(result).toEqual({ - "item1": "Concept A", - "item2": "Concept A", - "item3": "Concept B" - }); - }); - - it('should handle API errors gracefully', async () => { - llmService['model'].generateContent.mockRejectedValueOnce(new Error('API Error')); - - await expect(llmService.analyzeDesires([])).rejects.toThrow('API Error'); - }); -}); diff --git a/backend/tsconfig.json b/backend/tsconfig.json index 861ca4b..b6bd1fc 100644 --- a/backend/tsconfig.json +++ b/backend/tsconfig.json @@ -2,6 +2,8 @@ "compilerOptions": { "target": "es6", "module": "commonjs", + "moduleResolution": "node", + "types": ["node", "jest"], "outDir": "./dist", "rootDir": "./src", "strict": true, @@ -9,6 +11,6 @@ "skipLibCheck": true, "forceConsistentCasingInFileNames": true }, - "include": ["src/**/*"], - "exclude": ["node_modules", "dist", "tests"] + "include": ["src/**/*", "tests/**/*"], + "exclude": ["node_modules", "dist"] } \ No newline at end of file diff --git a/frontend/src/components/DesireForm.tsx b/frontend/src/components/DesireForm.tsx index 7f2303d..4b173d7 100644 --- a/frontend/src/components/DesireForm.tsx +++ b/frontend/src/components/DesireForm.tsx @@ -48,6 +48,8 @@ const DesireForm: React.FC = ({ onSubmit }) => { value={wants} onChange={(e) => setWants(e.target.value)} margin="normal" + inputProps={{ maxLength: 500 }} + helperText={`${wants.length}/500`} /> What you ACCEPT @@ -59,6 +61,8 @@ const DesireForm: React.FC = ({ onSubmit }) => { value={accepts} onChange={(e) => setAccepts(e.target.value)} margin="normal" + inputProps={{ maxLength: 500 }} + helperText={`${accepts.length}/500`} /> What you DO NOT WANT @@ -70,6 +74,8 @@ const DesireForm: React.FC = ({ onSubmit }) => { value={noGoes} onChange={(e) => setNoGoes(e.target.value)} margin="normal" + inputProps={{ maxLength: 500 }} + helperText={`${noGoes.length}/500`} />