Skip to content

Commit e946571

Browse files
authored
Merge pull request #5 from One-Team-One-Goal/development
Deployment Version Finish
2 parents 7968dd4 + 50433d8 commit e946571

File tree

4 files changed

+219
-327
lines changed

4 files changed

+219
-327
lines changed

render.yaml

Lines changed: 0 additions & 13 deletions
This file was deleted.

src/assessment/assessment.service.ts

Lines changed: 86 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { generateText } from 'ai';
44
import { PrismaService } from 'prisma/prisma.service';
55
import { AdaptiveService } from '../adaptive/adaptive.service';
66
import { buildAdaptiveQuizPrompt } from './prompts/adaptive-quiz-generation';
7+
import { AI_CONFIG } from '../config/ai.config';
78

89
@Injectable()
910
export class AssessmentService {
@@ -39,6 +40,7 @@ export class AssessmentService {
3940
];
4041

4142
async extractJsonArray(text: string): Promise<any> {
43+
// 1. Extract JSON content from Markdown code blocks or find array brackets
4244
const jsonArrayMatch = text.match(/```json\s*([\s\S]*?)```/i) || text.match(/(\[\s*{[\s\S]*}\s*\])/);
4345
let jsonString = '';
4446

@@ -57,6 +59,16 @@ export class AssessmentService {
5759
throw new Error('AI response did not contain a valid JSON array');
5860
}
5961

62+
// 2. Clean the JSON string to handle common LLM errors
63+
// Remove single-line comments (// ...)
64+
jsonString = jsonString.replace(/\/\/.*$/gm, '');
65+
// Remove multi-line comments (/* ... */)
66+
jsonString = jsonString.replace(/\/\*[\s\S]*?\*\//g, '');
67+
// Remove trailing commas before closing brackets/braces
68+
jsonString = jsonString.replace(/,(\s*[\]}])/g, '$1');
69+
// Fix unquoted keys (simple cases like key: "value")
70+
jsonString = jsonString.replace(/([{,]\s*)([a-zA-Z0-9_]+)(\s*:)/g, '$1"$2"$3');
71+
6072
try {
6173
const parsed = JSON.parse(jsonString);
6274

@@ -68,7 +80,7 @@ export class AssessmentService {
6880

6981
return parsed;
7082
} catch (err) {
71-
console.error('JSON parse error:', err.message, '\nJSON string:', jsonString.substring(0, 500));
83+
console.error('JSON parse error:', err.message, '\nJSON string snippet:', jsonString.substring(0, 500));
7284
throw new Error(`Failed to parse AI response as JSON: ${err.message}`);
7385
}
7486
}
@@ -263,64 +275,90 @@ private getTagRecommendation(tag: string): string | null {
263275

264276
if (!lessons || lessons.length < 4) throw new Error('Not enough lessons found');
265277

266-
// Adjust question distribution based on user's weak areas
267278
const focusTopicIds = recommendations.focusTopics.map(t => t.topicId);
279+
const focusLessonIds = recommendations.focusTopics.map(t => t.lessonId);
268280

269281
let lesson1Topics = lessons.find(l => l.id === 1)?.topics ?? [];
270282
let lesson2Topics = lessons.find(l => l.id === 2)?.topics ?? [];
271283
let lesson3Topics = lessons.find(l => l.id === 3)?.topics ?? [];
272284
let lesson4Topics = lessons.find(l => l.id === 4)?.topics ?? [];
273285

274-
// Prioritize focus topics - move them to the front of their respective lesson arrays
275-
if (focusTopicIds.length > 0) {
276-
lesson1Topics = lesson1Topics.filter(t => focusTopicIds.includes(t.id)).concat(
277-
lesson1Topics.filter(t => !focusTopicIds.includes(t.id))
278-
);
279-
lesson2Topics = lesson2Topics.filter(t => focusTopicIds.includes(t.id)).concat(
280-
lesson2Topics.filter(t => !focusTopicIds.includes(t.id))
281-
);
282-
lesson3Topics = lesson3Topics.filter(t => focusTopicIds.includes(t.id)).concat(
283-
lesson3Topics.filter(t => !focusTopicIds.includes(t.id))
284-
);
285-
lesson4Topics = lesson4Topics.filter(t => focusTopicIds.includes(t.id)).concat(
286-
lesson4Topics.filter(t => !focusTopicIds.includes(t.id))
287-
);
286+
const distribution = { lesson1: 3, lesson2: 3, lesson3: 3, lesson4: 3 };
287+
let remainingQuestions = 8;
288+
289+
if (focusLessonIds.length > 0) {
290+
const lessonCounts = focusLessonIds.reduce((acc, id) => {
291+
acc[id] = (acc[id] || 0) + 1;
292+
return acc;
293+
}, {} as Record<number, number>);
294+
295+
const totalFocusCount = focusLessonIds.length;
296+
297+
Object.entries(lessonCounts).forEach(([lessonId, count]) => {
298+
const share = Math.round((count / totalFocusCount) * remainingQuestions);
299+
if (lessonId === '1') distribution.lesson1 += share;
300+
if (lessonId === '2') distribution.lesson2 += share;
301+
if (lessonId === '3') distribution.lesson3 += share;
302+
if (lessonId === '4') distribution.lesson4 += share;
303+
});
304+
305+
const currentSum = Object.values(distribution).reduce((a, b) => a + b, 0);
306+
const diff = 20 - currentSum;
307+
308+
if (diff !== 0) {
309+
const maxFocusLesson = Object.keys(lessonCounts).reduce((a, b) => lessonCounts[a] > lessonCounts[b] ? a : b, '4');
310+
if (maxFocusLesson === '1') distribution.lesson1 += diff;
311+
else if (maxFocusLesson === '2') distribution.lesson2 += diff;
312+
else if (maxFocusLesson === '3') distribution.lesson3 += diff;
313+
else distribution.lesson4 += diff;
314+
}
315+
} else {
316+
distribution.lesson1 += 2;
317+
distribution.lesson2 += 2;
318+
distribution.lesson3 += 2;
319+
distribution.lesson4 += 2;
288320
}
289321

290-
// Select topics for adaptive question distribution
322+
const getTopicsForContext = (topics: any[], count: number) => {
323+
const sorted = [...topics].sort((a, b) => {
324+
const aIsFocus = focusTopicIds.includes(a.id);
325+
const bIsFocus = focusTopicIds.includes(b.id);
326+
return (aIsFocus === bIsFocus) ? 0 : aIsFocus ? -1 : 1;
327+
});
328+
return sorted.slice(0, Math.max(count, 3));
329+
};
330+
291331
const topicSelections = [
292-
...lesson1Topics.slice(0, 2),
293-
...lesson2Topics.slice(0, 6),
294-
...lesson3Topics.slice(0, 6),
295-
...lesson4Topics.slice(0, 6),
332+
...getTopicsForContext(lesson1Topics, distribution.lesson1),
333+
...getTopicsForContext(lesson2Topics, distribution.lesson2),
334+
...getTopicsForContext(lesson3Topics, distribution.lesson3),
335+
...getTopicsForContext(lesson4Topics, distribution.lesson4),
296336
].filter(Boolean);
297337

298-
// Generate questions with adaptive difficulty
299338
const promptParts = topicSelections.map((topic, idx) => `
300-
Topic ${idx + 1}:
301-
Title: ${topic.title}
339+
Topic: ${topic.title} (Lesson ${topic.lessonId})
302340
Content: ${topic.contentText}
303341
`);
304342

305343
const prompt = buildAdaptiveQuizPrompt({
306344
userMasteryPercent: parseFloat((recommendations.overallMastery * 100).toFixed(1)),
307345
recommendedDifficulty: recommendations.recommendedDifficulty,
308346
focusTopics: recommendations.focusTopics.map(t => t.topicTitle),
309-
topicContents: promptParts
347+
topicContents: promptParts,
348+
questionDistribution: distribution
310349
});
311350

312-
// Read model and sampling controls from env with safe defaults
313-
const modelName = process.env.GROQ_MODEL || "llama-3.1-8b-instant";
314-
const temp = process.env.LLM_TEMPERATURE ? Number(process.env.LLM_TEMPERATURE) : 0.3;
315-
const topP = process.env.LLM_TOP_P ? Number(process.env.LLM_TOP_P) : 0.9;
351+
// Read model and sampling controls from AI_CONFIG
352+
const modelName = AI_CONFIG.modelName;
353+
const temp = AI_CONFIG.temperature;
354+
const topP = AI_CONFIG.topP;
316355

317-
// Helper to call the model and parse JSON with minimal retry for robustness
318356
const requestQuestions = async () => {
319357
const { text } = await generateText({
320358
model: groq(modelName),
321359
prompt,
322-
temperature: isNaN(temp) ? 0.3 : temp,
323-
topP: isNaN(topP) ? 0.9 : topP,
360+
temperature: temp,
361+
topP: topP,
324362
});
325363
return this.extractJsonArray(text);
326364
};
@@ -331,38 +369,33 @@ private getTagRecommendation(tag: string): string | null {
331369
if (!Array.isArray(questions)) {
332370
console.error('AI generated non-array response:', questions);
333371
// Retry once with slightly lower temperature for consistency
334-
const retryTemp = Math.max(0.1, (isNaN(temp) ? 0.3 : temp) - 0.1);
372+
const retryTemp = Math.max(0.1, temp - 0.1);
335373
const { text: retryText } = await generateText({
336374
model: groq(modelName),
337375
prompt,
338376
temperature: retryTemp,
339-
topP: isNaN(topP) ? 0.9 : topP,
377+
topP: topP,
340378
});
341379
questions = await this.extractJsonArray(retryText);
342380
if (!Array.isArray(questions)) {
343381
throw new Error('Failed to generate valid questions array from AI response (after retry)');
344382
}
345383
}
346384

347-
// Enhanced validation and filtering
348385
const filteredQuestions = (questions || []).map((q: any, index: number) => {
349-
// Ensure exactly one correct answer
350386
const correctOptions = q.options?.filter((opt: any) => opt.isCorrect) || [];
351387
if (correctOptions.length !== 1) {
352388
console.warn(`Question ${index + 1} has ${correctOptions.length} correct answers, should have exactly 1`);
353-
// Fix by ensuring only the first correct option is marked as correct
354389
q.options?.forEach((opt: any, idx: number) => {
355390
opt.isCorrect = idx === 0 && correctOptions.length === 0 ? true :
356391
opt.isCorrect && correctOptions.indexOf(opt) === 0;
357392
});
358393
}
359394

360-
// Ensure 3-4 options
361395
if (q.options?.length < 3) {
362396
console.warn(`Question ${index + 1} has only ${q.options?.length} options, minimum is 3`);
363397
}
364398

365-
// Set answer ID to correct option
366399
const correctOption = q.options?.find((opt: any) => opt.isCorrect);
367400
if (correctOption) {
368401
q.answerId = correctOption.id;
@@ -381,6 +414,20 @@ private getTagRecommendation(tag: string): string | null {
381414

382415
// Additional validation
383416
const validQuestions = filteredQuestions.filter((q: any) => {
417+
// Check for broken visuals (text implies visual but stem is string)
418+
if (typeof q.stem === 'string') {
419+
const lowerStem = q.stem.toLowerCase();
420+
if (lowerStem.includes('table below') ||
421+
lowerStem.includes('circuit below') ||
422+
lowerStem.includes('map below') ||
423+
lowerStem.includes('shown below') ||
424+
lowerStem.includes('following truth table') ||
425+
lowerStem.includes('following circuit')) {
426+
console.warn(`Question rejected: Text implies visual but stem is string: "${q.stem.substring(0, 50)}..."`);
427+
return false;
428+
}
429+
}
430+
384431
return q.stem &&
385432
q.options &&
386433
q.options.length >= 3 &&

0 commit comments

Comments
 (0)