@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.
@@ -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,
@@ -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 generateLiveMessage('createQuestion', {
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 generateLiveMessage('correctQuestion', {
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
- // Mélanger les catégories pour une répartition aléatoire
823
- const shuffledCategories = [...categoriesToUse].sort(() => Math.random() - 0.5);
824
- for (const categoryInfo of shuffledCategories) {
825
- // Arrêter si on a déjà généré le nombre de questions demandé
826
- if (questionsGenerated >= numberOfQuestions)
827
- break;
828
- const categoryDoc = await TestCategory.findById(categoryInfo.categoryId);
829
- if (!categoryDoc)
830
- continue;
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
- catch (parseError) {
858
- console.error('Erreur lors du parsing de la question générée:', parseError);
859
- console.error('Réponse reçue:', generatedQuestion);
860
- 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++;
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,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@programisto/edrm-exams",
4
- "version": "0.2.12",
4
+ "version": "0.2.13",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },