@programisto/edrm-exams 0.2.12 → 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 +67 -40
- 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,
|
|
@@ -437,7 +478,7 @@ class ExamsRouter extends EnduranceRouter {
|
|
|
437
478
|
}
|
|
438
479
|
const otherQuestionsIds = test.questions.map(question => question.questionId);
|
|
439
480
|
const otherQuestions = await TestQuestion.find({ _id: { $in: otherQuestionsIds } });
|
|
440
|
-
const generatedQuestion = await
|
|
481
|
+
const generatedQuestion = await generateLiveMessageAssistant(process.env.OPENAI_ASSISTANT_ID_CREATE_QUESTION || '', 'createQuestion', {
|
|
441
482
|
job: test.targetJob,
|
|
442
483
|
seniority: test.seniorityLevel,
|
|
443
484
|
questionType,
|
|
@@ -715,7 +756,7 @@ class ExamsRouter extends EnduranceRouter {
|
|
|
715
756
|
const question = await TestQuestion.findById(response.questionId);
|
|
716
757
|
if (!question)
|
|
717
758
|
continue;
|
|
718
|
-
const score = await
|
|
759
|
+
const score = await generateLiveMessageAssistant(process.env.OPENAI_ASSISTANT_ID_CORRECT_QUESTION || '', 'correctQuestion', {
|
|
719
760
|
question: {
|
|
720
761
|
_id: question._id.toString(),
|
|
721
762
|
instruction: question.instruction,
|
|
@@ -819,45 +860,32 @@ class ExamsRouter extends EnduranceRouter {
|
|
|
819
860
|
}
|
|
820
861
|
const generatedQuestions = [];
|
|
821
862
|
let questionsGenerated = 0;
|
|
822
|
-
|
|
823
|
-
const
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
const otherQuestionsIds = test.questions.map(question => question.questionId);
|
|
832
|
-
const otherQuestions = await TestQuestion.find({ _id: { $in: otherQuestionsIds } });
|
|
833
|
-
// Calculer combien de questions générer pour cette catégorie
|
|
834
|
-
const remainingQuestions = numberOfQuestions - questionsGenerated;
|
|
835
|
-
const questionsForThisCategory = Math.min(remainingQuestions, Math.ceil(numberOfQuestions / categoriesToUse.length));
|
|
836
|
-
for (let i = 0; i < questionsForThisCategory; i++) {
|
|
837
|
-
const generatedQuestion = await generateLiveMessage('createQuestion', {
|
|
838
|
-
job: test.targetJob,
|
|
839
|
-
seniority: test.seniorityLevel,
|
|
840
|
-
category: categoryDoc.name,
|
|
841
|
-
questionType: ['MCQ', 'free question', 'exercice'][Math.floor(Math.random() * 3)],
|
|
842
|
-
expertiseLevel: categoryInfo.expertiseLevel,
|
|
843
|
-
otherQuestions: otherQuestions.map(question => question.instruction).join('\n')
|
|
844
|
-
}, true);
|
|
845
|
-
// Vérifier si la réponse est un JSON valide
|
|
846
|
-
if (generatedQuestion === 'Brain freezed, I cannot generate a live message right now.') {
|
|
847
|
-
console.error('Échec de génération de question pour la catégorie:', categoryDoc.name);
|
|
848
|
-
continue; // Passer à la question suivante
|
|
849
|
-
}
|
|
850
|
-
try {
|
|
851
|
-
const question = new TestQuestion(JSON.parse(generatedQuestion));
|
|
852
|
-
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) {
|
|
853
872
|
generatedQuestions.push(question);
|
|
854
|
-
test.questions.push({ questionId: question._id, order: test.questions.length });
|
|
855
873
|
questionsGenerated++;
|
|
856
874
|
}
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
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++;
|
|
861
889
|
}
|
|
862
890
|
}
|
|
863
891
|
}
|
|
@@ -867,7 +895,6 @@ class ExamsRouter extends EnduranceRouter {
|
|
|
867
895
|
message: 'Aucune question n\'a pu être générée. Veuillez réessayer plus tard.'
|
|
868
896
|
});
|
|
869
897
|
}
|
|
870
|
-
await test.save();
|
|
871
898
|
res.status(200).json({
|
|
872
899
|
message: `${generatedQuestions.length} question(s) générée(s) avec succès`,
|
|
873
900
|
questions: generatedQuestions,
|