TypeScript 98.3%
CSS 0.9%
Shell 0.7%
1// Prompts système modulaires : versionnés en BD (éditables par le professeur),2// amorcés depuis les fichiers prompts/*.md.34import { readFileSync, readdirSync, existsSync } from "node:fs";5import { join, resolve } from "node:path";6import { all, get, run } from "./db/index.ts";78export const PROMPT_DIR = () => resolve(process.cwd(), "prompts");910/** Version active d'un prompt (BD d'abord, fichier en secours). */11export function getPrompt(name: string): string {12 const row = get<{ content: string }>(13 "SELECT content FROM prompt_versions WHERE name = ? AND active = 1 ORDER BY version DESC LIMIT 1",14 name15 );16 if (row) return row.content;17 const file = join(PROMPT_DIR(), `${name}.md`);18 if (existsSync(file)) return readFileSync(file, "utf8").trim();19 return "";20}2122export function listPromptNames(): string[] {23 const fromDb = all<{ name: string }>("SELECT DISTINCT name FROM prompt_versions").map((r) => r.name);24 const fromFiles = existsSync(PROMPT_DIR())25 ? readdirSync(PROMPT_DIR())26 .filter((f) => f.endsWith(".md"))27 .map((f) => f.replace(/\.md$/, ""))28 : [];29 return [...new Set([...fromFiles, ...fromDb])].sort();30}3132export function savePromptVersion(name: string, content: string, createdBy: string): number {33 const last = get<{ v: number }>("SELECT MAX(version) as v FROM prompt_versions WHERE name = ?", name);34 const version = (last?.v ?? 0) + 1;35 run("UPDATE prompt_versions SET active = 0 WHERE name = ?", name);36 run(37 "INSERT INTO prompt_versions (name, content, version, active, created_by) VALUES (?, ?, ?, 1, ?)",38 name, content, version, createdBy39 );40 return version;41}4243export function promptHistory(name: string) {44 return all("SELECT id, version, active, created_by, created_at FROM prompt_versions WHERE name = ? ORDER BY version DESC", name);45}4647/** Amorçage : charge chaque fichier prompts/*.md absent de la BD comme version 1. */48export function seedPromptsFromFiles() {49 if (!existsSync(PROMPT_DIR())) return;50 for (const f of readdirSync(PROMPT_DIR())) {51 if (!f.endsWith(".md")) continue;52 const name = f.replace(/\.md$/, "");53 const existing = get("SELECT id FROM prompt_versions WHERE name = ?", name);54 if (!existing) {55 run(56 "INSERT INTO prompt_versions (name, content, version, active, created_by) VALUES (?, ?, 1, 1, 'seed')",57 name, readFileSync(join(PROMPT_DIR(), f), "utf8").trim()58 );59 }60 }61}6263// ---------- Assemblage du prompt système du chat ----------6465export type PedagogicalMode =66 | "ask" | "tutor" | "socratic" | "simple" | "professional"67 | "correction" | "exam-prep" | "challenge" | "multimodal" | "targeted-review";6869export type KnowledgeMode = "course-only" | "course-plus" | "course-tools" | "general";7071export const MODE_PROMPT_FILE: Record<PedagogicalMode, string | null> = {72 ask: null,73 tutor: "tutor-mode",74 socratic: "socratic-mode",75 simple: "simple-mode",76 professional: "professional-mode",77 correction: "correction-mode",78 "exam-prep": "exam-preparation-mode",79 challenge: "challenge-mode",80 multimodal: null, // multimodal-policy est injecté dès qu'il y a des pièces jointes81 "targeted-review": "targeted-review-mode",82};8384export function assembleSystemPrompt(opts: {85 courseCode: string | null;86 mode: PedagogicalMode;87 knowledgeMode: KnowledgeMode;88 hasAttachments: boolean;89 hasContext: boolean;90 hasTools?: boolean;91 hasWebTools?: boolean;92 integrityActive: boolean;93 masteryProfile?: string;94 crossCourse?: boolean;95}): string {96 const parts: string[] = [getPrompt("base-system")];9798 if (opts.courseCode === "IMM1003") parts.push(getPrompt("course-imm1003"));99 else if (opts.courseCode === "IMM1033") parts.push(getPrompt("course-imm1033"));100101 if (opts.hasContext) {102 parts.push(getPrompt("rag-grounding"), getPrompt("citation-policy"));103 }104105 if (opts.hasTools) {106 parts.push(107 "OUTILS DE CONSULTATION DU COURS : tu disposes d'outils pour consulter DIRECTEMENT le matériel officiel — lister_seances, plan_seance (sections + titres des diapositives), lire_diapositives (contenu complet d'une plage), rechercher_cours (recherche plein texte). Utilise-les dès que tu as besoin de vérifier ou d'approfondir : repère d'abord (plan_seance ou rechercher_cours), puis lis les diapositives pertinentes. Tout contenu servi par un outil porte une balise [Sx] : cite-la exactement comme les extraits fournis. Ne réponds pas de mémoire quand un outil peut confirmer."108 );109 }110 if (opts.hasWebTools) {111 parts.push(112 "OUTILS WEB : tu disposes aussi de recherche_web (recherche avancée) et lire_page_web (lecture d'une page en Markdown). Utilise-les pour l'information ACTUELLE ou externe (taux, marché, réglementation, OEAQ, SCHL). Toute information issue du Web doit être clairement attribuée avec son URL en lien Markdown, dans une partie distincte du matériel de cours — jamais mélangée aux citations [Sx]. Privilégie les sources institutionnelles québécoises et mentionne la date de l'information."113 );114 }115 if (opts.knowledgeMode === "course-tools") {116 parts.push(117 "MODE COURS INTERACTIF : aucun extrait n'est fourni d'avance — EXPLORE le matériel avec tes outils avant de répondre (1 à 3 appels suffisent généralement). Fonde ta réponse exclusivement sur ce que les outils retournent, avec citations [Sx]. Si, après recherche, le matériel ne couvre pas la question, dis-le honnêtement."118 );119 }120 if (opts.knowledgeMode === "course-only" && opts.hasContext) {121 parts.push(122 "MODE COURS UNIQUEMENT : réponds STRICTEMENT à partir des extraits fournis. Si les extraits ne permettent pas de répondre de façon suffisamment appuyée, réponds exactement : « Je ne trouve pas une réponse suffisamment appuyée dans le matériel officiel du cours. » puis suggère où chercher (séance probable) ou comment reformuler."123 );124 } else if (opts.knowledgeMode === "course-plus") {125 parts.push(126 "MODE COURS + CONNAISSANCES GÉNÉRALES : structure ta réponse en deux parties clairement titrées — « **Selon le matériel du cours** » (avec citations [Sx]) puis « **Complément général** » (tes connaissances, sans citation, en signalant que cela dépasse le matériel officiel). Si une partie est vide, omets-la et dis-le."127 );128 } else if (opts.knowledgeMode === "general") {129 parts.push(130 "MODE GÉNÉRAL : tu réponds sans t'appuyer sur le matériel officiel. Commence ta réponse par la mention en italique : *Cette réponse n'est pas nécessairement fondée sur le matériel officiel du cours.*"131 );132 }133134 const modeFile = MODE_PROMPT_FILE[opts.mode];135 if (modeFile) parts.push(getPrompt(modeFile));136 if (opts.mode === "targeted-review" && opts.masteryProfile) {137 parts.push(`PROFIL DE MAÎTRISE DE L'ÉTUDIANT :\n${opts.masteryProfile}`);138 }139140 if (opts.hasAttachments) parts.push(getPrompt("multimodal-policy"));141 if (opts.integrityActive) parts.push(getPrompt("integrity-policy"));142 if (opts.crossCourse) {143 parts.push(144 "RECHERCHE CROISÉE ACTIVE : des extraits des DEUX cours peuvent apparaître. Signale explicitement lorsqu'une notion provient du cours voisin (ex. « ce point est détaillé dans IMM1033 »)."145 );146 }147148 return parts.filter(Boolean).join("\n\n---\n\n");149}150