mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-23 06:58:14 +02:00
feat(skills): 5 méthodes expertes auto-injectées (routeur lexical)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
1 parent
afe0a837d4
commit
31dce6036b
9 files changed
+159
-1
No files matched your search
@@ -64,6 +64,7 @@ PROFILE_FIELDS = {
|
|||||||
"self_review",
|
"self_review",
|
||||||
"rag_enabled",
|
"rag_enabled",
|
||||||
"embed_model",
|
"embed_model",
|
||||||
|
"skills_enabled",
|
||||||
"keep_alive",
|
"keep_alive",
|
||||||
*GENERATION_FIELDS,
|
*GENERATION_FIELDS,
|
||||||
}
|
}
|
||||||
@@ -107,6 +108,8 @@ DEFAULT_CONFIG: dict = {
|
|||||||
# Mémoire long-terme (RAG) entre sessions, via un modèle d'embedding.
|
# Mémoire long-terme (RAG) entre sessions, via un modèle d'embedding.
|
||||||
"rag_enabled": True,
|
"rag_enabled": True,
|
||||||
"embed_model": "auto",
|
"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
|
# Durée de maintien du modèle en VRAM (préchargement). "0" = décharge
|
||||||
# aussitôt, "-1" = jamais, "30m" = 30 minutes.
|
# aussitôt, "-1" = jamais, "30m" = 30 minutes.
|
||||||
"keep_alive": "30m",
|
"keep_alive": "30m",
|
||||||
|
|||||||
@@ -18,7 +18,8 @@ from fastapi import APIRouter, HTTPException
|
|||||||
from fastapi.responses import StreamingResponse
|
from fastapi.responses import StreamingResponse
|
||||||
from pydantic import BaseModel
|
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 ..tools import check_html, _safe_path
|
||||||
from ..agent import run_agent
|
from ..agent import run_agent
|
||||||
from ..config import settings
|
from ..config import settings
|
||||||
@@ -237,6 +238,18 @@ async def chat(req: ChatRequest) -> StreamingResponse:
|
|||||||
+ "\n---\n".join(memories),
|
+ "\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:
|
if plan:
|
||||||
yield _sse("plan", {"steps": plan})
|
yield _sse("plan", {"steps": plan})
|
||||||
|
|
||||||
|
|||||||
@@ -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"]}
|
||||||
@@ -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.
|
||||||
@@ -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).
|
||||||
@@ -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.
|
||||||
@@ -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.
|
||||||
@@ -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.
|
||||||
@@ -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
|
||||||
Reference in new issue
Block a user