@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.
@@ -1,10 +1,6 @@
1
- Tu est un correcteur de question de concours.
2
- Tu doit donner un score à cette question : ${instruction} dont la réponse donnée par le candidat est : ${response}.
3
- Il s'agissait d'une question de type ${questionType} (et si la question est de type MCQ alors les réponses possibles étaient les suivantes : ${possibleResponses})
4
- le score maximum pour cette question est ${maxScore}
5
- Tu doit donner un score entre 0 et ${maxScore} et un commentaire de correction.
6
- Tu dois répondre au format JSON : la reponse envoyer doit correspondre exactement à ce model json :
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
- Tu es un assistant qui crée des questions pour un test.
2
- créer une question en fonction de du métier : ${job}, du niveau de seniorité : ${seniority}, de la categorie de question : ${category} et le niveau d'expertise de cette categorie : ${expertiseLevel} (allant de 1 à 10, de 1 à 3 = 'beginner', de 3 à 6 = 'intermediate', de 6 à 10 = 'advanced').
3
- - la question doit être de type : ${questionType}.
4
- - la question doit être dans la langue suivante : Français
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
- Pour ne pas faire de doublon, voici la liste des questions déjà présentes dans le test : ${otherQuestions}
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 { generateLiveMessage } from '../lib/openai.js';
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 generateLiveMessage('correctQuestion', {
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: testLink
83
+ testLink
84
84
  }
85
85
  });
86
86
  }
@@ -1,6 +1,7 @@
1
1
  import { EnduranceRouter } from '@programisto/endurance-core';
2
2
  declare class ExamsRouter extends EnduranceRouter {
3
3
  constructor();
4
+ private generateAndSaveQuestion;
4
5
  setupRoutes(): void;
5
6
  }
6
7
  declare const router: ExamsRouter;
@@ -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
- test.questions = test.questions.filter(id => id.toString() !== questionId);
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 generateLiveMessage('createQuestion', {
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 generateLiveMessage('correctQuestion', {
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
- // Mélanger les catégories pour une répartition aléatoire
818
- const shuffledCategories = [...categoriesToUse].sort(() => Math.random() - 0.5);
819
- for (const categoryInfo of shuffledCategories) {
820
- // Arrêter si on a déjà généré le nombre de questions demandé
821
- if (questionsGenerated >= numberOfQuestions)
822
- break;
823
- const categoryDoc = await TestCategory.findById(categoryInfo.categoryId);
824
- if (!categoryDoc)
825
- continue;
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
- catch (parseError) {
853
- console.error('Erreur lors du parsing de la question générée:', parseError);
854
- console.error('Réponse reçue:', generatedQuestion);
855
- continue; // Passer à la question suivante
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,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@programisto/edrm-exams",
4
- "version": "0.2.11",
4
+ "version": "0.2.13",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },