@programisto/edrm-exams 0.3.12 → 0.3.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/modules/edrm-exams/lib/openai/correctQuestion.txt +14 -3
- package/dist/modules/edrm-exams/lib/openai/createQuestion.txt +34 -5
- package/dist/modules/edrm-exams/lib/openai.d.ts +4 -1
- package/dist/modules/edrm-exams/lib/openai.js +13 -61
- package/dist/modules/edrm-exams/listeners/correct.listener.js +62 -28
- package/dist/modules/edrm-exams/listeners/invite-from-job.listener.d.ts +2 -0
- package/dist/modules/edrm-exams/listeners/invite-from-job.listener.js +79 -0
- package/dist/modules/edrm-exams/routes/exams.router.js +5 -11
- package/package.json +3 -1
|
@@ -1,9 +1,20 @@
|
|
|
1
|
+
Tu es un correcteur de question de concours / test en ligne.
|
|
2
|
+
Tu dois donner un score à la question et à la réponse fournie par le candidat.
|
|
3
|
+
Tu dois répondre au format JSON, en français pour les explications de correction.
|
|
4
|
+
Modèle JSON attendu :
|
|
5
|
+
{
|
|
6
|
+
"score": score,
|
|
7
|
+
"comment": commentaire
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
1
12
|
Question : ${instruction}
|
|
2
|
-
Type de question : ${questionType}
|
|
13
|
+
Type de question : ${questionType}
|
|
3
14
|
- si MCQ les réponses possibles étaient : ${possibleResponses}
|
|
4
15
|
|
|
5
16
|
Réponse du candidat à corriger :
|
|
6
17
|
--------------------------------
|
|
7
|
-
|
|
18
|
+
${response}
|
|
8
19
|
--------------------------------
|
|
9
|
-
Score à donner :
|
|
20
|
+
Score à donner : de 0 point (tout faux ou réponse vide) à ${maxScore} points (réponse correcte), avec un commentaire de correction.
|
|
@@ -1,6 +1,35 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
Tu es un assistant qui crée des questions pour un outil de test en ligne.
|
|
2
|
+
L'utilisateur te demande de générer une question avec les critères suivants :
|
|
3
|
+
- Métier ciblé : ${job}
|
|
4
|
+
- Séniorité ciblée : ${seniority}
|
|
5
|
+
- Catégorie (domaine) : ${category}
|
|
6
|
+
- Niveau d'expertise sur cette catégorie : ${expertiseLevel} (sur une échelle : 'beginner' = 1 à 3, 'intermediate' = 3 à 6, 'advanced' = 6 à 10)
|
|
7
|
+
- Type de question demandé : ${questionType}
|
|
5
8
|
|
|
6
|
-
|
|
9
|
+
Contraintes :
|
|
10
|
+
- La question doit obligatoirement être en français.
|
|
11
|
+
- Les instructions et réponses doivent être compatibles avec LaTeX pour affichage dans des balises <MathJax>. Exemple : \\( \\frac{a}{b} = c \\)
|
|
12
|
+
- Le format de réponse doit être un JSON avec uniquement les champs du schéma ci-dessous, sans "_id".
|
|
13
|
+
|
|
14
|
+
Schéma JSON à produire :
|
|
15
|
+
{
|
|
16
|
+
"instruction": "string, required",
|
|
17
|
+
"questionType": "MCQ" | "free question" | "exercice",
|
|
18
|
+
"maxScore": "number, required",
|
|
19
|
+
"possibleResponses": [{"possibleResponse": "string", "valid": "boolean"}] // uniquement pour MCQ, 4 réponses dont une seule juste
|
|
20
|
+
"time": "number, en secondes, required",
|
|
21
|
+
"textType": "text" | "code" // "code" si la réponse attendue est du code, sinon "text"
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
Contraintes par type :
|
|
25
|
+
- MCQ : une seule réponse juste sur quatre au total ; time entre 30 s et 1 min.
|
|
26
|
+
- Free Question : ne rien mettre dans possibleResponses ; time entre 30 s et 5 min selon complexité.
|
|
27
|
+
- Exercice : ne rien mettre dans possibleResponses ; time entre 3 et 20 min selon complexité.
|
|
28
|
+
|
|
29
|
+
- Les instructions et la difficulté doivent être en rapport avec le métier et la séniorité.
|
|
30
|
+
- Le temps (time) doit laisser le temps de répondre sans favoriser la triche (surtout pour les MCQ).
|
|
31
|
+
|
|
32
|
+
Questions déjà présentes dans le test (éviter les doublons) :
|
|
33
|
+
${otherQuestions}
|
|
34
|
+
|
|
35
|
+
Génère une question au format JSON correspondant à la demande et au schéma ci-dessus.
|
|
@@ -32,6 +32,9 @@ interface ContextBuilder {
|
|
|
32
32
|
maxScore: number;
|
|
33
33
|
}>;
|
|
34
34
|
}
|
|
35
|
+
/**
|
|
36
|
+
* Génère une réponse via l'API Responses (gpt-5-mini).
|
|
37
|
+
* Pour forcer le JSON : text.format = { type: 'json_object' }.
|
|
38
|
+
*/
|
|
35
39
|
export declare function generateLiveMessage(messageType: keyof ContextBuilder, params: CreateQuestionParams | CorrectQuestionParams, json?: boolean): Promise<string>;
|
|
36
|
-
export declare function generateLiveMessageAssistant(assistantId: string, messageType: keyof ContextBuilder, params: CreateQuestionParams | CorrectQuestionParams, json?: boolean): Promise<string>;
|
|
37
40
|
export {};
|
|
@@ -2,6 +2,7 @@ import OpenAI from 'openai';
|
|
|
2
2
|
import { fileURLToPath } from 'url';
|
|
3
3
|
import fs from 'fs';
|
|
4
4
|
import path from 'path';
|
|
5
|
+
const DEFAULT_MODEL = 'gpt-5-mini';
|
|
5
6
|
const __filename = fileURLToPath(import.meta.url);
|
|
6
7
|
const __dirname = path.dirname(__filename);
|
|
7
8
|
const openai = new OpenAI({
|
|
@@ -41,6 +42,10 @@ const contextBuilder = {
|
|
|
41
42
|
return context;
|
|
42
43
|
}
|
|
43
44
|
};
|
|
45
|
+
/**
|
|
46
|
+
* Génère une réponse via l'API Responses (gpt-5-mini).
|
|
47
|
+
* Pour forcer le JSON : text.format = { type: 'json_object' }.
|
|
48
|
+
*/
|
|
44
49
|
export async function generateLiveMessage(messageType, params, json) {
|
|
45
50
|
const MAX_RETRY = 2;
|
|
46
51
|
let retryCount = 0;
|
|
@@ -49,17 +54,17 @@ export async function generateLiveMessage(messageType, params, json) {
|
|
|
49
54
|
const message = text.replace(/\${(.*?)}/g, (_, v) => context[v]);
|
|
50
55
|
while (retryCount <= MAX_RETRY) {
|
|
51
56
|
try {
|
|
52
|
-
const
|
|
53
|
-
model:
|
|
54
|
-
|
|
55
|
-
|
|
57
|
+
const createParams = {
|
|
58
|
+
model: DEFAULT_MODEL,
|
|
59
|
+
instructions: message,
|
|
60
|
+
input: json ? 'Réponds en JSON uniquement. Traite la demande.' : 'Traite la demande.'
|
|
56
61
|
};
|
|
57
62
|
if (json) {
|
|
58
|
-
|
|
63
|
+
createParams.text = { format: { type: 'json_object' } };
|
|
59
64
|
}
|
|
60
|
-
const result = await openai.
|
|
61
|
-
const content = result.
|
|
62
|
-
if (!content) {
|
|
65
|
+
const result = await openai.responses.create(createParams);
|
|
66
|
+
const content = result.output_text;
|
|
67
|
+
if (!content || typeof content !== 'string') {
|
|
63
68
|
throw new Error('No content in response');
|
|
64
69
|
}
|
|
65
70
|
return removeQuotes(content);
|
|
@@ -74,59 +79,6 @@ export async function generateLiveMessage(messageType, params, json) {
|
|
|
74
79
|
}
|
|
75
80
|
return 'Brain freezed, I cannot generate a live message right now.';
|
|
76
81
|
}
|
|
77
|
-
export async function generateLiveMessageAssistant(assistantId, messageType, params, json) {
|
|
78
|
-
const MAX_RETRY = 2;
|
|
79
|
-
let retryCount = 0;
|
|
80
|
-
// Construire le contexte pour le message
|
|
81
|
-
const context = await contextBuilder[messageType](params);
|
|
82
|
-
const text = fs.readFileSync(path.join(__dirname, 'openai', `${messageType}.txt`), 'utf8');
|
|
83
|
-
const message = text.replace(/\${(.*?)}/g, (_, v) => context[v]);
|
|
84
|
-
while (retryCount <= MAX_RETRY) {
|
|
85
|
-
try {
|
|
86
|
-
// Créer un thread avec l'assistant
|
|
87
|
-
const thread = await openai.beta.threads.create();
|
|
88
|
-
// Ajouter le message avec le contexte
|
|
89
|
-
await openai.beta.threads.messages.create(thread.id, {
|
|
90
|
-
role: 'user',
|
|
91
|
-
content: message
|
|
92
|
-
});
|
|
93
|
-
// Exécuter l'assistant
|
|
94
|
-
const run = await openai.beta.threads.runs.create(thread.id, {
|
|
95
|
-
assistant_id: assistantId
|
|
96
|
-
});
|
|
97
|
-
// Attendre que l'exécution soit terminée
|
|
98
|
-
let runStatus = await openai.beta.threads.runs.retrieve(thread.id, run.id);
|
|
99
|
-
while (runStatus.status === 'in_progress' || runStatus.status === 'queued') {
|
|
100
|
-
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
101
|
-
runStatus = await openai.beta.threads.runs.retrieve(thread.id, run.id);
|
|
102
|
-
}
|
|
103
|
-
if (runStatus.status === 'failed') {
|
|
104
|
-
throw new Error('Assistant execution failed');
|
|
105
|
-
}
|
|
106
|
-
// Récupérer les messages de réponse
|
|
107
|
-
const messages = await openai.beta.threads.messages.list(thread.id);
|
|
108
|
-
const lastMessage = messages.data[0]; // Le premier message est le plus récent
|
|
109
|
-
if (!lastMessage || !lastMessage.content || lastMessage.content.length === 0) {
|
|
110
|
-
throw new Error('No content in response');
|
|
111
|
-
}
|
|
112
|
-
const content = lastMessage.content[0];
|
|
113
|
-
if (content.type === 'text') {
|
|
114
|
-
return removeQuotes(content.text.value);
|
|
115
|
-
}
|
|
116
|
-
else {
|
|
117
|
-
throw new Error('Unexpected content type');
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
catch (error) {
|
|
121
|
-
retryCount++;
|
|
122
|
-
console.log(error);
|
|
123
|
-
if (retryCount > MAX_RETRY) {
|
|
124
|
-
return 'Brain freezed, I cannot generate a live message right now.';
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
return 'Brain freezed, I cannot generate a live message right now.';
|
|
129
|
-
}
|
|
130
82
|
function removeQuotes(str) {
|
|
131
83
|
if (str.startsWith('"') && str.endsWith('"')) {
|
|
132
84
|
return str.substring(1, str.length - 1);
|
|
@@ -1,11 +1,36 @@
|
|
|
1
1
|
import { enduranceListener, enduranceEventTypes, enduranceEmitter } from '@programisto/endurance';
|
|
2
2
|
import TestQuestion from '../models/test-question.model.js';
|
|
3
|
-
import {
|
|
3
|
+
import { generateLiveMessage } from '../lib/openai.js';
|
|
4
4
|
import { computeScoresByCategory } from '../lib/score-utils.js';
|
|
5
5
|
import TestResult, { TestState } from '../models/test-result.model.js';
|
|
6
6
|
import CandidateModel from '../models/candidate.model.js';
|
|
7
7
|
import ContactModel from '../models/contact.model.js';
|
|
8
8
|
import TestModel from '../models/test.model.js';
|
|
9
|
+
const QUESTION_TYPE_MCQ = 'MCQ';
|
|
10
|
+
/**
|
|
11
|
+
* Pour un QCM avec bonne réponse enregistrée (possibleResponses avec valid: true),
|
|
12
|
+
* retourne { score, comment } sans appeler OpenAI.
|
|
13
|
+
* Retourne null si la question n'est pas un QCM corrigeable automatiquement.
|
|
14
|
+
*/
|
|
15
|
+
function correctMcqIfPossible(question, candidateResponse) {
|
|
16
|
+
if (question.questionType !== QUESTION_TYPE_MCQ)
|
|
17
|
+
return null;
|
|
18
|
+
const possibleResponses = question.possibleResponses;
|
|
19
|
+
if (!Array.isArray(possibleResponses) || possibleResponses.length === 0)
|
|
20
|
+
return null;
|
|
21
|
+
const validIndex = possibleResponses.findIndex((r) => r.valid === true);
|
|
22
|
+
if (validIndex === -1)
|
|
23
|
+
return null;
|
|
24
|
+
const normalizedCandidate = String(candidateResponse ?? '').trim();
|
|
25
|
+
const validChoice = possibleResponses[validIndex];
|
|
26
|
+
const validText = (validChoice?.possibleResponse ?? '').trim();
|
|
27
|
+
const isCorrect = normalizedCandidate === validText ||
|
|
28
|
+
normalizedCandidate === String(validIndex);
|
|
29
|
+
return {
|
|
30
|
+
score: isCorrect ? question.maxScore : 0,
|
|
31
|
+
comment: ''
|
|
32
|
+
};
|
|
33
|
+
}
|
|
9
34
|
async function sendDiscordNotification(message) {
|
|
10
35
|
const discordWebhook = process.env.TEST_CORRECTION_DISCORD_WEBHOOKS;
|
|
11
36
|
if (discordWebhook) {
|
|
@@ -51,37 +76,46 @@ async function correctTest(options) {
|
|
|
51
76
|
continue;
|
|
52
77
|
}
|
|
53
78
|
maxScore += question.maxScore;
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
|
|
79
|
+
const mcqResult = correctMcqIfPossible(question, dbResponse.response ?? '');
|
|
80
|
+
let validScore;
|
|
81
|
+
let comment;
|
|
82
|
+
if (mcqResult !== null) {
|
|
83
|
+
validScore = mcqResult.score;
|
|
84
|
+
comment = mcqResult.comment;
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
const scoreResponse = await generateLiveMessage('correctQuestion', {
|
|
88
|
+
question: {
|
|
89
|
+
_id: question._id.toString(),
|
|
90
|
+
instruction: question.instruction,
|
|
91
|
+
possibleResponses: question.possibleResponses,
|
|
92
|
+
questionType: question.questionType,
|
|
93
|
+
maxScore: question.maxScore
|
|
94
|
+
},
|
|
95
|
+
result: {
|
|
96
|
+
responses: [{
|
|
97
|
+
questionId: dbResponse.questionId.toString(),
|
|
98
|
+
response: dbResponse.response
|
|
99
|
+
}]
|
|
100
|
+
}
|
|
101
|
+
}, true);
|
|
102
|
+
console.log('Correction result:', { scoreResponse });
|
|
103
|
+
const parsedResult = JSON.parse(scoreResponse);
|
|
104
|
+
validScore = 0;
|
|
105
|
+
if (parsedResult.score !== undefined && parsedResult.score !== null) {
|
|
106
|
+
const score = parseFloat(parsedResult.score.toString());
|
|
107
|
+
if (!isNaN(score) && isFinite(score) && score >= 0) {
|
|
108
|
+
validScore = score;
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
console.warn('Invalid score returned by AI:', parsedResult.score);
|
|
112
|
+
}
|
|
80
113
|
}
|
|
114
|
+
comment = parsedResult.comment || '';
|
|
81
115
|
}
|
|
82
116
|
finalscore += validScore;
|
|
83
117
|
dbResponse.score = validScore;
|
|
84
|
-
dbResponse.comment =
|
|
118
|
+
dbResponse.comment = comment;
|
|
85
119
|
}
|
|
86
120
|
// S'assurer que finalscore est un nombre valide
|
|
87
121
|
if (isNaN(finalscore) || !isFinite(finalscore)) {
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { enduranceListener, enduranceEmitter, enduranceEventTypes } from '@programisto/endurance';
|
|
2
|
+
import Test from '../models/test.model.js';
|
|
3
|
+
import TestResult from '../models/test-result.model.js';
|
|
4
|
+
import Candidate from '../models/candidate.model.js';
|
|
5
|
+
import ContactModel from '../models/contact.model.js';
|
|
6
|
+
const EVENT_INVITE_TO_TECHNICAL_TEST = 'INVITE_TO_TECHNICAL_TEST';
|
|
7
|
+
/**
|
|
8
|
+
* Même logique que POST /exams/invite : crée un TestResult et envoie l'email d'invitation.
|
|
9
|
+
* Déclenché quand une candidature est créée sur une offre avec un test lié (internal-portal).
|
|
10
|
+
*/
|
|
11
|
+
async function inviteCandidateToTest(payload) {
|
|
12
|
+
const candidateId = typeof payload.candidateId === 'object' && payload.candidateId?.toString
|
|
13
|
+
? payload.candidateId.toString()
|
|
14
|
+
: String(payload.candidateId ?? '');
|
|
15
|
+
const testId = typeof payload.testId === 'object' && payload.testId?.toString
|
|
16
|
+
? payload.testId.toString()
|
|
17
|
+
: String(payload.testId ?? '');
|
|
18
|
+
if (!candidateId || !testId)
|
|
19
|
+
return;
|
|
20
|
+
const existing = await TestResult.findOne({ candidateId, testId });
|
|
21
|
+
if (existing) {
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
const test = await Test.findById(testId);
|
|
25
|
+
if (!test) {
|
|
26
|
+
console.warn('[INVITE_TO_TECHNICAL_TEST] Test not found:', testId);
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
const categories = test.categories?.map((cat) => ({ categoryId: cat.categoryId })) ?? [];
|
|
30
|
+
const newResult = new TestResult({
|
|
31
|
+
candidateId,
|
|
32
|
+
testId,
|
|
33
|
+
categories,
|
|
34
|
+
state: 'pending',
|
|
35
|
+
invitationDate: Date.now()
|
|
36
|
+
});
|
|
37
|
+
await newResult.save();
|
|
38
|
+
const candidate = await Candidate.findById(candidateId);
|
|
39
|
+
if (!candidate) {
|
|
40
|
+
console.warn('[INVITE_TO_TECHNICAL_TEST] Candidate not found:', candidateId);
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
const contact = await ContactModel.findById(candidate.contact);
|
|
44
|
+
if (!contact) {
|
|
45
|
+
console.warn('[INVITE_TO_TECHNICAL_TEST] Contact not found for candidate:', candidateId);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
const email = contact.email;
|
|
49
|
+
if (!email) {
|
|
50
|
+
console.warn('[INVITE_TO_TECHNICAL_TEST] No email for contact');
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
const testLink = (process.env.TEST_INVITATION_LINK || '') + email;
|
|
54
|
+
const emailUser = process.env.EMAIL_USER;
|
|
55
|
+
const emailPassword = process.env.EMAIL_PASSWORD;
|
|
56
|
+
await enduranceEmitter.emit(enduranceEventTypes.SEND_EMAIL, {
|
|
57
|
+
template: 'test-invitation',
|
|
58
|
+
to: email,
|
|
59
|
+
from: emailUser,
|
|
60
|
+
emailUser,
|
|
61
|
+
emailPassword,
|
|
62
|
+
data: {
|
|
63
|
+
firstname: contact.firstname,
|
|
64
|
+
testName: test?.title || '',
|
|
65
|
+
testLink
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
enduranceListener.createListener(EVENT_INVITE_TO_TECHNICAL_TEST, async (args) => {
|
|
70
|
+
try {
|
|
71
|
+
if (typeof args === 'object' && args !== null) {
|
|
72
|
+
await inviteCandidateToTest(args);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
catch (err) {
|
|
76
|
+
console.error('[INVITE_TO_TECHNICAL_TEST] Error inviting candidate to test:', err);
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
export default enduranceListener;
|
|
@@ -6,7 +6,7 @@ import TestCategory from '../models/test-category.models.js';
|
|
|
6
6
|
import TestJob from '../models/test-job.model.js';
|
|
7
7
|
import Candidate from '../models/candidate.model.js';
|
|
8
8
|
import ContactModel from '../models/contact.model.js';
|
|
9
|
-
import { generateLiveMessage
|
|
9
|
+
import { generateLiveMessage } from '../lib/openai.js';
|
|
10
10
|
import { computeScoresByCategory } from '../lib/score-utils.js';
|
|
11
11
|
// Fonction utilitaire pour récupérer le nom du job
|
|
12
12
|
async function getJobName(targetJob) {
|
|
@@ -53,7 +53,7 @@ class ExamsRouter extends EnduranceRouter {
|
|
|
53
53
|
constructor() {
|
|
54
54
|
super(EnduranceAuthMiddleware.getInstance());
|
|
55
55
|
}
|
|
56
|
-
async generateAndSaveQuestion(test, categoryInfo,
|
|
56
|
+
async generateAndSaveQuestion(test, categoryInfo, _useAssistant = false, questionTypeOverride) {
|
|
57
57
|
try {
|
|
58
58
|
const categoryDoc = await TestCategory.findById(categoryInfo.categoryId);
|
|
59
59
|
if (!categoryDoc) {
|
|
@@ -74,13 +74,7 @@ class ExamsRouter extends EnduranceRouter {
|
|
|
74
74
|
expertiseLevel: categoryInfo.expertiseLevel,
|
|
75
75
|
otherQuestions: otherQuestions.map(question => question.instruction).join('\n')
|
|
76
76
|
};
|
|
77
|
-
|
|
78
|
-
if (useAssistant) {
|
|
79
|
-
generatedQuestion = await generateLiveMessageAssistant(process.env.OPENAI_ASSISTANT_ID_CREATE_QUESTION || '', 'createQuestion', questionParams, true);
|
|
80
|
-
}
|
|
81
|
-
else {
|
|
82
|
-
generatedQuestion = await generateLiveMessage('createQuestion', questionParams, true);
|
|
83
|
-
}
|
|
77
|
+
const generatedQuestion = await generateLiveMessage('createQuestion', questionParams, true);
|
|
84
78
|
if (generatedQuestion === 'Brain freezed, I cannot generate a live message right now.') {
|
|
85
79
|
console.error('Échec de génération de question pour la catégorie:', categoryDoc.name);
|
|
86
80
|
return null;
|
|
@@ -1252,7 +1246,7 @@ class ExamsRouter extends EnduranceRouter {
|
|
|
1252
1246
|
const otherQuestionsIds = test.questions.map(question => question.questionId);
|
|
1253
1247
|
const otherQuestions = await TestQuestion.find({ _id: { $in: otherQuestionsIds } });
|
|
1254
1248
|
const jobName = await getJobName(test.targetJob);
|
|
1255
|
-
const generatedQuestion = await
|
|
1249
|
+
const generatedQuestion = await generateLiveMessage('createQuestion', {
|
|
1256
1250
|
job: jobName,
|
|
1257
1251
|
seniority: test.seniorityLevel,
|
|
1258
1252
|
questionType,
|
|
@@ -1791,7 +1785,7 @@ class ExamsRouter extends EnduranceRouter {
|
|
|
1791
1785
|
const question = await TestQuestion.findById(response.questionId);
|
|
1792
1786
|
if (!question)
|
|
1793
1787
|
continue;
|
|
1794
|
-
const score = await
|
|
1788
|
+
const score = await generateLiveMessage('correctQuestion', {
|
|
1795
1789
|
question: {
|
|
1796
1790
|
_id: question._id.toString(),
|
|
1797
1791
|
instruction: question.instruction,
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@programisto/edrm-exams",
|
|
4
|
-
"version": "0.3.
|
|
4
|
+
"version": "0.3.14",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
7
7
|
},
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
"start": "node ./dist/bin/www",
|
|
12
12
|
"dev": "tsc-watch --onSuccess \"node ./dist/bin/www\"",
|
|
13
13
|
"test": "mocha",
|
|
14
|
+
"test:openai": "node -r dotenv/config node_modules/.bin/mocha",
|
|
14
15
|
"build": "tsc && npm run copy-files",
|
|
15
16
|
"copy-files": "copyfiles -u 1 'src/**/*.txt' dist",
|
|
16
17
|
"lint": "eslint \"**/*.{ts,tsx}\"",
|
|
@@ -48,6 +49,7 @@
|
|
|
48
49
|
"@typescript-eslint/parser": "^8.26.0",
|
|
49
50
|
"commitlint": "^19.8.1",
|
|
50
51
|
"copyfiles": "^2.4.1",
|
|
52
|
+
"dotenv": "^16.6.1",
|
|
51
53
|
"eslint": "^8.57.1",
|
|
52
54
|
"eslint-config-standard": "^17.1.0",
|
|
53
55
|
"eslint-plugin-import": "^2.31.0",
|