diff --git a/backend/app/agent_config.py b/backend/app/agent_config.py index 522ee9e..6fe4410 100644 --- a/backend/app/agent_config.py +++ b/backend/app/agent_config.py @@ -64,6 +64,7 @@ PROFILE_FIELDS = { "self_review", "rag_enabled", "embed_model", + "skills_enabled", "keep_alive", *GENERATION_FIELDS, } @@ -107,6 +108,8 @@ DEFAULT_CONFIG: dict = { # Mémoire long-terme (RAG) entre sessions, via un modèle d'embedding. "rag_enabled": True, "embed_model": "auto", + # Skills : méthodes expertes injectées automatiquement selon la tâche. + "skills_enabled": True, # Durée de maintien du modèle en VRAM (préchargement). "0" = décharge # aussitôt, "-1" = jamais, "30m" = 30 minutes. "keep_alive": "30m", diff --git a/backend/app/routes/chat.py b/backend/app/routes/chat.py index 90bc329..2717c14 100644 --- a/backend/app/routes/chat.py +++ b/backend/app/routes/chat.py @@ -18,7 +18,8 @@ from fastapi import APIRouter, HTTPException from fastapi.responses import StreamingResponse from pydantic import BaseModel -from .. import agent_config, coder, db, enhance, memory, rag, router as msg_router +from .. import agent_config, coder, db, enhance, memory, rag, skills +from .. import router as msg_router from ..tools import check_html, _safe_path from ..agent import run_agent from ..config import settings @@ -237,6 +238,18 @@ async def chat(req: ChatRequest) -> StreamingResponse: + "\n---\n".join(memories), }) + # Skill : méthode experte injectée pour ce tour (jamais persistée). + if cfg.get("skills_enabled", True): + skill = skills.pick_skill(req.content) + if skill: + convo.insert(1, { + "role": "system", + "content": ( + "Méthode à suivre pour cette tâche :\n" + skill["body"] + ), + }) + yield _sse("notice", {"message": f"📘 Méthode : {skill['title']}"}) + if plan: yield _sse("plan", {"steps": plan}) diff --git a/backend/app/skills.py b/backend/app/skills.py new file mode 100644 index 0000000..c9887e9 --- /dev/null +++ b/backend/app/skills.py @@ -0,0 +1,61 @@ +"""Skills : méthodes expertes injectées automatiquement selon la tâche. + +Sélection lexicale instantanée (aucun appel LLM) : au plus UNE skill par +message, injectée en message système pour ce tour uniquement. +""" +from __future__ import annotations + +import os +import re + +_SKILLS_DIR = os.path.join(os.path.dirname(__file__), "..", "skills") + +# Seuil : nombre minimal de mots-clés distincts trouvés dans le message. +_MIN_HITS = 2 + + +def _load() -> dict[str, dict]: + out: dict[str, dict] = {} + if not os.path.isdir(_SKILLS_DIR): + return out + for fname in sorted(os.listdir(_SKILLS_DIR)): + if not fname.endswith(".md"): + continue + with open(os.path.join(_SKILLS_DIR, fname), encoding="utf-8") as f: + raw = f.read() + m = re.match(r"^---\n(.*?)\n---\n(.*)$", raw, re.S) + if not m: + continue + meta: dict[str, str] = {} + for line in m.group(1).splitlines(): + if ":" in line: + key, _, value = line.partition(":") + meta[key.strip()] = value.strip() + keywords = [ + k.strip().lower() + for k in meta.get("keywords", "").split(",") + if k.strip() + ] + out[meta.get("name", fname[:-3])] = { + "name": meta.get("name", fname[:-3]), + "title": meta.get("title", fname[:-3]), + "keywords": keywords, + "body": m.group(2).strip(), + } + return out + + +ALL_SKILLS: dict[str, dict] = _load() + + +def pick_skill(message: str) -> dict | None: + """Meilleure skill pour ce message, ou None si rien d'assez net.""" + low = message.lower() + best, best_hits = None, 0 + for skill in ALL_SKILLS.values(): + hits = sum(1 for kw in skill["keywords"] if kw in low) + if hits > best_hits: + best, best_hits = skill, hits + if best is None or best_hits < _MIN_HITS: + return None + return {"name": best["name"], "title": best["title"], "body": best["body"]} diff --git a/backend/skills/analyse-donnees.md b/backend/skills/analyse-donnees.md new file mode 100644 index 0000000..08e1808 --- /dev/null +++ b/backend/skills/analyse-donnees.md @@ -0,0 +1,11 @@ +--- +name: analyse-donnees +title: Analyse de données +keywords: csv, json, données, tableau, statistique, moyenne, analyse, colonnes, tri, filtre, graphique, export +--- +Méthode d'analyse de données : +1. EXAMINER : lis un échantillon du fichier réel (read_file) AVANT tout traitement. Identifie séparateur, encodage, en-têtes, types de colonnes. +2. VALIDER : repère valeurs manquantes, doublons, incohérences de type. Signale-les explicitement au lieu de les masquer. +3. TRANSFORMER : script clair et borné (pas de dépendance exotique) ; garde les données d'origine intactes, écris le résultat dans un nouveau fichier. +4. PRÉSENTER : résumé chiffré (compte, min/max, moyennes pertinentes) + limites de l'analyse (données ignorées, hypothèses faites). +Interdit : supposer le format sans avoir lu le fichier ; modifier les données sources ; présenter des chiffres sans dire comment ils sont calculés. diff --git a/backend/skills/creation-web.md b/backend/skills/creation-web.md new file mode 100644 index 0000000..fb7f24f --- /dev/null +++ b/backend/skills/creation-web.md @@ -0,0 +1,11 @@ +--- +name: creation-web +title: Création web +keywords: site, page, html, css, landing, formulaire, portfolio, interface, responsive, boutique, vitrine, menu, header, footer, animation +--- +Méthode de création web, dans cet ordre : +1. STRUCTURE : écris d'abord le HTML sémantique COMPLET (header, main, sections, footer). Contenu réel, pas de lorem ipsum si le sujet est connu. +2. STYLE : CSS cohérent — palette limitée (3-4 couleurs), typographie lisible, espacements réguliers. Mobile-first, responsive (flexbox/grid, max-width sur images). +3. INTERACTIVITÉ : JavaScript minimal et sans dépendance externe. Chaque interaction doit fonctionner hors ligne. +4. VÉRIFIER : run_check sur chaque fichier produit ; contrôle les liens internes. Si les outils navigateur (mcp_playwright_*) sont disponibles, ouvre la page et lis la console pour vérifier qu'elle est propre. +Interdit : livrer sans vérification ; référencer des images ou CDN externes non demandés ; produire un fichier tronqué (utilise write_file en plusieurs morceaux). diff --git a/backend/skills/debogage-systematique.md b/backend/skills/debogage-systematique.md new file mode 100644 index 0000000..ef9fb14 --- /dev/null +++ b/backend/skills/debogage-systematique.md @@ -0,0 +1,12 @@ +--- +name: debogage-systematique +title: Débogage systématique +keywords: bug, plante, erreur, exception, traceback, crash, échoue, marche pas, fonctionne pas, cassé, debug, débogue, corrige le bug, ne s'affiche pas, undefined, null, NaN +--- +Méthode de débogage à suivre STRICTEMENT, étape par étape : +1. REPRODUIRE : identifie l'entrée exacte et le comportement observé vs attendu. Si le message d'erreur est fourni, cite-le et pars de là. +2. LOCALISER : lis le code concerné (read_file / grep_search) AVANT toute modification. Trouve la ligne qui produit le symptôme. +3. HYPOTHÈSE : formule UNE cause précise. Vérifie-la en lisant le code, pas en devinant. +4. CORRIGER : modification minimale et ciblée (edit_file). Ne réécris pas tout le fichier. Ne corrige qu'une cause à la fois. +5. VÉRIFIER : relis le code modifié (run_check) et explique pourquoi le symptôme disparaît. Signale tout autre problème repéré sans le corriger. +Interdit : proposer une correction sans avoir lu le code ; corriger plusieurs choses à la fois ; conclure « ça devrait marcher » sans vérification. diff --git a/backend/skills/redaction-structuree.md b/backend/skills/redaction-structuree.md new file mode 100644 index 0000000..52bbed1 --- /dev/null +++ b/backend/skills/redaction-structuree.md @@ -0,0 +1,10 @@ +--- +name: redaction-structuree +title: Rédaction structurée +keywords: rédige, écris un texte, article, documentation, readme, rapport, résumé, lettre, mail, présentation, plan +--- +Méthode de rédaction : +1. PLAN : annonce la structure (titres/sections) avant de rédiger. Adapte la longueur à la demande — court par défaut. +2. RÉDACTION : paragraphes courts, une idée par paragraphe, français précis. Concret d'abord, généralités interdites. Markdown propre (titres, listes, gras parcimonieux). +3. RELECTURE : traque répétitions, phrases creuses, incohérences de ton. Coupe tout ce qui n'apporte rien. +Interdit : remplissage, formules toutes faites (« dans un monde où… »), conclusion qui répète l'introduction. diff --git a/backend/skills/refactor-sur.md b/backend/skills/refactor-sur.md new file mode 100644 index 0000000..bf9ea30 --- /dev/null +++ b/backend/skills/refactor-sur.md @@ -0,0 +1,11 @@ +--- +name: refactor-sur +title: Refactor sûr +keywords: refactor, refactorise, réorganise, nettoie, simplifie, renomme, découpe, extrait, duplication, dette +--- +Méthode de refactoring, comportement STRICTEMENT identique avant/après : +1. COMPRENDRE : lis TOUT le code concerné et ses usages (grep_search sur chaque symbole touché) avant de modifier quoi que ce soit. +2. PETITS PAS : une seule transformation à la fois (renommage, extraction, déplacement). Jamais plusieurs changements mélangés. +3. VÉRIFIER après CHAQUE pas : run_check sur les fichiers modifiés ; re-grep pour confirmer qu'aucun usage n'est resté sur l'ancien nom. +4. RÉCAPITULER : liste ce qui a changé et pourquoi le comportement est inchangé. +Interdit : changer le comportement ou l'API publique sans le signaler ; renommer sans vérifier tous les usages ; réécrire un fichier entier quand edit_file suffit. diff --git a/backend/tests/test_skills.py b/backend/tests/test_skills.py new file mode 100644 index 0000000..bef7de5 --- /dev/null +++ b/backend/tests/test_skills.py @@ -0,0 +1,26 @@ +import os +import tempfile + +os.environ.setdefault("DATA_DIR", tempfile.mkdtemp()) +os.environ.setdefault("WORKSPACE_DIR", tempfile.mkdtemp()) + +from app import skills # noqa: E402 + + +def test_five_skills_loaded(): + assert len(skills.ALL_SKILLS) == 5 + assert all(s["title"] and s["body"] for s in skills.ALL_SKILLS.values()) + + +def test_bug_message_picks_debug(): + s = skills.pick_skill("mon script plante avec une erreur TypeError au démarrage") + assert s and s["name"] == "debogage-systematique" + + +def test_web_message_picks_web(): + s = skills.pick_skill("crée une page html responsive pour ma boutique") + assert s and s["name"] == "creation-web" + + +def test_banal_message_picks_nothing(): + assert skills.pick_skill("bonjour, quelle heure est-il ?") is None