Skip to content

Commit 3fe8a24

Browse files
committed
Merge branch 'NXD-12-generate-quiz' into NXD-12
Signed-off-by: Tal Jacob <taljacob2@gmail.com>
2 parents 5cc6a08 + 4ea1ab2 commit 3fe8a24

5 files changed

Lines changed: 137 additions & 11 deletions

File tree

.vscode/launch.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,21 +9,21 @@
99
"name": "Backend: Run Script: dev",
1010
"request": "launch",
1111
"command": "npm run dev",
12-
"cwd": "${workspaceFolder}\\nextstep-backend"
12+
"cwd": "${workspaceFolder}/nextstep-backend"
1313
},
1414
{
1515
"type": "node-terminal",
1616
"name": "Backend: Run Script: test",
1717
"request": "launch",
1818
"command": "npm run test",
19-
"cwd": "${workspaceFolder}\\nextstep-backend"
19+
"cwd": "${workspaceFolder}/nextstep-backend"
2020
},
2121
{
2222
"type": "node-terminal",
2323
"name": "Frontend: Run Script: dev",
2424
"request": "launch",
2525
"command": "npm run dev",
26-
"cwd": "${workspaceFolder}\\nextstep-frontend"
26+
"cwd": "${workspaceFolder}/nextstep-frontend"
2727
}
2828
]
2929
}

nextstep-backend/src/controllers/quizzes_controller.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,20 @@ const getQuizzesByTags = async (req: Request, res: Response): Promise<void> => {
1414
}
1515
};
1616

17-
export default { getQuizzesByTags };
17+
const getGeneratedQuizBySubject = async (req: Request, res: Response): Promise<void> => {
18+
try {
19+
const { subject } = req.body;
20+
21+
if (!subject) {
22+
res.status(400).json({ error: 'Missing required fields' });
23+
return;
24+
}
25+
26+
const generatedQuiz = await companiesService.generateQuiz(subject);
27+
res.json(generatedQuiz);
28+
} catch (err) {
29+
handleError(err, res);
30+
}
31+
};
32+
33+
export default { getQuizzesByTags, getGeneratedQuizBySubject };

nextstep-backend/src/openapi/swagger.yaml

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1116,7 +1116,7 @@ paths:
11161116
'400':
11171117
description: Bad Request
11181118

1119-
/quiz:
1119+
/quiz/raw:
11201120
get:
11211121
tags:
11221122
- Quizzes
@@ -1145,6 +1145,48 @@ paths:
11451145
'500':
11461146
description: Internal server error
11471147

1148+
/quiz/generate:
1149+
post:
1150+
tags:
1151+
- Quizzes
1152+
summary: Generate a quiz for a certain subject.
1153+
description: Generate a quiz for a certain subject. The quiz is generated based on our real-world quizzes database.
1154+
security:
1155+
- BearerAuth: []
1156+
requestBody:
1157+
required: true
1158+
content:
1159+
application/json:
1160+
schema:
1161+
type: object
1162+
required:
1163+
- subject
1164+
properties:
1165+
subject:
1166+
type: string
1167+
description: The subject of the quiz to be generated
1168+
responses:
1169+
'200':
1170+
description: A generated quiz custom-tailored for the targeted subject.
1171+
content:
1172+
application/json:
1173+
schema:
1174+
type: object # TODO:
1175+
'400':
1176+
description: Bad request - Missing required fields
1177+
content:
1178+
application/json:
1179+
schema:
1180+
type: object
1181+
properties:
1182+
error:
1183+
type: string
1184+
description: Error message indicating missing fields
1185+
'401':
1186+
description: Unauthorized
1187+
'500':
1188+
description: Internal server error
1189+
11481190
components:
11491191
schemas:
11501192
Post:

nextstep-backend/src/routes/quizzes_routes.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import Quiz from '../controllers/quizzes_controller';
33

44
const router = express.Router();
55

6-
router.get('/', Quiz.getQuizzesByTags);
6+
router.get('/raw', Quiz.getQuizzesByTags);
7+
8+
router.post('/generate', Quiz.getGeneratedQuizBySubject);
79

810
export default router;

nextstep-backend/src/services/companies_service.ts

Lines changed: 71 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { CompanyModel } from '../models/company_model';
55
import { CompanyData, ICompany, QuizData } from 'types/company_types';
66
import { Document, PipelineStage } from 'mongoose';
77
import path from 'path';
8+
import { chatWithAI } from './chat_api_service';
89

