Initial commit : agent2_debian13 spécialisé Debian

- 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 <noreply@anthropic.com>
This commit is contained in:
2026-03-07 12:33:51 +00:00
parent 47f88f7cde
commit fb104b352a
4 changed files with 91 additions and 51 deletions
+66 -26
View File
@@ -3,18 +3,18 @@
import asyncio import asyncio
import sys import sys
import threading
import requests import requests
import json import json
from pathlib import Path from pathlib import Path
from slixmpp import ClientXMPP 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_debian13")
sys.path.insert(0, "/opt/agent2")
from skills.loader import load_skills, run_skills from skills.loader import load_skills, run_skills
# ── CONFIG ─────────────────────────────────────────────────────────────── # ── CONFIG ───────────────────────────────────────────────────────────────
CONFIG_DIR = Path("/opt/agent2/config") CONFIG_DIR = Path("/opt/agent2_debian13/config")
CONFIG_FILE = CONFIG_DIR / "config.json" CONFIG_FILE = CONFIG_DIR / "config.json"
PROMPT_FILE = CONFIG_DIR / "system_prompt.txt" PROMPT_FILE = CONFIG_DIR / "system_prompt.txt"
@@ -32,9 +32,13 @@ MODEL = cfg["model"]
XMPP_JID = cfg["xmpp_jid"] XMPP_JID = cfg["xmpp_jid"]
XMPP_PASS = cfg["xmpp_pass"] XMPP_PASS = cfg["xmpp_pass"]
ADMIN_JID = cfg["admin_jid"] 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() SYSTEM_PROMPT = load_system_prompt()
# Charger les skills au démarrage
load_skills() load_skills()
conversation_history = [] conversation_history = []
@@ -48,38 +52,65 @@ def call_ollama(messages: list) -> str:
"options" : {"temperature": 0.3} "options" : {"temperature": 0.3}
} }
response = requests.post(OLLAMA_URL, json=payload, timeout=180) response = requests.post(OLLAMA_URL, json=payload, timeout=180)
data = response.json() return response.json()["message"]["content"]
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
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: try:
# Boucle agentique : le LLM peut enchaîner plusieurs skills
MAX_STEPS = 5 MAX_STEPS = 5
for _ in range(MAX_STEPS): for _ in range(MAX_STEPS):
reply = call_ollama(messages) reply = call_ollama(messages)
skill_triggered, result = run_skills(reply) skill_triggered, result = run_skills(reply)
if not skill_triggered: if not skill_triggered:
# Réponse finale sans commande history.append({"role": "assistant", "content": reply})
conversation_history.append({"role": "assistant", "content": reply})
return reply return reply
# Injecter le résultat du skill et relancer le LLM
messages.append({"role": "assistant", "content": reply}) messages.append({"role": "assistant", "content": reply})
messages.append({"role": "user", "content": "[Résultat skill]\n" + result}) messages.append({"role": "user", "content": "[Résultat skill]\n" + result})
# Sécurité : trop d'étapes
reply = call_ollama(messages) reply = call_ollama(messages)
conversation_history.append({"role": "assistant", "content": reply}) history.append({"role": "assistant", "content": reply})
return reply return reply
except Exception as e: except Exception as e:
error_reply = "Erreur : " + str(e) err = "Erreur : " + str(e)
conversation_history.append({"role": "assistant", "content": error_reply}) history.append({"role": "assistant", "content": err})
return error_reply 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 ───────────────────────────────────────────────────────────── # ── BOT XMPP ─────────────────────────────────────────────────────────────
class AgentBot(ClientXMPP): class AgentBot(ClientXMPP):
@@ -93,7 +124,9 @@ class AgentBot(ClientXMPP):
async def session_start(self, event): async def session_start(self, event):
self.send_presence() self.send_presence()
await self.get_roster() 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): async def message(self, msg):
if msg['type'] not in ('chat', 'normal'): if msg['type'] not in ('chat', 'normal'):
@@ -105,7 +138,9 @@ class AgentBot(ClientXMPP):
if user_input == "!reset": if user_input == "!reset":
conversation_history.clear() 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 return
loop = asyncio.get_event_loop() loop = asyncio.get_event_loop()
@@ -114,6 +149,11 @@ class AgentBot(ClientXMPP):
# ── MAIN ───────────────────────────────────────────────────────────────── # ── MAIN ─────────────────────────────────────────────────────────────────
if __name__ == "__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 = AgentBot()
bot.connect() bot.connect()
bot.loop.run_forever() bot.loop.run_forever()
+23 -23
View File
@@ -1,34 +1,34 @@
Tu es un agent autonome de recherche web. Tu es agent2_debian13, un agent autonome spécialisé dans l'administration de systèmes Debian.
Tu peux chercher des informations sur internet et lire des pages web. Tu travailles sous les ordres d'agent1 qui te délègue des tâches via MQTT.
Tu mémorises les informations importantes pour les réutiliser.
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 : Formats de commandes disponibles :
SEARCH: <requête web> SEARCH: <requête>
→ Recherche web DuckDuckGo (max 5 résultats) → Recherche web si besoin de documentation
READ: <url> READ: <url>
→ Lire et convertir une page web en markdown → Lire une page de documentation
REMEMBER: <clé> | <valeur> REMEMBER: <clé> | <valeur>
→ Mémoriser une information en base SQLite → Mémoriser une information
RECALL: <clé> RECALL: <clé>
→ Récupérer une information mémorisée → Récupérer une information mémorisée
⚠ RÈGLES ABSOLUES : ⚠ RÈGLES :
- Pour toute question sur l'actualité, les événements récents, les prix, - Tu reçois des tâches d'agent1 via MQTT et tu lui réponds via MQTT
les versions de logiciels, les personnes en poste, la météo, ou tout - Tu peux aussi recevoir des instructions directement de sylvain via XMPP
fait pouvant avoir changé : utilise TOUJOURS SEARCH: - Réponds de façon claire, concise et technique
- Ne JAMAIS répondre de mémoire à une question d'actualité - Si une commande shell est nécessaire, indique-la explicitement avec des blocs de code
- Commence TOUJOURS par SEARCH: si la question concerne une information - Signale à agent1 si une tâche est hors de ton domaine Debian
datée ou changeante - Réponds toujours en français
- 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.
+1 -1
View File
@@ -9,7 +9,7 @@ SKILL_NAME = "memory"
TRIGGER = None TRIGGER = None
TRIGGERS = {"REMEMBER:": "remember", "RECALL:": "recall"} TRIGGERS = {"REMEMBER:": "remember", "RECALL:": "recall"}
DB_PATH = Path("/opt/agent2/memory.db") DB_PATH = Path("/opt/agent2_debian13/memory.db")
def _get_conn(): def _get_conn():
conn = sqlite3.connect(DB_PATH) conn = sqlite3.connect(DB_PATH)
+1 -1
View File
@@ -23,7 +23,7 @@ TRIGGERS = {
"PROMPT_DEL:": "prompt_del", "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 1 : embedding factice (hash MD5 → vecteur 16 dims)
# Phase 2 : remplacer par un vrai modèle (ex: sentence-transformers) # Phase 2 : remplacer par un vrai modèle (ex: sentence-transformers)