// Prompts système modulaires : versionnés en BD (éditables par le professeur), // amorcés depuis les fichiers prompts/*.md. import { readFileSync, readdirSync, existsSync } from "node:fs"; import { join, resolve } from "node:path"; import { all, get, run } from "./db/index.ts"; export const PROMPT_DIR = () => resolve(process.cwd(), "prompts"); /** Version active d'un prompt (BD d'abord, fichier en secours). */ export function getPrompt(name: string): string { const row = get<{ content: string }>( "SELECT content FROM prompt_versions WHERE name = ? AND active = 1 ORDER BY version DESC LIMIT 1", name ); if (row) return row.content; const file = join(PROMPT_DIR(), `${name}.md`); if (existsSync(file)) return readFileSync(file, "utf8").trim(); return ""; } export function listPromptNames(): string[] { const fromDb = all<{ name: string }>("SELECT DISTINCT name FROM prompt_versions").map((r) => r.name); const fromFiles = existsSync(PROMPT_DIR()) ? readdirSync(PROMPT_DIR()) .filter((f) => f.endsWith(".md")) .map((f) => f.replace(/\.md$/, "")) : []; return [...new Set([...fromFiles, ...fromDb])].sort(); } export function savePromptVersion(name: string, content: string, createdBy: string): number { const last = get<{ v: number }>("SELECT MAX(version) as v FROM prompt_versions WHERE name = ?", name); const version = (last?.v ?? 0) + 1; run("UPDATE prompt_versions SET active = 0 WHERE name = ?", name); run( "INSERT INTO prompt_versions (name, content, version, active, created_by) VALUES (?, ?, ?, 1, ?)", name, content, version, createdBy ); return version; } export function promptHistory(name: string) { return all("SELECT id, version, active, created_by, created_at FROM prompt_versions WHERE name = ? ORDER BY version DESC", name); } /** Amorçage : charge chaque fichier prompts/*.md absent de la BD comme version 1. */ export function seedPromptsFromFiles() { if (!existsSync(PROMPT_DIR())) return; for (const f of readdirSync(PROMPT_DIR())) { if (!f.endsWith(".md")) continue; const name = f.replace(/\.md$/, ""); const existing = get("SELECT id FROM prompt_versions WHERE name = ?", name); if (!existing) { run( "INSERT INTO prompt_versions (name, content, version, active, created_by) VALUES (?, ?, 1, 1, 'seed')", name, readFileSync(join(PROMPT_DIR(), f), "utf8").trim() ); } } } // ---------- Assemblage du prompt système du chat ---------- export type PedagogicalMode = | "ask" | "tutor" | "socratic" | "simple" | "professional" | "correction" | "exam-prep" | "challenge" | "multimodal" | "targeted-review"; export type KnowledgeMode = "course-only" | "course-plus" | "course-tools" | "general"; export const MODE_PROMPT_FILE: Record = { ask: null, tutor: "tutor-mode", socratic: "socratic-mode", simple: "simple-mode", professional: "professional-mode", correction: "correction-mode", "exam-prep": "exam-preparation-mode", challenge: "challenge-mode", multimodal: null, // multimodal-policy est injecté dès qu'il y a des pièces jointes "targeted-review": "targeted-review-mode", }; export function assembleSystemPrompt(opts: { courseCode: string | null; mode: PedagogicalMode; knowledgeMode: KnowledgeMode; hasAttachments: boolean; hasContext: boolean; hasTools?: boolean; hasWebTools?: boolean; integrityActive: boolean; masteryProfile?: string; crossCourse?: boolean; }): string { const parts: string[] = [getPrompt("base-system")]; if (opts.courseCode === "IMM1003") parts.push(getPrompt("course-imm1003")); else if (opts.courseCode === "IMM1033") parts.push(getPrompt("course-imm1033")); if (opts.hasContext) { parts.push(getPrompt("rag-grounding"), getPrompt("citation-policy")); } if (opts.hasTools) { parts.push( "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." ); } if (opts.hasWebTools) { parts.push( "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." ); } if (opts.knowledgeMode === "course-tools") { parts.push( "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." ); } if (opts.knowledgeMode === "course-only" && opts.hasContext) { parts.push( "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." ); } else if (opts.knowledgeMode === "course-plus") { parts.push( "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." ); } else if (opts.knowledgeMode === "general") { parts.push( "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.*" ); } const modeFile = MODE_PROMPT_FILE[opts.mode]; if (modeFile) parts.push(getPrompt(modeFile)); if (opts.mode === "targeted-review" && opts.masteryProfile) { parts.push(`PROFIL DE MAÎTRISE DE L'ÉTUDIANT :\n${opts.masteryProfile}`); } if (opts.hasAttachments) parts.push(getPrompt("multimodal-policy")); if (opts.integrityActive) parts.push(getPrompt("integrity-policy")); if (opts.crossCourse) { parts.push( "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 »)." ); } return parts.filter(Boolean).join("\n\n---\n\n"); }