From fb104b352aeff813ba4e7ad1e709512be22000a4 Mon Sep 17 00:00:00 2001 From: sylvain Date: Sat, 7 Mar 2026 12:33:51 +0000 Subject: [PATCH] =?UTF-8?q?Initial=20commit=20:=20agent2=5Fdebian13=20sp?= =?UTF-8?q?=C3=A9cialis=C3=A9=20Debian?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - agent2_debian13.py : bot XMPP + listener MQTT continu - System prompt spécialisé administration Debian - Skills : web_search, web_read, memory, prompt_memory, mqtt - Reçoit les tâches d'agent1 via MQTT (agents/agent2_debian13/inbox) - Répond via MQTT (agents/agent1/inbox) - Communication directe avec sylvain via XMPP Co-Authored-By: Claude Sonnet 4.6 --- agent2.py => agent2_debian13.py | 92 +++++++++++++++++++++++---------- config/system_prompt.txt | 46 ++++++++--------- skills/memory.py | 2 +- skills/prompt_memory.py | 2 +- 4 files changed, 91 insertions(+), 51 deletions(-) rename agent2.py => agent2_debian13.py (55%) diff --git a/agent2.py b/agent2_debian13.py similarity index 55% rename from agent2.py rename to agent2_debian13.py index ac361bc..865ec78 100644 --- a/agent2.py +++ b/agent2_debian13.py @@ -3,18 +3,18 @@ import asyncio import sys +import threading import requests import json from pathlib import Path from slixmpp import ClientXMPP +import paho.mqtt.client as mqtt -# Ajouter /opt/agent2 au path pour importer les skills -sys.path.insert(0, "/opt/agent2") - +sys.path.insert(0, "/opt/agent2_debian13") from skills.loader import load_skills, run_skills # ── CONFIG ─────────────────────────────────────────────────────────────── -CONFIG_DIR = Path("/opt/agent2/config") +CONFIG_DIR = Path("/opt/agent2_debian13/config") CONFIG_FILE = CONFIG_DIR / "config.json" PROMPT_FILE = CONFIG_DIR / "system_prompt.txt" @@ -32,9 +32,13 @@ MODEL = cfg["model"] XMPP_JID = cfg["xmpp_jid"] XMPP_PASS = cfg["xmpp_pass"] ADMIN_JID = cfg["admin_jid"] +MQTT_HOST = cfg["mqtt_host"] +MQTT_PORT = int(cfg["mqtt_port"]) +MQTT_CLIENT = cfg["mqtt_client_id"] +MQTT_INBOX = cfg["mqtt_inbox"] +MQTT_OUTBOX = cfg["mqtt_outbox"] SYSTEM_PROMPT = load_system_prompt() -# Charger les skills au démarrage load_skills() conversation_history = [] @@ -48,38 +52,65 @@ def call_ollama(messages: list) -> str: "options" : {"temperature": 0.3} } response = requests.post(OLLAMA_URL, json=payload, timeout=180) - data = response.json() - return data["message"]["content"] - -def ask_llm(user_message: str) -> str: - conversation_history.append({"role": "user", "content": user_message}) - messages = [{"role": "system", "content": SYSTEM_PROMPT}] + conversation_history + return response.json()["message"]["content"] +def ask_llm(user_message: str, history: list = None) -> str: + if history is None: + history = conversation_history + history.append({"role": "user", "content": user_message}) + messages = [{"role": "system", "content": SYSTEM_PROMPT}] + history try: - # Boucle agentique : le LLM peut enchaîner plusieurs skills MAX_STEPS = 5 for _ in range(MAX_STEPS): reply = call_ollama(messages) skill_triggered, result = run_skills(reply) - if not skill_triggered: - # Réponse finale sans commande - conversation_history.append({"role": "assistant", "content": reply}) + history.append({"role": "assistant", "content": reply}) return reply - - # Injecter le résultat du skill et relancer le LLM messages.append({"role": "assistant", "content": reply}) messages.append({"role": "user", "content": "[Résultat skill]\n" + result}) - - # Sécurité : trop d'étapes reply = call_ollama(messages) - conversation_history.append({"role": "assistant", "content": reply}) + history.append({"role": "assistant", "content": reply}) return reply - except Exception as e: - error_reply = "Erreur : " + str(e) - conversation_history.append({"role": "assistant", "content": error_reply}) - return error_reply + err = "Erreur : " + str(e) + history.append({"role": "assistant", "content": err}) + return err + +# ── MQTT LISTENER ───────────────────────────────────────────────────────── +mqtt_publish_client = None + +def mqtt_publish(topic: str, message: str): + global mqtt_publish_client + if mqtt_publish_client: + mqtt_publish_client.publish(topic, message) + +def on_mqtt_message(client, userdata, msg): + task = msg.payload.decode(errors="replace") + print(f"[MQTT] Tâche reçue d'agent1 : {task[:100]}") + # Historique isolé par tâche MQTT (pas mélangé avec XMPP) + mqtt_history = [] + reply = ask_llm(task, history=mqtt_history) + print(f"[MQTT] Réponse envoyée : {reply[:100]}") + mqtt_publish(MQTT_OUTBOX, reply) + +def start_mqtt_listener(): + global mqtt_publish_client + + # Client dédié à la publication + mqtt_publish_client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, + client_id=MQTT_CLIENT + "_pub") + mqtt_publish_client.connect(MQTT_HOST, MQTT_PORT) + mqtt_publish_client.loop_start() + + # Client dédié à la souscription + sub_client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, + client_id=MQTT_CLIENT + "_sub") + sub_client.on_message = on_mqtt_message + sub_client.connect(MQTT_HOST, MQTT_PORT) + sub_client.subscribe(MQTT_INBOX) + print(f"[MQTT] Écoute sur {MQTT_INBOX}") + sub_client.loop_forever() # ── BOT XMPP ───────────────────────────────────────────────────────────── class AgentBot(ClientXMPP): @@ -93,7 +124,9 @@ class AgentBot(ClientXMPP): async def session_start(self, event): self.send_presence() await self.get_roster() - self.send_message(mto=ADMIN_JID, mbody="Agent en ligne !", mtype='chat') + self.send_message(mto=ADMIN_JID, + mbody="Agent2_Debian13 en ligne !", + mtype='chat') async def message(self, msg): if msg['type'] not in ('chat', 'normal'): @@ -105,7 +138,9 @@ class AgentBot(ClientXMPP): if user_input == "!reset": conversation_history.clear() - self.send_message(mto=ADMIN_JID, mbody="Conversation reinitialisee.", mtype='chat') + self.send_message(mto=ADMIN_JID, + mbody="Conversation reinitialisee.", + mtype='chat') return loop = asyncio.get_event_loop() @@ -114,6 +149,11 @@ class AgentBot(ClientXMPP): # ── MAIN ───────────────────────────────────────────────────────────────── if __name__ == "__main__": + # Lancer le listener MQTT dans un thread séparé + mqtt_thread = threading.Thread(target=start_mqtt_listener, daemon=True) + mqtt_thread.start() + + # Lancer le bot XMPP bot = AgentBot() bot.connect() bot.loop.run_forever() diff --git a/config/system_prompt.txt b/config/system_prompt.txt index 717d0c4..79f01ef 100644 --- a/config/system_prompt.txt +++ b/config/system_prompt.txt @@ -1,34 +1,34 @@ -Tu es un agent autonome de recherche web. -Tu peux chercher des informations sur internet et lire des pages web. -Tu mémorises les informations importantes pour les réutiliser. +Tu es agent2_debian13, un agent autonome spécialisé dans l'administration de systèmes Debian. +Tu travailles sous les ordres d'agent1 qui te délègue des tâches via MQTT. + +Tes domaines de compétence : +- Gestion des paquets : apt, dpkg, snap +- Services systemd : start, stop, enable, status, journalctl +- Conteneurs : LXC, LXD, Docker sur Debian +- Machines virtuelles : KVM/QEMU, libvirt +- Réseau Debian : interfaces, /etc/network, NetworkManager +- Sécurité : ufw, fail2ban, SSH, sudoers +- Fichiers de config système : fstab, crontab, hosts +- Surveillance : top, htop, df, du, netstat, ss Formats de commandes disponibles : -SEARCH: - → Recherche web DuckDuckGo (max 5 résultats) +SEARCH: + → Recherche web si besoin de documentation READ: - → Lire et convertir une page web en markdown + → Lire une page de documentation REMEMBER: | - → Mémoriser une information en base SQLite + → Mémoriser une information RECALL: → Récupérer une information mémorisée -⚠ RÈGLES ABSOLUES : -- Pour toute question sur l'actualité, les événements récents, les prix, - les versions de logiciels, les personnes en poste, la météo, ou tout - fait pouvant avoir changé : utilise TOUJOURS SEARCH: -- Ne JAMAIS répondre de mémoire à une question d'actualité -- Commence TOUJOURS par SEARCH: si la question concerne une information - datée ou changeante -- Ta date de coupure est ancienne : toute info récente DOIT être vérifiée - via SEARCH: -- Si les résultats de recherche ne sont pas suffisants, utilise READ: sur - les URLs prometteuses pour approfondir -- Mémorise les informations importantes avec REMEMBER: -- Synthétise toujours les informations trouvées de façon claire et concise - -Réponds toujours en français. Sois concis mais précis. -Explique ce que tu vas faire avant de le faire. +⚠ RÈGLES : +- Tu reçois des tâches d'agent1 via MQTT et tu lui réponds via MQTT +- Tu peux aussi recevoir des instructions directement de sylvain via XMPP +- Réponds de façon claire, concise et technique +- Si une commande shell est nécessaire, indique-la explicitement avec des blocs de code +- Signale à agent1 si une tâche est hors de ton domaine Debian +- Réponds toujours en français diff --git a/skills/memory.py b/skills/memory.py index 0104106..3f06e54 100644 --- a/skills/memory.py +++ b/skills/memory.py @@ -9,7 +9,7 @@ SKILL_NAME = "memory" TRIGGER = None TRIGGERS = {"REMEMBER:": "remember", "RECALL:": "recall"} -DB_PATH = Path("/opt/agent2/memory.db") +DB_PATH = Path("/opt/agent2_debian13/memory.db") def _get_conn(): conn = sqlite3.connect(DB_PATH) diff --git a/skills/prompt_memory.py b/skills/prompt_memory.py index 870c4d4..5f67d8c 100644 --- a/skills/prompt_memory.py +++ b/skills/prompt_memory.py @@ -23,7 +23,7 @@ TRIGGERS = { "PROMPT_DEL:": "prompt_del", } -DB_PATH = Path("/opt/agent2/chroma_db") +DB_PATH = Path("/opt/agent2_debian13/chroma_db") # Phase 1 : embedding factice (hash MD5 → vecteur 16 dims) # Phase 2 : remplacer par un vrai modèle (ex: sentence-transformers)