SPB Git forge

spb/immbot-ai

Public
1commits 1branches 0releases
1.5 MBsize
maindefault branch
20 days agolast push
TypeScript 98.3% CSS 0.9% Shell 0.7%
7.1 KB · 150 lines typescript
Raw Blame History
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