Initial commit : agent XMPP avec système de skills
- agent1.py : bot XMPP connecté à Ollama avec boucle agentique - skills/web_search.py : recherche DuckDuckGo (ddgs) - skills/web_read.py : lecture et extraction de pages web - skills/memory.py : mémoire persistante SQLite (REMEMBER/RECALL) - skills/loader.py : chargement dynamique des skills
This commit is contained in:
@@ -0,0 +1,61 @@
|
||||
"""
|
||||
Chargeur de skills.
|
||||
Détecte les commandes dans une réponse LLM et exécute le skill correspondant.
|
||||
|
||||
Format attendu dans la réponse du LLM :
|
||||
SEARCH: <requête>
|
||||
READ: <url>
|
||||
REMEMBER: <clé> | <valeur>
|
||||
RECALL: <clé>
|
||||
"""
|
||||
import importlib
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
SKILLS_DIR = Path(__file__).parent
|
||||
|
||||
# Map trigger -> fonction d'exécution
|
||||
_REGISTRY: dict = {}
|
||||
|
||||
def load_skills():
|
||||
"""Charge tous les skills disponibles dans le dossier skills/."""
|
||||
_REGISTRY.clear()
|
||||
|
||||
for py_file in SKILLS_DIR.glob("*.py"):
|
||||
if py_file.name.startswith("_") or py_file.name == "loader.py":
|
||||
continue
|
||||
module_name = "skills.{}".format(py_file.stem)
|
||||
try:
|
||||
mod = importlib.import_module(module_name)
|
||||
except Exception as e:
|
||||
print("[Skills] Impossible de charger {} : {}".format(py_file.name, e))
|
||||
continue
|
||||
|
||||
# Skill avec un seul trigger (ex: SEARCH:, READ:)
|
||||
if hasattr(mod, "TRIGGER") and mod.TRIGGER and hasattr(mod, "execute"):
|
||||
_REGISTRY[mod.TRIGGER] = mod.execute
|
||||
print("[Skills] Chargé : {}".format(mod.TRIGGER))
|
||||
|
||||
# Skill memory : deux triggers
|
||||
if py_file.stem == "memory":
|
||||
if hasattr(mod, "remember"):
|
||||
_REGISTRY["REMEMBER:"] = mod.remember
|
||||
print("[Skills] Chargé : REMEMBER:")
|
||||
if hasattr(mod, "recall"):
|
||||
_REGISTRY["RECALL:"] = mod.recall
|
||||
print("[Skills] Chargé : RECALL:")
|
||||
|
||||
def run_skills(llm_response: str) -> tuple[bool, str]:
|
||||
"""
|
||||
Parcourt la réponse du LLM ligne par ligne.
|
||||
Si une commande est détectée, exécute le skill et retourne (True, résultat).
|
||||
Sinon retourne (False, réponse originale).
|
||||
"""
|
||||
for line in llm_response.splitlines():
|
||||
line = line.strip()
|
||||
for trigger, fn in _REGISTRY.items():
|
||||
if line.upper().startswith(trigger):
|
||||
args = line[len(trigger):].strip()
|
||||
result = fn(args)
|
||||
return True, result
|
||||
return False, llm_response
|
||||
@@ -0,0 +1,49 @@
|
||||
"""
|
||||
Skill : REMEMBER / RECALL
|
||||
Mémorise et récupère des informations dans une base SQLite.
|
||||
"""
|
||||
import sqlite3
|
||||
from pathlib import Path
|
||||
|
||||
SKILL_NAME = "memory"
|
||||
TRIGGER = None # Géré via REMEMBER: et RECALL: séparément dans le loader
|
||||
|
||||
DB_PATH = Path("/opt/agent/memory.db")
|
||||
|
||||
def _get_conn():
|
||||
conn = sqlite3.connect(DB_PATH)
|
||||
conn.execute("""
|
||||
CREATE TABLE IF NOT EXISTS memory (
|
||||
key TEXT PRIMARY KEY,
|
||||
value TEXT NOT NULL
|
||||
)
|
||||
""")
|
||||
conn.commit()
|
||||
return conn
|
||||
|
||||
def remember(args: str) -> str:
|
||||
if "|" not in args:
|
||||
return "Erreur : format attendu → REMEMBER: <clé> | <valeur>"
|
||||
key, _, value = args.partition("|")
|
||||
key, value = key.strip(), value.strip()
|
||||
if not key or not value:
|
||||
return "Erreur : clé ou valeur vide."
|
||||
try:
|
||||
with _get_conn() as conn:
|
||||
conn.execute("INSERT OR REPLACE INTO memory (key, value) VALUES (?, ?)", (key, value))
|
||||
return "Mémorisé : «{}» = «{}»".format(key, value)
|
||||
except Exception as e:
|
||||
return "Erreur mémoire : {}".format(e)
|
||||
|
||||
def recall(args: str) -> str:
|
||||
key = args.strip()
|
||||
if not key:
|
||||
return "Erreur : clé vide."
|
||||
try:
|
||||
with _get_conn() as conn:
|
||||
row = conn.execute("SELECT value FROM memory WHERE key = ?", (key,)).fetchone()
|
||||
if row:
|
||||
return "Mémoire «{}» : {}".format(key, row[0])
|
||||
return "Aucune information mémorisée pour «{}».".format(key)
|
||||
except Exception as e:
|
||||
return "Erreur mémoire : {}".format(e)
|
||||
@@ -0,0 +1,38 @@
|
||||
"""
|
||||
Skill : READ
|
||||
Télécharge une page web et la convertit en texte lisible.
|
||||
"""
|
||||
import urllib.request
|
||||
from bs4 import BeautifulSoup
|
||||
|
||||
SKILL_NAME = "read"
|
||||
TRIGGER = "READ:"
|
||||
|
||||
MAX_CHARS = 4000
|
||||
|
||||
def execute(args: str) -> str:
|
||||
url = args.strip()
|
||||
if not url:
|
||||
return "Erreur : URL vide."
|
||||
try:
|
||||
req = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0"})
|
||||
with urllib.request.urlopen(req, timeout=15) as r:
|
||||
html = r.read()
|
||||
|
||||
soup = BeautifulSoup(html, "html.parser")
|
||||
|
||||
# Supprimer scripts, styles, nav
|
||||
for tag in soup(["script", "style", "nav", "footer", "header"]):
|
||||
tag.decompose()
|
||||
|
||||
text = soup.get_text(separator="\n")
|
||||
lines = [l.strip() for l in text.splitlines() if l.strip()]
|
||||
content = "\n".join(lines)
|
||||
|
||||
if len(content) > MAX_CHARS:
|
||||
content = content[:MAX_CHARS] + "\n...[tronqué]"
|
||||
|
||||
return "Contenu de {} :\n{}".format(url, content)
|
||||
|
||||
except Exception as e:
|
||||
return "Erreur lors de la lecture de {} : {}".format(url, e)
|
||||
@@ -0,0 +1,28 @@
|
||||
"""
|
||||
Skill : SEARCH
|
||||
Effectue une recherche DuckDuckGo et retourne les 5 premiers résultats.
|
||||
"""
|
||||
from ddgs import DDGS
|
||||
|
||||
SKILL_NAME = "search"
|
||||
TRIGGER = "SEARCH:"
|
||||
|
||||
def execute(args: str) -> str:
|
||||
query = args.strip()
|
||||
if not query:
|
||||
return "Erreur : requête vide."
|
||||
try:
|
||||
with DDGS() as ddgs:
|
||||
results = list(ddgs.text(query, max_results=5))
|
||||
|
||||
if not results:
|
||||
return "Aucun résultat trouvé pour : {}".format(query)
|
||||
|
||||
lines = ["Résultats de recherche pour «{}» :".format(query)]
|
||||
for r in results:
|
||||
lines.append("- **{}**\n {}\n {}".format(r.get("title", ""), r.get("body", ""), r.get("href", "")))
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
except Exception as e:
|
||||
return "Erreur lors de la recherche : {}".format(e)
|
||||
Reference in New Issue
Block a user