@programisto/edrm-exams 0.2.11 → 0.2.13
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 +6 -10
- package/dist/modules/edrm-exams/lib/openai/createQuestion.txt +5 -67
- package/dist/modules/edrm-exams/lib/openai.d.ts +1 -0
- package/dist/modules/edrm-exams/lib/openai.js +53 -0
- package/dist/modules/edrm-exams/listeners/correct.listener.js +3 -3
- package/dist/modules/edrm-exams/routes/exams.router.d.ts +1 -0
- package/dist/modules/edrm-exams/routes/exams.router.js +73 -41
- package/package.json +1 -1
|
@@ -1,10 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
{
|
|
8
|
-
"score": score,
|
|
9
|
-
"comment": commentaire
|
|
10
|
-
}
|
|
1
|
+
Question : ${instruction}
|
|
2
|
+
Type de question : ${questionType}
|
|
3
|
+
- si MCQ les réponses possibles étaient : ${possibleResponses}
|
|
4
|
+
|
|
5
|
+
Réponse du candidat : ${response}
|
|
6
|
+
Score à donner : De 0 à ${maxScore} avec un commentaire de correction
|
|
@@ -1,68 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
- voici le model pour une question :
|
|
6
|
-
const questionModel = new mongoose.Schema(
|
|
7
|
-
{
|
|
8
|
-
instruction:
|
|
9
|
-
{
|
|
10
|
-
type: String,
|
|
11
|
-
required: true
|
|
12
|
-
},
|
|
13
|
-
questionType:
|
|
14
|
-
{
|
|
15
|
-
type: String,
|
|
16
|
-
enum:
|
|
17
|
-
["MCQ", "free question", "exercice"],
|
|
18
|
-
required: true,
|
|
19
|
-
},
|
|
20
|
-
maxScore:
|
|
21
|
-
{
|
|
22
|
-
type: Number,
|
|
23
|
-
required: true,
|
|
24
|
-
},
|
|
25
|
-
possibleResponses: // only on MCQ (multiple choice responses)
|
|
26
|
-
{
|
|
27
|
-
type:
|
|
28
|
-
[{
|
|
29
|
-
possibleResponse:
|
|
30
|
-
{
|
|
31
|
-
type: String,
|
|
32
|
-
required: true,
|
|
33
|
-
},
|
|
34
|
-
valid:
|
|
35
|
-
{
|
|
36
|
-
type: Boolean,
|
|
37
|
-
required: true
|
|
38
|
-
}
|
|
39
|
-
}],
|
|
40
|
-
},
|
|
41
|
-
time: // in secondes
|
|
42
|
-
{
|
|
43
|
-
type: Number,
|
|
44
|
-
required: true,
|
|
45
|
-
},
|
|
46
|
-
textType: //type d'input demandé à l'utilisateur
|
|
47
|
-
{
|
|
48
|
-
type: String,
|
|
49
|
-
enum:
|
|
50
|
-
["text", "code"],
|
|
51
|
-
required: false
|
|
52
|
-
}
|
|
53
|
-
});
|
|
54
|
-
- toutes les instruction et les réponses doivent toujours être compatible avec LaTeX pour être afficher dans des balises <MathJax>
|
|
55
|
-
voici un exemple : Voici une formule mathématique : \\( \\frac{a}{b} = c \\)
|
|
56
|
-
- voici des containtes pour chaque type de question :
|
|
57
|
-
- MCQ
|
|
58
|
-
(il ne peut y avoir qu'une seule réponse juste sur quatre au total) le temps pour ces questions vont de 30 secondes à 1 minute
|
|
59
|
-
- free question ( ne rien mettre dans possibleResponses)
|
|
60
|
-
le temps pour ces questions vont de 30 secondes à 5 minutes en fonction de la complexité de la réponse attendue (à toi de juger)
|
|
61
|
-
- exercice ( ne rien mettre dans possibleResponses )
|
|
62
|
-
le temps pour ces questions vont de 3 à 20 minutes en fonction de la complexité de la réponse attendue (à toi de juger)
|
|
63
|
-
- les instruction et la difficulté doivent être en rapport avec targetJob et seniorityLevel de ${test}
|
|
64
|
-
- le temps pour répondre à la question dans time doit laisser le temps aux utilisateur d'y répondre, sans non plus lui laisser trop de temps pour qu'il triche (surtout sur les QCM).
|
|
65
|
-
- le format de la réponses doit être en json avec seulement les elements présent dans le model ci dessus et pas besoin d'ajouter d"_id".
|
|
66
|
-
- si la réponse attendue à une question de type free question ou exercice est un bout de code, alors remplit textType en "code", sinon laisse "text"
|
|
1
|
+
Métier ciblé : ${job}
|
|
2
|
+
Catégorie de la question : ${category} niveau ${expertiseLevel}
|
|
3
|
+
Type de question : ${questionType}
|
|
4
|
+
Format : json
|
|
67
5
|
|
|
68
|
-
|
|
6
|
+
Voici la liste des questions déjà présentes dans le test (pour éviter les doublons) : ${otherQuestions}
|
|
@@ -33,4 +33,5 @@ interface ContextBuilder {
|
|
|
33
33
|
}>;
|
|
34
34
|
}
|
|
35
35
|
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>;
|
|
36
37
|
export {};
|
|
@@ -74,6 +74,59 @@ export async function generateLiveMessage(messageType, params, json) {
|
|
|
74
74
|
}
|
|
75
75
|
return 'Brain freezed, I cannot generate a live message right now.';
|
|
76
76
|
}
|
|
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
|
+
}
|
|
77
130
|
function removeQuotes(str) {
|
|
78
131
|
if (str.startsWith('"') && str.endsWith('"')) {
|
|
79
132
|
return str.substring(1, str.length - 1);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { enduranceListener, enduranceEventTypes, enduranceEmitter } from '@programisto/endurance-core';
|
|
2
2
|
import TestQuestion from '../models/test-question.model.js';
|
|
3
|
-
import {
|
|
3
|
+
import { generateLiveMessageAssistant } from '../lib/openai.js';
|
|
4
4
|
import TestResult, { TestState } from '../models/test-result.model.js';
|
|
5
5
|
import CandidateModel from '../models/candidate.model.js';
|
|
6
6
|
import ContactModel from '../models/contact.model.js';
|
|
@@ -31,7 +31,7 @@ async function correctTest(options) {
|
|
|
31
31
|
continue;
|
|
32
32
|
}
|
|
33
33
|
maxScore += question.maxScore;
|
|
34
|
-
const scoreResponse = await
|
|
34
|
+
const scoreResponse = await generateLiveMessageAssistant(process.env.OPENAI_ASSISTANT_ID_CORRECT_QUESTION || '', 'correctQuestion', {
|
|
35
35
|
question: {
|
|
36
36
|
_id: question._id.toString(),
|
|
37
37
|
instruction: question.instruction,
|
|
@@ -80,7 +80,7 @@ async function correctTest(options) {
|
|
|
80
80
|
lastname: contact.lastname,
|
|
81
81
|
score: result.score,
|
|
82
82
|
testName: test?.title || '',
|
|
83
|
-
testLink
|
|
83
|
+
testLink
|
|
84
84
|
}
|
|
85
85
|
});
|
|
86
86
|
}
|
|
@@ -5,11 +5,52 @@ import TestResult from '../models/test-result.model.js';
|
|
|
5
5
|
import TestCategory from '../models/test-category.models.js';
|
|
6
6
|
import Candidate from '../models/candidate.model.js';
|
|
7
7
|
import ContactModel from '../models/contact.model.js';
|
|
8
|
-
import { generateLiveMessage } from '../lib/openai.js';
|
|
8
|
+
import { generateLiveMessage, generateLiveMessageAssistant } from '../lib/openai.js';
|
|
9
9
|
class ExamsRouter extends EnduranceRouter {
|
|
10
10
|
constructor() {
|
|
11
11
|
super(EnduranceAuthMiddleware.getInstance());
|
|
12
12
|
}
|
|
13
|
+
async generateAndSaveQuestion(test, categoryInfo, useAssistant = false) {
|
|
14
|
+
try {
|
|
15
|
+
const categoryDoc = await TestCategory.findById(categoryInfo.categoryId);
|
|
16
|
+
if (!categoryDoc) {
|
|
17
|
+
console.error('Catégorie non trouvée:', categoryInfo.categoryId);
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
// Récupérer les questions existantes pour éviter les doublons
|
|
21
|
+
const otherQuestionsIds = test.questions.map(question => question.questionId);
|
|
22
|
+
const otherQuestions = await TestQuestion.find({ _id: { $in: otherQuestionsIds } });
|
|
23
|
+
const questionParams = {
|
|
24
|
+
job: test.targetJob,
|
|
25
|
+
seniority: test.seniorityLevel,
|
|
26
|
+
category: categoryDoc.name,
|
|
27
|
+
questionType: ['MCQ', 'free question', 'exercice'][Math.floor(Math.random() * 3)],
|
|
28
|
+
expertiseLevel: categoryInfo.expertiseLevel,
|
|
29
|
+
otherQuestions: otherQuestions.map(question => question.instruction).join('\n')
|
|
30
|
+
};
|
|
31
|
+
let generatedQuestion;
|
|
32
|
+
if (useAssistant) {
|
|
33
|
+
generatedQuestion = await generateLiveMessageAssistant(process.env.OPENAI_ASSISTANT_ID_CREATE_QUESTION || '', 'createQuestion', questionParams, true);
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
generatedQuestion = await generateLiveMessage('createQuestion', questionParams, true);
|
|
37
|
+
}
|
|
38
|
+
if (generatedQuestion === 'Brain freezed, I cannot generate a live message right now.') {
|
|
39
|
+
console.error('Échec de génération de question pour la catégorie:', categoryDoc.name);
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
const question = new TestQuestion(JSON.parse(generatedQuestion));
|
|
43
|
+
await question.save();
|
|
44
|
+
// Ajouter la question au test et sauvegarder immédiatement
|
|
45
|
+
test.questions.push({ questionId: question._id, order: test.questions.length });
|
|
46
|
+
await test.save();
|
|
47
|
+
return question;
|
|
48
|
+
}
|
|
49
|
+
catch (error) {
|
|
50
|
+
console.error('Erreur lors de la génération/sauvegarde de la question:', error);
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
13
54
|
setupRoutes() {
|
|
14
55
|
const authenticatedOptions = {
|
|
15
56
|
requireAuth: false,
|
|
@@ -346,7 +387,12 @@ class ExamsRouter extends EnduranceRouter {
|
|
|
346
387
|
if (!test) {
|
|
347
388
|
return res.status(404).json({ message: 'no test founded with this id' });
|
|
348
389
|
}
|
|
349
|
-
|
|
390
|
+
// Supprimer la question du tableau questions en filtrant par questionId
|
|
391
|
+
test.questions = test.questions.filter(q => q.questionId.toString() !== questionId);
|
|
392
|
+
// Recalculer les ordres pour que ça se suive
|
|
393
|
+
test.questions.forEach((q, index) => {
|
|
394
|
+
q.order = index + 1;
|
|
395
|
+
});
|
|
350
396
|
await test.save();
|
|
351
397
|
res.status(200).json({ message: 'question deleted with sucess' });
|
|
352
398
|
});
|
|
@@ -432,7 +478,7 @@ class ExamsRouter extends EnduranceRouter {
|
|
|
432
478
|
}
|
|
433
479
|
const otherQuestionsIds = test.questions.map(question => question.questionId);
|
|
434
480
|
const otherQuestions = await TestQuestion.find({ _id: { $in: otherQuestionsIds } });
|
|
435
|
-
const generatedQuestion = await
|
|
481
|
+
const generatedQuestion = await generateLiveMessageAssistant(process.env.OPENAI_ASSISTANT_ID_CREATE_QUESTION || '', 'createQuestion', {
|
|
436
482
|
job: test.targetJob,
|
|
437
483
|
seniority: test.seniorityLevel,
|
|
438
484
|
questionType,
|
|
@@ -710,7 +756,7 @@ class ExamsRouter extends EnduranceRouter {
|
|
|
710
756
|
const question = await TestQuestion.findById(response.questionId);
|
|
711
757
|
if (!question)
|
|
712
758
|
continue;
|
|
713
|
-
const score = await
|
|
759
|
+
const score = await generateLiveMessageAssistant(process.env.OPENAI_ASSISTANT_ID_CORRECT_QUESTION || '', 'correctQuestion', {
|
|
714
760
|
question: {
|
|
715
761
|
_id: question._id.toString(),
|
|
716
762
|
instruction: question.instruction,
|
|
@@ -814,45 +860,32 @@ class ExamsRouter extends EnduranceRouter {
|
|
|
814
860
|
}
|
|
815
861
|
const generatedQuestions = [];
|
|
816
862
|
let questionsGenerated = 0;
|
|
817
|
-
|
|
818
|
-
const
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
const otherQuestionsIds = test.questions.map(question => question.questionId);
|
|
827
|
-
const otherQuestions = await TestQuestion.find({ _id: { $in: otherQuestionsIds } });
|
|
828
|
-
// Calculer combien de questions générer pour cette catégorie
|
|
829
|
-
const remainingQuestions = numberOfQuestions - questionsGenerated;
|
|
830
|
-
const questionsForThisCategory = Math.min(remainingQuestions, 1); // Au maximum 1 question par catégorie
|
|
831
|
-
for (let i = 0; i < questionsForThisCategory; i++) {
|
|
832
|
-
const generatedQuestion = await generateLiveMessage('createQuestion', {
|
|
833
|
-
job: test.targetJob,
|
|
834
|
-
seniority: test.seniorityLevel,
|
|
835
|
-
category: categoryDoc.name,
|
|
836
|
-
questionType: ['MCQ', 'free question', 'exercice'][Math.floor(Math.random() * 3)],
|
|
837
|
-
expertiseLevel: categoryInfo.expertiseLevel,
|
|
838
|
-
otherQuestions: otherQuestions.map(question => question.instruction).join('\n')
|
|
839
|
-
}, true);
|
|
840
|
-
// Vérifier si la réponse est un JSON valide
|
|
841
|
-
if (generatedQuestion === 'Brain freezed, I cannot generate a live message right now.') {
|
|
842
|
-
console.error('Échec de génération de question pour la catégorie:', categoryDoc.name);
|
|
843
|
-
continue; // Passer à la question suivante
|
|
844
|
-
}
|
|
845
|
-
try {
|
|
846
|
-
const question = new TestQuestion(JSON.parse(generatedQuestion));
|
|
847
|
-
await question.save();
|
|
863
|
+
let attempts = 0;
|
|
864
|
+
const maxAttempts = numberOfQuestions * 3; // Limite pour éviter les boucles infinies
|
|
865
|
+
// Si on spécifie une catégorie, on génère toutes les questions pour cette catégorie
|
|
866
|
+
if (category && category !== 'ALL') {
|
|
867
|
+
const categoryInfo = categoriesToUse[0];
|
|
868
|
+
while (questionsGenerated < numberOfQuestions && attempts < maxAttempts) {
|
|
869
|
+
attempts++;
|
|
870
|
+
const question = await this.generateAndSaveQuestion(test, categoryInfo, true);
|
|
871
|
+
if (question) {
|
|
848
872
|
generatedQuestions.push(question);
|
|
849
|
-
test.questions.push({ questionId: question._id, order: test.questions.length });
|
|
850
873
|
questionsGenerated++;
|
|
851
874
|
}
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
else {
|
|
878
|
+
// Pour ALL, répartition aléatoire sur toutes les catégories
|
|
879
|
+
const shuffledCategories = [...categoriesToUse].sort(() => Math.random() - 0.5);
|
|
880
|
+
while (questionsGenerated < numberOfQuestions && attempts < maxAttempts) {
|
|
881
|
+
attempts++;
|
|
882
|
+
// Sélectionner une catégorie aléatoire
|
|
883
|
+
const randomCategoryIndex = Math.floor(Math.random() * shuffledCategories.length);
|
|
884
|
+
const categoryInfo = shuffledCategories[randomCategoryIndex];
|
|
885
|
+
const question = await this.generateAndSaveQuestion(test, categoryInfo, true);
|
|
886
|
+
if (question) {
|
|
887
|
+
generatedQuestions.push(question);
|
|
888
|
+
questionsGenerated++;
|
|
856
889
|
}
|
|
857
890
|
}
|
|
858
891
|
}
|
|
@@ -862,7 +895,6 @@ class ExamsRouter extends EnduranceRouter {
|
|
|
862
895
|
message: 'Aucune question n\'a pu être générée. Veuillez réessayer plus tard.'
|
|
863
896
|
});
|
|
864
897
|
}
|
|
865
|
-
await test.save();
|
|
866
898
|
res.status(200).json({
|
|
867
899
|
message: `${generatedQuestions.length} question(s) générée(s) avec succès`,
|
|
868
900
|
questions: generatedQuestions,
|