Initial commit — nexus v2.0

This commit is contained in:
2026-03-09 09:01:33 +00:00
commit 6cd701d673
12 changed files with 798 additions and 0 deletions
+10
View File
@@ -0,0 +1,10 @@
venv/
__pycache__/
*.pyc
*.pyo
*.db
*.log
data/
*.egg-info/
.vault_pass
config/config.json
+27
View File
@@ -0,0 +1,27 @@
Tu es Nexus, l'orchestrateur principal d'un réseau d'agents autonomes spécialisés.
Tu reçois les instructions de l'administrateur (Sylvain) via XMPP et tu décides de les traiter toi-même ou de les déléguer aux agents disponibles.
## Tes capacités de communication
- Tu communiques avec l'utilisateur via XMPP (messages chiffrés OMEMO si activé)
- Tu coordonnes les agents via MQTT (bus de messages structurés)
- Tu peux publier sur n'importe quel topic MQTT avec le skill mqtt_send
- Tu peux envoyer des messages directs à un agent avec le skill delegate
- Les agents t'envoient leurs résultats sur ton inbox MQTT (agents/nexus/inbox)
## Règles de délégation
- Utilise SKILL:delegate pour confier une tâche à un agent spécialisé
- Délègue uniquement à un agent [EN LIGNE]
- Si l'agent est hors ligne, informe l'utilisateur et propose des alternatives
- Transmets toujours le résultat de l'agent à l'utilisateur avec un résumé clair
## Règles générales
- Réponds toujours en français
- Sois concis et précis
- Pour les informations récentes, utilise SKILL:web_search
- Pour mémoriser des informations importantes, utilise SKILL:memory
- Si une tâche nécessite plusieurs étapes sur plusieurs agents, explique le plan avant d'exécuter
## Format des skills
SKILL:<nom> ARGS:<arguments>
La liste des agents disponibles et leur statut est injectée dynamiquement ci-dessous.
+40
View File
@@ -0,0 +1,40 @@
"""
Gestion des rapports quotidiens agrégés de tous les agents.
"""
import logging
from datetime import datetime, timezone
from typing import Optional
logger = logging.getLogger(__name__)
class DailyReportManager:
def __init__(self):
# {agent_id: {"received_at": ..., "content": ...}}
self._reports: dict[str, dict] = {}
def add_report(self, agent_id: str, content: str):
self._reports[agent_id] = {
"received_at": datetime.now(timezone.utc).isoformat(),
"content": content,
}
logger.info(f"[DailyReport] Rapport reçu de {agent_id}")
def get_report(self, agent_id: Optional[str] = None) -> str:
if agent_id:
r = self._reports.get(agent_id)
if not r:
return f"Aucun rapport reçu de {agent_id} aujourd'hui."
return f"── Rapport {agent_id} ({r['received_at'][:16]}) ──\n{r['content']}"
if not self._reports:
return "Aucun rapport reçu aujourd'hui."
lines = [f"── Rapport quotidien ({datetime.now().strftime('%d/%m/%Y')}) ──"]
for aid, r in self._reports.items():
lines.append(f"\n[{aid}] reçu à {r['received_at'][11:16]}")
lines.append(r["content"])
return "\n".join(lines)
def clear(self):
self._reports.clear()
+384
View File
@@ -0,0 +1,384 @@
#!/usr/bin/env python3
"""
Nexus — Orchestrateur principal du système multi-agents.
Reçoit les instructions via XMPP (ou CLI), les traite via LLM,
délègue aux agents spécialisés via MQTT, et renvoie les résultats.
"""
import json
import logging
import os
import sys
import threading
from datetime import datetime, timezone
from pathlib import Path
from typing import Optional
# Ajout du chemin agents_core si non installé en package
sys.path.insert(0, "/opt")
from agents_core import BaseAgent, AgentContext, Message, MessageType
from agents_core.command_parser import ParsedCommand, CommandType, help_text
from scheduler import NexusScheduler
from daily_report import DailyReportManager
logger = logging.getLogger(__name__)
CONFIG_DIR = Path(__file__).parent / "config"
class Nexus(BaseAgent):
AGENT_TYPE = "nexus"
DESCRIPTION = "Orchestrateur principal — reçoit les instructions et coordonne les agents spécialisés"
DEFAULT_CONFIG_PATH = str(CONFIG_DIR / "config.json")
def __init__(self):
super().__init__()
# Rapport quotidien agrégé
self.report_manager = DailyReportManager()
# Scheduler (tâches planifiées, rapports automatiques)
self.scheduler = NexusScheduler(
send_task_callback=self._schedule_send_task,
request_report_callback=self._request_daily_report,
)
# Résultats en attente de réponse XMPP
# {correlation_id: sender_jid}
self._pending_replies: dict[str, str] = {}
self._pending_lock = threading.Lock()
# Mode veille global
self._sleep_mode = False
# ──────────────────────────────────────────────
# Démarrage
# ──────────────────────────────────────────────
def get_skills_dir(self) -> str:
return str(Path(__file__).parent / "skills")
def on_start(self):
self.scheduler.start(self.config.get("schedules", {}))
logger.info("Nexus prêt — scheduler démarré")
def setup_extra_subscriptions(self):
"""Souscriptions MQTT supplémentaires de Nexus."""
# Résultats des agents (réponses à nos délégations)
self.mqtt.subscribe("agents/nexus/inbox", self._on_agent_result)
# Rapports quotidiens des agents
self.mqtt.subscribe("agents/daily_report", self._on_daily_report)
# ──────────────────────────────────────────────
# Réception des résultats agents (MQTT)
# ──────────────────────────────────────────────
def _on_agent_result(self, msg: Message | str, topic: str):
"""Un agent a renvoyé un résultat → transmettre à l'utilisateur via XMPP."""
if isinstance(msg, str):
self._forward_to_user(msg, sender="unknown")
return
if msg.type == MessageType.ALERT:
severity = msg.metadata.get("severity", "warning")
text = f"⚠ Alerte [{severity}] de {msg.sender} :\n{msg.payload}"
self._forward_to_user(text, sender=msg.sender)
return
if msg.type == MessageType.RESULT:
result_text = (
f"Résultat de {msg.sender} :\n{msg.payload}"
)
# Retrouver le JID de l'utilisateur qui a initié la demande
with self._pending_lock:
reply_jid = self._pending_replies.pop(msg.correlation_id, None)
self._forward_to_user(result_text, sender=msg.sender, reply_jid=reply_jid)
return
# Message direct d'un agent
if msg.type == MessageType.DIRECT:
self._forward_to_user(
f"Message de {msg.sender} :\n{msg.payload}",
sender=msg.sender,
)
def _on_daily_report(self, msg: Message | str, topic: str):
"""Réception d'un rapport quotidien d'un agent."""
if isinstance(msg, Message):
self.report_manager.add_report(msg.sender, msg.payload)
logger.info(f"Rapport reçu de {msg.sender}")
def _forward_to_user(self, text: str, sender: str = "", reply_jid: Optional[str] = None):
"""
Envoie un message à l'utilisateur XMPP.
- Si reply_jid : répond à l'expéditeur spécifique (réponse async)
- Sinon : envoie à tous les admins configurés
- Toujours dans le groupe MUC si configuré
"""
if self.xmpp:
if reply_jid:
self.xmpp.send_message(reply_jid, text)
else:
self.xmpp.send_to_all_admins(text)
if self.xmpp.muc_room:
self.xmpp.send_to_group(text)
# ──────────────────────────────────────────────
# Traitement des messages XMPP
# ──────────────────────────────────────────────
def _on_xmpp_message(self, sender: str, body: str, is_muc: bool = False):
"""Override — Nexus gère les commandes et délègue au LLM."""
from agents_core.command_parser import parse as parse_command
if self._sleep_mode and not body.strip().startswith("/"):
return # En veille, ignore les messages sauf commandes système
cmd = parse_command(body)
context = AgentContext(self)
# ── Commandes système /xxx
if cmd.type == CommandType.SYSTEM:
reply = self._handle_system_command(f"/{cmd.command} {cmd.args}", raw_cmd=cmd)
if reply and self.xmpp:
self.xmpp.send_message(sender, reply)
return
# ── Message direct @agent
if cmd.type == CommandType.DIRECT:
reply = self._delegate_direct(cmd, sender)
if self.xmpp:
self.xmpp.send_message(sender, reply)
return
# ── Broadcast @all
if cmd.type == CommandType.BROADCAST:
self.mqtt.broadcast(cmd.args or "")
if self.xmpp:
self.xmpp.send_message(sender, "Broadcast envoyé à tous les agents.")
return
# ── Mode naturel → LLM → skills
extra_ctx = self.registry.summary_for_llm(self._online_agents)
response = self._llm_loop(body, context, extra_ctx)
# Enregistre le JID pour le retour asynchrone éventuel
# (si le LLM a délégué à un agent via DELEGATE skill)
if self.xmpp:
self.xmpp.send_message(sender, response)
def _delegate_direct(self, cmd: ParsedCommand, sender_jid: str) -> str:
"""Route @agent message directement via MQTT."""
target = cmd.target
message = cmd.args or ""
caps = self.registry.get(target)
if caps is None:
known = [a.agent_id for a in self.registry.all_agents()]
return f"Agent '{target}' inconnu.\nAgents connus : {', '.join(known) or 'aucun'}"
with self._online_lock:
online = target in self._online_agents
if not online:
return f"Agent '{target}' est hors ligne."
sent = self.mqtt.send_to(
recipient_id=target,
payload=message,
reply_to=self.mqtt.topic_inbox(),
)
# Mémoriser le JID pour renvoyer la réponse
with self._pending_lock:
self._pending_replies[sent.correlation_id] = sender_jid
return f"Message envoyé à {target}. Attente de la réponse..."
# ──────────────────────────────────────────────
# Commandes système étendues
# ──────────────────────────────────────────────
def handle_custom_command(self, cmd: str, args: str,
source_msg: Optional[Message] = None) -> Optional[str]:
"""Commandes spécifiques à Nexus."""
if cmd == "sleep":
self._sleep_mode = True
return "Nexus en veille. Tape /wake pour reprendre."
if cmd == "wake":
self._sleep_mode = False
return "Nexus actif."
if cmd == "agents":
with self._online_lock:
online = list(self._online_agents)
all_caps = self.registry.all_agents()
lines = ["── Agents ──────────────────"]
for a in all_caps:
status = "🟢 EN LIGNE" if a.agent_id in online else "🔴 hors ligne"
skills = ", ".join(s["name"] for s in a.skills[:5])
lines.append(f" {a.agent_id} [{a.agent_type}] {status}")
lines.append(f" {a.description}")
lines.append(f" Skills: {skills or 'aucun'}")
return "\n".join(lines) if len(lines) > 1 else "Aucun agent connu."
if cmd == "report":
target = args.strip() or None
return self.report_manager.get_report(target)
if cmd == "schedule":
return self._handle_schedule_command(args)
if cmd == "schedules":
return self.scheduler.list_jobs()
if cmd == "update":
# @agent update → git pull + restart
target = args.strip()
if not target:
return "Usage : /update <agent_id>"
self.mqtt.send_to(target, "/update", msg_type=MessageType.COMMAND)
return f"Mise à jour demandée à {target}."
if cmd == "admins":
return self._handle_admins_command(args)
if cmd == "help":
return self._nexus_help()
return f"Commande inconnue : /{cmd}. Tape /help."
def _handle_admins_command(self, args: str) -> str:
"""
/admins → liste les admins autorisés
/admins add <jid> → ajoute un admin
/admins remove <jid> → retire un admin
"""
if not self.xmpp:
return "XMPP non configuré."
parts = args.strip().split(None, 1)
sub = parts[0].lower() if parts else "list"
jid = parts[1].strip() if len(parts) > 1 else ""
# Auto-complète le domaine si pas de @
xmpp_domain = self.config.get("xmpp", {}).get("jid", "@").split("@")[1]
if jid and "@" not in jid:
jid = f"{jid}@{xmpp_domain}"
if sub in ("list", ""):
admins = sorted(self.xmpp.admin_jids)
return "Admins autorisés :\n" + "\n".join(f"{j}" for j in admins) \
if admins else "Aucun admin configuré (accès ouvert)."
if sub == "add":
if not jid:
return "Usage : /admins add <jid>"
self.xmpp.add_admin(jid)
return f"Admin ajouté : {jid}"
if sub == "remove":
if not jid:
return "Usage : /admins remove <jid>"
self.xmpp.remove_admin(jid)
return f"Admin retiré : {jid}"
return "Usage : /admins | /admins add <jid> | /admins remove <jid>"
def _handle_schedule_command(self, args: str) -> str:
"""
/schedule <fréquence> @<agent> <tâche>
Exemples :
/schedule daily 03:00 @debian apt upgrade -y
/schedule every 6h @ansible playbook site.yml
/schedule cancel <job_id>
"""
args = args.strip()
if not args:
return "Usage : /schedule <fréquence> @<agent> <tâche>\nOu : /schedule cancel <job_id>"
if args.startswith("cancel "):
job_id = args.split(" ", 1)[1].strip()
ok = self.scheduler.cancel_job(job_id)
return f"Tâche {job_id} annulée." if ok else f"Tâche {job_id} introuvable."
# Parse : "daily 03:00 @debian apt upgrade"
# ou : "every 6h @ansible playbook site.yml"
try:
parts = args.split()
# Trouver @agent
agent_idx = next(i for i, p in enumerate(parts) if p.startswith("@"))
freq_parts = parts[:agent_idx]
agent_id = parts[agent_idx][1:] # Enlève le @
task = " ".join(parts[agent_idx + 1:])
job_id = self.scheduler.add_job(
frequency=" ".join(freq_parts),
agent_id=agent_id,
task=task,
)
return f"Tâche planifiée (id={job_id}) : [{' '.join(freq_parts)}] @{agent_id}{task}"
except (StopIteration, IndexError, ValueError) as e:
return f"Format invalide : {e}\nUsage : /schedule daily 03:00 @agent tâche"
def _nexus_help(self) -> str:
base = help_text()
nexus_extra = """
── Commandes Nexus ─────────────────
/agents — Liste et statut des agents
/sleep — Mettre Nexus en veille
/wake — Réveiller Nexus
/report [agent] — Rapport quotidien
/schedule <freq> @a tâche — Planifier une tâche
/schedules — Voir les tâches planifiées
/update <agent> — Mettre à jour un agent (git pull)
/admins — Lister les utilisateurs autorisés
/admins add <jid> — Autoriser un nouvel utilisateur
/admins remove <jid> — Retirer un utilisateur
/pause [agent] — Mettre en pause
/resume [agent] — Reprendre
/reset — Effacer l'historique LLM
/status — Statut de Nexus
Mode @agent :
@debian-prod apt update — Commande directe
@all status — Broadcast à tous
"""
return base + nexus_extra
# ──────────────────────────────────────────────
# Scheduler callbacks
# ──────────────────────────────────────────────
def _schedule_send_task(self, agent_id: str, task: str):
"""Callback du scheduler pour envoyer une tâche planifiée."""
with self._online_lock:
online = agent_id in self._online_agents
if not online:
logger.warning(f"[Scheduler] Agent {agent_id} hors ligne, tâche ignorée : {task}")
return
self.mqtt.send_to(agent_id, task)
logger.info(f"[Scheduler] Tâche envoyée à {agent_id} : {task}")
def _request_daily_report(self, agent_id: str):
"""Demande un rapport quotidien à un agent."""
self.mqtt.send_to(agent_id, "/report", msg_type=MessageType.COMMAND)
# ──────────────────────────────────────────────
# Broadcast handler
# ──────────────────────────────────────────────
def on_broadcast(self, msg: Message):
"""Nexus reçoit les broadcasts — les transmet à l'admin si pertinent."""
if msg.type == MessageType.ALERT:
self._forward_to_user(
f"⚠ Alerte broadcast de {msg.sender} :\n{msg.payload}"
)
if __name__ == "__main__":
Nexus().run()
+18
View File
@@ -0,0 +1,18 @@
[Unit]
Description=Nexus — Orchestrateur principal multi-agents
After=network.target mosquitto.service
Wants=mosquitto.service
[Service]
Type=simple
User=root
WorkingDirectory=/opt/nexus
ExecStart=/opt/nexus/venv/bin/python /opt/nexus/nexus.py
Restart=always
RestartSec=5
StandardOutput=journal
StandardError=journal
SyslogIdentifier=nexus
[Install]
WantedBy=multi-user.target
+5
View File
@@ -0,0 +1,5 @@
agents_core @ file:///opt/agents_core
apscheduler>=3.10
duckduckgo-search>=6.0
beautifulsoup4>=4.12
requests>=2.28
+149
View File
@@ -0,0 +1,149 @@
"""
Scheduler de Nexus — gère les tâches planifiées et les rapports automatiques.
Basé sur APScheduler.
"""
import logging
import uuid
from typing import Callable, Optional
from apscheduler.schedulers.background import BackgroundScheduler
from apscheduler.triggers.cron import CronTrigger
from apscheduler.triggers.interval import IntervalTrigger
logger = logging.getLogger(__name__)
class NexusScheduler:
def __init__(
self,
send_task_callback: Callable[[str, str], None],
request_report_callback: Callable[[str], None],
):
self._scheduler = BackgroundScheduler(timezone="Europe/Paris")
self._send_task = send_task_callback
self._request_report = request_report_callback
self._jobs: dict[str, dict] = {} # job_id → metadata
def start(self, config: dict):
"""Démarre le scheduler avec la config initiale."""
self._scheduler.start()
# Tâches planifiées initiales depuis config
for job in config.get("scheduled_tasks", []):
try:
self.add_job(
frequency=job["frequency"],
agent_id=job["agent"],
task=job["task"],
job_id=job.get("id"),
)
except Exception as e:
logger.error(f"[Scheduler] Erreur chargement tâche {job}: {e}")
# Rapports automatiques
for report in config.get("daily_reports", []):
try:
self._add_report_job(
agent_id=report["agent"],
time_str=report["time"], # "08:00"
)
except Exception as e:
logger.error(f"[Scheduler] Erreur rapport {report}: {e}")
logger.info(f"[Scheduler] {len(self._jobs)} job(s) chargé(s)")
def add_job(self, frequency: str, agent_id: str, task: str,
job_id: Optional[str] = None) -> str:
"""
Ajoute une tâche planifiée.
Formats de fréquence supportés :
daily HH:MM → tous les jours à HH:MM
every Xh → toutes les X heures
every Xmin → toutes les X minutes
weekly <day> HH:MM → une fois par semaine (lun/mar/...)
"""
job_id = job_id or str(uuid.uuid4())[:8]
trigger = self._parse_frequency(frequency)
self._scheduler.add_job(
func=self._send_task,
trigger=trigger,
args=[agent_id, task],
id=job_id,
replace_existing=True,
)
self._jobs[job_id] = {
"id": job_id,
"frequency": frequency,
"agent": agent_id,
"task": task,
}
logger.info(f"[Scheduler] Job {job_id} : [{frequency}] @{agent_id}{task}")
return job_id
def _add_report_job(self, agent_id: str, time_str: str) -> str:
"""Planifie une demande de rapport quotidien."""
job_id = f"report_{agent_id}"
hour, minute = map(int, time_str.split(":"))
self._scheduler.add_job(
func=self._request_report,
trigger=CronTrigger(hour=hour, minute=minute),
args=[agent_id],
id=job_id,
replace_existing=True,
)
self._jobs[job_id] = {
"id": job_id,
"frequency": f"daily {time_str}",
"agent": agent_id,
"task": "[rapport quotidien]",
}
return job_id
def cancel_job(self, job_id: str) -> bool:
try:
self._scheduler.remove_job(job_id)
self._jobs.pop(job_id, None)
return True
except Exception:
return False
def list_jobs(self) -> str:
if not self._jobs:
return "Aucune tâche planifiée."
lines = ["── Tâches planifiées ────────────────"]
for j in self._jobs.values():
lines.append(f" [{j['id']}] {j['frequency']} → @{j['agent']} : {j['task']}")
return "\n".join(lines)
def _parse_frequency(self, frequency: str):
"""Parse une fréquence en trigger APScheduler."""
parts = frequency.strip().split()
# daily HH:MM
if parts[0] == "daily" and len(parts) >= 2:
hour, minute = map(int, parts[1].split(":"))
return CronTrigger(hour=hour, minute=minute)
# weekly lun HH:MM
if parts[0] == "weekly" and len(parts) >= 3:
day_map = {
"lun": "mon", "mar": "tue", "mer": "wed",
"jeu": "thu", "ven": "fri", "sam": "sat", "dim": "sun",
}
day = day_map.get(parts[1].lower(), parts[1])
hour, minute = map(int, parts[2].split(":"))
return CronTrigger(day_of_week=day, hour=hour, minute=minute)
# every Xh
if parts[0] == "every" and len(parts) >= 2:
val = parts[1]
if val.endswith("h"):
return IntervalTrigger(hours=int(val[:-1]))
if val.endswith("min"):
return IntervalTrigger(minutes=int(val[:-3]))
if val.endswith("m"):
return IntervalTrigger(minutes=int(val[:-1]))
raise ValueError(f"Format de fréquence non reconnu : '{frequency}'")
+31
View File
@@ -0,0 +1,31 @@
"""
Skill DELEGATE — déléguer une tâche à un agent spécialisé via MQTT.
Usage LLM : SKILL:delegate ARGS:<agent_id> | <tâche>
"""
DESCRIPTION = "Déléguer une tâche à un agent spécialisé"
USAGE = "SKILL:delegate ARGS:<agent_id> | <tâche>"
def run(args: str, context) -> str:
if "|" not in args:
return "Format invalide. Usage : SKILL:delegate ARGS:<agent_id> | <tâche>"
agent_id, task = args.split("|", 1)
agent_id = agent_id.strip()
task = task.strip()
# Vérifier que l'agent est connu
caps = context.registry.get(agent_id)
if caps is None:
known = [a.agent_id for a in context.registry.all_agents()]
return f"Agent '{agent_id}' inconnu. Agents connus : {', '.join(known)}"
# Envoyer la tâche via MQTT
sent = context.mqtt.send_to(
recipient_id=agent_id,
payload=task,
reply_to=context.mqtt.topic_inbox(),
)
return f"Tâche déléguée à {agent_id} (id={sent.correlation_id[:8]}). Attente de la réponse..."
+59
View File
@@ -0,0 +1,59 @@
"""
Skill MEMORY — mémorisation persistante clé/valeur (SQLite).
Usage LLM :
SKILL:memory ARGS:set | <clé> | <valeur>
SKILL:memory ARGS:get | <clé>
SKILL:memory ARGS:list
SKILL:memory ARGS:delete | <clé>
"""
import sqlite3
import os
DESCRIPTION = "Mémorisation persistante d'informations clé/valeur"
USAGE = "SKILL:memory ARGS:set|<clé>|<valeur> ou get|<clé> ou list"
DB_PATH = os.path.join(os.path.dirname(__file__), "..", "data", "memory.db")
def _connect():
os.makedirs(os.path.dirname(DB_PATH), exist_ok=True)
conn = sqlite3.connect(DB_PATH)
conn.execute("""
CREATE TABLE IF NOT EXISTS memory (
key TEXT PRIMARY KEY,
value TEXT NOT NULL,
updated_at TEXT DEFAULT (datetime('now'))
)
""")
return conn
def run(args: str, context) -> str:
parts = [p.strip() for p in args.split("|")]
action = parts[0].lower() if parts else ""
with _connect() as conn:
if action == "set" and len(parts) >= 3:
key, value = parts[1], "|".join(parts[2:])
conn.execute(
"INSERT OR REPLACE INTO memory (key, value, updated_at) VALUES (?, ?, datetime('now'))",
(key, value)
)
return f"Mémorisé : {key} = {value}"
if action == "get" and len(parts) >= 2:
row = conn.execute("SELECT value FROM memory WHERE key = ?", (parts[1],)).fetchone()
return row[0] if row else f"Clé '{parts[1]}' introuvable."
if action == "list":
rows = conn.execute("SELECT key, value FROM memory ORDER BY key").fetchall()
if not rows:
return "Mémoire vide."
return "\n".join(f" {k}: {v}" for k, v in rows)
if action == "delete" and len(parts) >= 2:
conn.execute("DELETE FROM memory WHERE key = ?", (parts[1],))
return f"Clé '{parts[1]}' supprimée."
return "Usage : SKILL:memory ARGS:set|clé|valeur ou get|clé ou list ou delete|clé"
+24
View File
@@ -0,0 +1,24 @@
"""
Skill MQTT_SEND — publier un message sur n'importe quel topic MQTT.
Permet au LLM (et à l'utilisateur) de publier librement sur le bus.
Usage LLM : SKILL:mqtt_send ARGS:<topic> | <message>
"""
DESCRIPTION = "Publier un message sur un topic MQTT arbitraire"
USAGE = "SKILL:mqtt_send ARGS:<topic> | <message>"
def run(args: str, context) -> str:
if "|" not in args:
return "Format invalide. Usage : SKILL:mqtt_send ARGS:<topic> | <message>"
topic, message = args.split("|", 1)
topic = topic.strip()
message = message.strip()
if not topic:
return "Topic vide."
context.mqtt.publish_raw(topic, message)
return f"Message publié sur '{topic}'."
+27
View File
@@ -0,0 +1,27 @@
"""
Skill WEB_READ — lire le contenu d'une URL.
Usage LLM : SKILL:web_read ARGS:<url>
"""
DESCRIPTION = "Lire le contenu d'une page web"
USAGE = "SKILL:web_read ARGS:<url>"
def run(args: str, context) -> str:
url = args.strip()
if not url:
return "URL vide."
try:
import requests
from bs4 import BeautifulSoup
resp = requests.get(url, timeout=15, headers={"User-Agent": "Mozilla/5.0"})
resp.raise_for_status()
soup = BeautifulSoup(resp.text, "html.parser")
# Supprime scripts et styles
for tag in soup(["script", "style", "nav", "footer"]):
tag.decompose()
text = soup.get_text(separator="\n", strip=True)
# Tronqué à 3000 caractères
return text[:3000] + ("..." if len(text) > 3000 else "")
except Exception as e:
return f"Erreur lecture URL : {e}"
+24
View File
@@ -0,0 +1,24 @@
"""
Skill WEB_SEARCH — recherche DuckDuckGo.
Usage LLM : SKILL:web_search ARGS:<requête>
"""
DESCRIPTION = "Recherche web via DuckDuckGo"
USAGE = "SKILL:web_search ARGS:<requête de recherche>"
def run(args: str, context) -> str:
query = args.strip()
if not query:
return "Requête vide."
try:
from duckduckgo_search import DDGS
results = []
with DDGS() as ddgs:
for r in ddgs.text(query, max_results=5):
results.append(f"- {r['title']}\n {r['href']}\n {r['body'][:200]}")
return "\n\n".join(results) if results else "Aucun résultat."
except ImportError:
return "Module duckduckgo_search non installé (pip install duckduckgo-search)"
except Exception as e:
return f"Erreur recherche : {e}"