910
// Predefined tags to match against quizzes
1011
const PREDEFINED_TAGS = [
@@ -304,16 +305,15 @@ export const initCompanies = async (): Promise<number> => {
304305
return allCompanies.length;
305306
};
306307

307-
308308
/**
309309
* Search for quizzes that best match the given tags using MongoDB aggregation
310310
* and $text search for content fields.
311311
* Returns quizzes sorted by relevance (text score + tag matches).
312312
*/
313313
export const searchQuizzesByTags = async (tags: string[]): Promise<QuizData[]> => {
314-
if (!tags || tags.length === 0) {
315-
return []; // No tags provided, return empty results
316-
}
314+
// if (!tags || tags.length === 0) {
315+
// return []; // No tags provided, return empty results
316+
// }
317317

318318
// For $text search, tags are space-separated words or phrases.
319319
// We'll also use them for array matching.
@@ -462,4 +462,70 @@ export const searchQuizzesByTags = async (tags: string[]): Promise<QuizData[]> =
462462
console.error('Error during quiz search aggregation:', error);
463463
return [];
464464
}
465-
};
465+
};
466+
467+
/**
468+
* Generate a custom-tailored quiz on any subject, based on our real quiz database.
469+
* @param quizSubject The subject you want to generate a quiz for.
470+
* Example: "Java Spring Boot Microservices Interview Questions" or "QA Automation Python"
471+
*/
472+
export const generateQuiz = async (quizSubject: string): Promise<any> => {
473+
// Take the first 10 best matching quizzes.
474+
const tags = quizSubject.split(' ');
475+
const quizzes = (await searchQuizzesByTags(tags)).slice(0, 10);
476+
477+
const GEN_QUIZ_SYSTEM_PROMPT = "You are an AI assistant specialized in generating personalized interview quiz content based on real-world interview data. Your goal is to create a new, well-structured interview quiz tailored to a user's specific search query, drawing insights and patterns from a provided set of actual interview quizzes.";
478+
479+
const prompt = `
480+
**User Search Query:**
481+
${quizSubject}
482+
483+
**Relevant Real Quiz Data (JSON Array):**
484+
\`\`\`json
485+
${quizzes}
486+
\`\`\`
487+
488+
**Instructions:**
489+
Analyze: Review the User Search Query and the provided Relevant Real Quiz Data.
490+
Synthesize: Combine common themes, technologies, question types, process details, and implied soft skill requirements found across the relevant real quizzes, prioritizing relevance to the user's query.
491+
Generate One Quiz: Create one (1) brand new, highly relevant, and unique interview quiz specifically tailored to the User Search Query. Do NOT simply copy-paste existing quizzes.
492+
Structure: Format the output as a single JSON object adhering strictly to the QuizData interface below.
493+
Content Details:
494+
_id: Generate a unique string, e.g., "generated_[TIMESTAMP]".
495+
title: A concise and relevant title for the generated quiz.
496+
tags: Include highly relevant technical and role-based tags, both English and Hebrew if appropriate, derived from the query and the provided data.
497+
content: A comprehensive narrative combining the interview process and specific questions, similar in style to the provided examples.
498+
job_role: Infer the most appropriate job role from the user query and real data.
499+
company_name_en, company_name_he: You can use a generic "Leading Tech Company" / "חברת טכנולוגיה מובילה" or "Startup in [relevant field]" / "סטארטאפ בתחום ה[רלוונטי]".
500+
process_details: A concise summary of a typical interview process for this role/company type, extracted or synthesized from the provided data.
501+
question_list: Crucially, all generated specific interview questions. An array of individual questions. Each element should be a distinct question.
502+
answer_list: Crucially, parse the question_list string into an array of individual answers. Each element should be a distinct answer, corresponding to the questions.
503+
keywords: Extract 5-10 additional relevant technical or conceptual keywords that the user might find useful for preparation.
504+
interviewer_mindset: Describe the soft skills, characteristics, temperament, and professional attributes that an interviewer for this specific job role (based on the user's query and the context from real quizzes) would likely be looking for. Focus on traits that would give the applicant "extra points," such as straightforwardness, curiosity, social skills, professionalism, collaboration, communication (with colleagues, 3rd parties, customers), problem-solving approach, adaptability, initiative, attention to detail, etc. Aim for a paragraph or two.
505+
506+
**Desired Output Format (JSON)**:
507+
\`\`\`json
508+
{
509+
"_id": "string",
510+
"title": "string",
511+
"tags": ["string", "string", ...],
512+
"content": "string",
513+
"job_role": "string",
514+
"company_name_en": "string",
515+
"company_name_he": "string",
516+
"process_details": "string",
517+
"question_list": ["string", "string", ...],
518+
"answer_list": ["string", "string", ...],
519+
"keywords": ["string", "string", ...],
520+
"interviewer_mindset": "string"
521+
}
522+
\`\`\`
523+
524+
Return ONLY the JSON, without any other text, so I could easily retrieve it.
525+
`;
526+
527+
const aiResponse = await chatWithAI(GEN_QUIZ_SYSTEM_PROMPT, [prompt]);
528+
const parsed = JSON.parse(aiResponse.trim().replace("```json", "").replace("```", "")) as any;
529+
530+
return parsed;
531+
}

0 commit comments

Comments
 (0)