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:
@@ -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
@@ -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
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user