59c49060aa
- task_queue.py : module FIFO persistant (queue.db), QoS 1 - agent2_ansible.py : intégration queue, topic agents/agent2_ansible/control commandes : pause / resume / report (stats envoyées sur agents/daily_report) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
227 lines
8.7 KiB
Python
227 lines
8.7 KiB
Python
#!/usr/bin/env python3
|
|
# -*- coding: utf-8 -*-
|
|
|
|
import asyncio
|
|
import sys
|
|
import threading
|
|
import requests
|
|
import json
|
|
from pathlib import Path
|
|
from datetime import datetime
|
|
from slixmpp import ClientXMPP
|
|
import paho.mqtt.client as mqtt
|
|
|
|
BASE_DIR = Path(__file__).parent.resolve()
|
|
sys.path.insert(0, str(BASE_DIR))
|
|
from skills.loader import load_skills, run_skills
|
|
from task_queue import TaskQueue
|
|
|
|
# ── CONFIG ───────────────────────────────────────────────────────────────
|
|
CONFIG_DIR = BASE_DIR / "config"
|
|
CONFIG_FILE = CONFIG_DIR / "config.json"
|
|
PROMPT_FILE = CONFIG_DIR / "system_prompt.txt"
|
|
QUEUE_DB = BASE_DIR / "queue.db"
|
|
|
|
def load_config():
|
|
with open(CONFIG_FILE, "r", encoding="utf-8") as f:
|
|
return json.load(f)
|
|
|
|
def load_system_prompt():
|
|
with open(PROMPT_FILE, "r", encoding="utf-8") as f:
|
|
return f.read()
|
|
|
|
cfg = load_config()
|
|
OLLAMA_URL = cfg["ollama_url"]
|
|
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()
|
|
|
|
load_skills()
|
|
|
|
conversation_history = []
|
|
start_time = datetime.now()
|
|
|
|
# ── LLM ──────────────────────────────────────────────────────────────────
|
|
def call_ollama(messages: list) -> str:
|
|
payload = {
|
|
"model" : MODEL,
|
|
"messages": messages,
|
|
"stream" : False,
|
|
"options" : {"temperature": 0.3}
|
|
}
|
|
response = requests.post(OLLAMA_URL, json=payload, timeout=180)
|
|
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:
|
|
MAX_STEPS = 5
|
|
for _ in range(MAX_STEPS):
|
|
reply = call_ollama(messages)
|
|
skill_triggered, result = run_skills(reply)
|
|
if not skill_triggered:
|
|
history.append({"role": "assistant", "content": reply})
|
|
return reply
|
|
messages.append({"role": "assistant", "content": reply})
|
|
messages.append({"role": "user", "content": "[Résultat skill]\n" + result})
|
|
reply = call_ollama(messages)
|
|
history.append({"role": "assistant", "content": reply})
|
|
return reply
|
|
except Exception as e:
|
|
err = "Erreur : " + str(e)
|
|
history.append({"role": "assistant", "content": err})
|
|
return err
|
|
|
|
# ── MQTT ──────────────────────────────────────────────────────────────────
|
|
mqtt_publish_client = None
|
|
task_queue: TaskQueue = None
|
|
|
|
def mqtt_publish(topic: str, message: str):
|
|
global mqtt_publish_client
|
|
if mqtt_publish_client:
|
|
mqtt_publish_client.publish(topic, message)
|
|
|
|
def _process_task(task: str) -> str:
|
|
"""Worker appelé par la TaskQueue pour chaque tâche."""
|
|
mqtt_history = []
|
|
return ask_llm(task, history=mqtt_history)
|
|
|
|
def on_mqtt_message(client, userdata, msg):
|
|
"""Enqueue la tâche — le worker la traitera en FIFO."""
|
|
task = msg.payload.decode(errors="replace")
|
|
print(f"[MQTT] Tâche reçue, mise en queue : {task[:100]}")
|
|
task_queue.enqueue(task)
|
|
|
|
def on_control_message(client, userdata, msg):
|
|
"""Gère les commandes de contrôle : pause / resume / report."""
|
|
try:
|
|
data = json.loads(msg.payload.decode(errors="replace"))
|
|
command = data.get("command", "")
|
|
print(f"[CONTROL] Commande reçue : {command}")
|
|
|
|
if command == "pause":
|
|
task_queue.pause()
|
|
|
|
elif command == "resume":
|
|
task_queue.resume()
|
|
|
|
elif command == "report":
|
|
stats = task_queue.get_stats()
|
|
uptime_s = int((datetime.now() - start_time).total_seconds())
|
|
payload = json.dumps({
|
|
"agent" : MQTT_CLIENT,
|
|
"timestamp" : datetime.now().isoformat(timespec='seconds'),
|
|
"uptime_s" : uptime_s,
|
|
"paused" : task_queue.paused,
|
|
**stats
|
|
}, ensure_ascii=False)
|
|
mqtt_publish("agents/daily_report", payload)
|
|
print(f"[CONTROL] Rapport envoyé sur agents/daily_report")
|
|
|
|
except Exception as e:
|
|
print(f"[CONTROL] Erreur : {e}")
|
|
|
|
def register_to_agent1():
|
|
payload = json.dumps({
|
|
"agent" : MQTT_CLIENT,
|
|
"jid" : XMPP_JID,
|
|
"mqtt_inbox": MQTT_INBOX,
|
|
"speciality": "Automatisation infrastructure via Ansible : playbooks, commandes ad-hoc, déploiement multi-hôtes, gestion de configuration sur le réseau local",
|
|
})
|
|
mqtt_publish("agents/register", payload)
|
|
print("[REGISTER] Déclaration envoyée à agent1.")
|
|
|
|
def start_mqtt_listener():
|
|
global mqtt_publish_client, task_queue
|
|
|
|
_status_topic = "agents/status/{}".format(MQTT_CLIENT)
|
|
_offline_payload = json.dumps({"status": "offline", "agent": MQTT_CLIENT})
|
|
_online_payload = json.dumps({
|
|
"status" : "online",
|
|
"agent" : MQTT_CLIENT,
|
|
"jid" : XMPP_JID,
|
|
"mqtt_inbox": MQTT_INBOX,
|
|
})
|
|
|
|
mqtt_publish_client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2,
|
|
client_id=MQTT_CLIENT + "_pub",
|
|
clean_session=False)
|
|
mqtt_publish_client.will_set(_status_topic, _offline_payload, retain=True)
|
|
mqtt_publish_client.connect(MQTT_HOST, MQTT_PORT)
|
|
mqtt_publish_client.loop_start()
|
|
mqtt_publish_client.publish(_status_topic, _online_payload, retain=True)
|
|
register_to_agent1()
|
|
|
|
# Initialiser et démarrer la queue
|
|
task_queue = TaskQueue(QUEUE_DB, _process_task, mqtt_publish, MQTT_OUTBOX)
|
|
task_queue.start_worker()
|
|
|
|
# Client souscription (QoS 1 + session persistante)
|
|
sub_client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2,
|
|
client_id=MQTT_CLIENT + "_sub",
|
|
clean_session=False)
|
|
sub_client.message_callback_add("agents/{}/control".format(MQTT_CLIENT),
|
|
on_control_message)
|
|
sub_client.on_message = on_mqtt_message
|
|
sub_client.connect(MQTT_HOST, MQTT_PORT)
|
|
sub_client.subscribe([
|
|
(MQTT_INBOX, 1),
|
|
("agents/{}/control".format(MQTT_CLIENT), 1),
|
|
])
|
|
print(f"[MQTT] Écoute sur {MQTT_INBOX} + agents/{MQTT_CLIENT}/control")
|
|
sub_client.loop_forever()
|
|
|
|
# ── BOT XMPP ─────────────────────────────────────────────────────────────
|
|
class AgentBot(ClientXMPP):
|
|
def __init__(self):
|
|
ClientXMPP.__init__(self, XMPP_JID, XMPP_PASS)
|
|
self.add_event_handler("session_start", self.session_start)
|
|
self.add_event_handler("message", self.message)
|
|
self.register_plugin('xep_0030')
|
|
self.register_plugin('xep_0199')
|
|
|
|
async def session_start(self, event):
|
|
self.send_presence()
|
|
await self.get_roster()
|
|
self.send_message(mto=ADMIN_JID,
|
|
mbody="Agent2_Ansible en ligne !",
|
|
mtype='chat')
|
|
|
|
async def message(self, msg):
|
|
if msg['type'] not in ('chat', 'normal'):
|
|
return
|
|
if str(msg['from']).split('/')[0] != ADMIN_JID:
|
|
return
|
|
|
|
user_input = msg['body'].strip()
|
|
|
|
if user_input == "!reset":
|
|
conversation_history.clear()
|
|
self.send_message(mto=ADMIN_JID,
|
|
mbody="Conversation reinitialisee.",
|
|
mtype='chat')
|
|
return
|
|
|
|
loop = asyncio.get_event_loop()
|
|
reply = await loop.run_in_executor(None, ask_llm, user_input)
|
|
self.send_message(mto=ADMIN_JID, mbody=reply, mtype='chat')
|
|
|
|
# ── MAIN ─────────────────────────────────────────────────────────────────
|
|
if __name__ == "__main__":
|
|
mqtt_thread = threading.Thread(target=start_mqtt_listener, daemon=True)
|
|
mqtt_thread.start()
|
|
|
|
bot = AgentBot()
|
|
bot.connect()
|
|
bot.loop.run_forever()
|