Compare commits
6 Commits
60a216d565
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 0fe1ece68d | |||
| 1565b145dc | |||
| 3575b391b6 | |||
| 1c951f46f1 | |||
| 37e881bc39 | |||
| d765a8457a |
@@ -2,38 +2,221 @@
|
||||
|
||||
Agent principal du réseau. Il reçoit les instructions de l'utilisateur (via XMPP ou CLI MQTT), les analyse avec un LLM, et délègue les tâches aux agents spécialisés.
|
||||
|
||||
## Rôle
|
||||
---
|
||||
|
||||
- Point d'entrée unique pour l'utilisateur (`sylvain@xmpp.ovh`)
|
||||
- Délègue aux agents spécialisés via MQTT (`DELEGATE: <agent> | <tâche>`)
|
||||
- Planifie des tâches récurrentes (`SCHEDULE:`)
|
||||
- Centralise les rapports et erreurs des agents
|
||||
- Maintient un registre des agents disponibles
|
||||
## Commandes XMPP (sylvain@xmpp.ovh → agent1)
|
||||
|
||||
## Skills disponibles
|
||||
### Contrôle des agents
|
||||
|
||||
| Trigger | Description |
|
||||
| Commande | Effet |
|
||||
|---|---|
|
||||
| `SEARCH: <requête>` | Recherche DuckDuckGo |
|
||||
| `READ: <url>` | Lecture de page web |
|
||||
| `REMEMBER: <clé> \| <valeur>` | Mémoire SQLite |
|
||||
| `RECALL: <clé>` | Récupération mémoire |
|
||||
| `MQTT_PUBLISH: <topic> \| <msg>` | Publication MQTT |
|
||||
| `DELEGATE: <agent> \| <tâche>` | Délégation à un agent spécialisé |
|
||||
| `PLAN: <agent>\|<tâche> ;; <agent>\|<tâche>` | Plan multi-étapes |
|
||||
| `SCHEDULE: <cron> \| <agent> \| <tâche>` | Planification |
|
||||
| `PLAN_LIST:` | Lister les tâches planifiées |
|
||||
| `PLAN_CANCEL: <id>` | Annuler une planification |
|
||||
| `REPORT:` | Rapport d'exécutions |
|
||||
| `REPORT_ERRORS:` | Rapport des erreurs |
|
||||
| `!agentsOFF` | Agent1 en veille + pause de tous les agents |
|
||||
| `!agentsON` | Agent1 actif + reprise de tous les agents |
|
||||
| `!agentOFF <nom>` | Pause d'un agent spécifique (ex: `!agentOFF agent2_debian13`) |
|
||||
| `!agentON <nom>` | Reprise d'un agent spécifique |
|
||||
| `!agentOFF agent1` | Agent1 en veille uniquement (les autres agents continuent) |
|
||||
| `!agentON agent1` | Agent1 sort de veille |
|
||||
|
||||
> En mode veille, agent1 reste connecté XMPP et répond uniquement aux commandes `!agentON`.
|
||||
|
||||
### Mises à jour git
|
||||
|
||||
| Commande | Effet |
|
||||
|---|---|
|
||||
| `!agentUPDATE <nom>` | Vérifie si une mise à jour est disponible sur le dépôt git de l'agent |
|
||||
| `!agentsUPDATE` | Vérifie les dépôts de tous les agents enregistrés |
|
||||
| `!agentUPGRADE <nom>` | `git pull` + `systemctl restart` de l'agent |
|
||||
| `!agentsUPGRADE` | `git pull` + restart de tous les agents (agent1 en dernier) |
|
||||
|
||||
> `!agentUPGRADE agent1` redémarre agent1 lui-même via systemd. La réponse XMPP est envoyée avant le redémarrage.
|
||||
|
||||
### Affichage des configurations
|
||||
|
||||
| Commande | Effet |
|
||||
|---|---|
|
||||
| `!reports` | Afficher `reports_schedule.json` (horaires des rapports) |
|
||||
| `!tasks` | Afficher `tasks_schedule.json` (tâches planifiées) |
|
||||
| `!blackout` | Afficher `blackout_hours.json` (plages d'inactivité) |
|
||||
|
||||
### Divers
|
||||
|
||||
| Commande | Effet |
|
||||
|---|---|
|
||||
| `!reset` | Réinitialiser l'historique de conversation |
|
||||
|
||||
---
|
||||
|
||||
## Skills LLM (utilisables en XMPP ou CLI)
|
||||
|
||||
Ces commandes sont générées par le LLM en réponse à une instruction en langage naturel.
|
||||
|
||||
### Délégation et planification
|
||||
|
||||
| Trigger | Syntaxe | Description |
|
||||
|---|---|---|
|
||||
| `DELEGATE:` | `DELEGATE: <agent> \| <tâche>` | Délègue une tâche à un agent (avec vérif. plage horaire + blackout) |
|
||||
| `PLAN:` | `PLAN: <agent>\|<tâche> ;; <agent>\|<tâche>` | Exécute plusieurs tâches en séquence |
|
||||
| `SCHEDULE:` | `SCHEDULE: <cron> \| <agent> \| <tâche>` | Planifie une tâche récurrente |
|
||||
| `PLAN_LIST:` | `PLAN_LIST:` | Lister les tâches planifiées |
|
||||
| `PLAN_CANCEL:` | `PLAN_CANCEL: <id>` | Annuler une planification |
|
||||
|
||||
**Expressions cron supportées :**
|
||||
```
|
||||
daily 03:00 → tous les jours à 03h00
|
||||
every 6h → toutes les 6 heures
|
||||
every 30min → toutes les 30 minutes
|
||||
weekly lun 08:00 → chaque lundi à 08h00
|
||||
```
|
||||
|
||||
### Rapports
|
||||
|
||||
| Trigger | Syntaxe | Description |
|
||||
|---|---|---|
|
||||
| `REPORT:` | `REPORT: [agent]` | Rapport des 20 dernières exécutions |
|
||||
| `REPORT_ERRORS:` | `REPORT_ERRORS: [agent]` | Rapport des 20 dernières erreurs |
|
||||
| `DAILY_REPORT:` | `DAILY_REPORT: [agent]` | Rapport journalier compilé (stats de tous les agents) |
|
||||
|
||||
### Mémoire et recherche
|
||||
|
||||
| Trigger | Syntaxe | Description |
|
||||
|---|---|---|
|
||||
| `SEARCH:` | `SEARCH: <requête>` | Recherche DuckDuckGo |
|
||||
| `READ:` | `READ: <url>` | Lecture de page web |
|
||||
| `REMEMBER:` | `REMEMBER: <clé> \| <valeur>` | Mémoriser une information (SQLite) |
|
||||
| `RECALL:` | `RECALL: <clé>` | Récupérer une information mémorisée |
|
||||
|
||||
### MQTT direct
|
||||
|
||||
| Trigger | Syntaxe | Description |
|
||||
|---|---|---|
|
||||
| `MQTT_PUBLISH:` | `MQTT_PUBLISH: <topic> \| <message>` | Publier un message MQTT |
|
||||
| `MQTT_SUBSCRIBE:` | `MQTT_SUBSCRIBE: <topic>` | Écouter un topic MQTT |
|
||||
|
||||
---
|
||||
|
||||
## CLI (`cli.py`)
|
||||
|
||||
```bash
|
||||
python3 /opt/agent/cli.py # parler à agent1
|
||||
python3 /opt/agent/cli.py agent2_debian13 # accès direct à un agent
|
||||
python3 /opt/agent/cli.py --plans # voir les planifications
|
||||
```
|
||||
|
||||
**Commandes dans la CLI :**
|
||||
|
||||
| Commande | Effet |
|
||||
|---|---|
|
||||
| `/reset` | Réinitialiser la conversation |
|
||||
| `/plans` | Lister les tâches planifiées |
|
||||
| `/report` | Rapport des dernières exécutions |
|
||||
| `/errors` | Rapport des erreurs |
|
||||
| `/agent <nom>` | Basculer vers un autre agent |
|
||||
| `/quit` | Quitter la CLI |
|
||||
|
||||
---
|
||||
|
||||
## Fichiers de configuration
|
||||
|
||||
| Fichier | Rôle | Modifié par |
|
||||
|---|---|---|
|
||||
| `config/config.json` | Paramètres XMPP, MQTT, Ollama | Utilisateur |
|
||||
| `config/agents_registry.json` | Registre des agents (auto-mis à jour) | Agent1 au démarrage des agents |
|
||||
| `config/blackout_hours.json` | Plages horaires d'inactivité totale | **Utilisateur directement** |
|
||||
| `config/reports_schedule.json` | Horaires de sollicitation des rapports | Agent1 (avec confirmation) |
|
||||
| `config/tasks_schedule.json` | Tâches planifiées par agent | Agent1 (avec confirmation) |
|
||||
|
||||
### `blackout_hours.json`
|
||||
Aucun agent ne travaille pendant ces plages, quelle que soit la planification :
|
||||
```json
|
||||
[
|
||||
{ "start": "02:00", "end": "05:00", "label": "maintenance nuit", "enabled": true }
|
||||
]
|
||||
```
|
||||
|
||||
### `reports_schedule.json`
|
||||
```json
|
||||
{
|
||||
"agents": {
|
||||
"agent2_debian13": { "report_time": "22:00", "enabled": true },
|
||||
"agent2_ansible": { "report_time": "22:05", "enabled": true },
|
||||
"agent2_deploy": { "report_time": "22:10", "enabled": true }
|
||||
},
|
||||
"daily_report_time": "22:30",
|
||||
"daily_report_enabled": true
|
||||
}
|
||||
```
|
||||
|
||||
### `tasks_schedule.json`
|
||||
```json
|
||||
{
|
||||
"tasks": [
|
||||
{
|
||||
"agent": "agent2_debian13",
|
||||
"task": "apt update && apt upgrade -y",
|
||||
"cron": "weekly lun 03:00",
|
||||
"enabled": true
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### `agents_registry.json` — champ `work_hours`
|
||||
Plage horaire de travail par agent (vérifiée avant chaque DELEGATE) :
|
||||
```json
|
||||
"work_hours": {
|
||||
"start": "07:00",
|
||||
"end": "23:00",
|
||||
"days": ["mon","tue","wed","thu","fri","sat","sun"],
|
||||
"enabled": true
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Topics MQTT
|
||||
|
||||
| Topic | Direction | Rôle |
|
||||
|---|---|---|
|
||||
| `agents/agent1/inbox` | CLI / agents → agent1 | Tâches pour agent1 |
|
||||
| `agents/<name>/inbox` | Agent1 → agent2_* | Tâches pour les agents |
|
||||
| `agents/<name>/control` | Agent1 → agent2_* | Pause / Resume / Report |
|
||||
| `agents/daily_report` | Agent2_* → agent1 | Rapports journaliers |
|
||||
| `agents/errors` | Agent2_* → agent1 | Erreurs (notif XMPP) |
|
||||
| `agents/register` | Agent2_* → agent1 | Enregistrement au démarrage |
|
||||
| `agents/status/<name>` | Agent2_* → agent1 | Statut en ligne / hors ligne (LWT) |
|
||||
| `agents/cli/outbox` | Agent1 → CLI | Réponses vers la CLI |
|
||||
| `agents/scheduler/notifications` | Scheduler → agent1 | Notifications planificateur |
|
||||
|
||||
---
|
||||
|
||||
## Lancer les agents
|
||||
|
||||
```bash
|
||||
# Agent1
|
||||
python3 /opt/agent/agent1.py > /tmp/agent1.log 2>&1 &
|
||||
|
||||
# Agents spécialisés
|
||||
python3 /opt/agent2_debian13/agent2_debian13.py > /tmp/agent2_debian13.log 2>&1 &
|
||||
python3 /opt/agent2_ansible/agent2_ansible.py > /tmp/agent2_ansible.log 2>&1 &
|
||||
python3 /opt/agent2_deploy/agent2_deploy.py > /tmp/agent2_deploy.log 2>&1 &
|
||||
|
||||
# Via systemd
|
||||
systemctl start agent agent2_debian13 agent2_ansible agent2_deploy
|
||||
```
|
||||
|
||||
## Logs
|
||||
|
||||
```bash
|
||||
journalctl -u agent -f
|
||||
journalctl -u agent2_debian13 -f
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Déploiement manuel
|
||||
|
||||
```bash
|
||||
# Prérequis
|
||||
apt-get install -y python3 python3-pip python3-venv git mosquitto
|
||||
|
||||
# Cloner et installer
|
||||
git clone https://git.piaf.im/sylvain/agent1.git /opt/agent
|
||||
cd /opt/agent
|
||||
python3 -m venv venv
|
||||
@@ -44,35 +227,5 @@ cp config/config.json.example config/config.json # adapter les valeurs
|
||||
|
||||
# Service systemd
|
||||
cp agent.service /etc/systemd/system/
|
||||
systemctl daemon-reload
|
||||
systemctl enable agent
|
||||
systemctl start agent
|
||||
```
|
||||
|
||||
## Configuration (`config/config.json`)
|
||||
|
||||
```json
|
||||
{
|
||||
"ollama_url" : "http://<ip_ollama>:11434/api/chat",
|
||||
"model" : "qwen3:8b",
|
||||
"xmpp_jid" : "agent1@xmpp.ovh",
|
||||
"xmpp_pass" : "<mot_de_passe>",
|
||||
"admin_jid" : "sylvain@xmpp.ovh",
|
||||
"mqtt_host" : "localhost",
|
||||
"mqtt_port" : 1883
|
||||
}
|
||||
```
|
||||
|
||||
## CLI
|
||||
|
||||
```bash
|
||||
python3 /opt/agent/cli.py # parler à agent1
|
||||
python3 /opt/agent/cli.py agent2_debian13 # accès direct à un agent
|
||||
python3 /opt/agent/cli.py --plans # voir les planifications
|
||||
```
|
||||
|
||||
## Logs
|
||||
|
||||
```bash
|
||||
journalctl -u agent -f
|
||||
systemctl daemon-reload && systemctl enable agent && systemctl start agent
|
||||
```
|
||||
|
||||
@@ -3,81 +3,61 @@
|
||||
## 1. Fichiers de configuration (dans `/opt/agent/config/`)
|
||||
|
||||
### `reports_schedule.json` _(géré par agent1 avec confirmation utilisateur)_
|
||||
- [ ] Créer le fichier avec horaires de sollicitation des rapports par agent
|
||||
- [ ] Agent1 : commande XMPP pour modifier ce fichier (confirmation avant écriture)
|
||||
- [ ] Agent1 : planifier les demandes de rapport selon ce fichier (APScheduler)
|
||||
- [ ] Chaque agent2_* : répondre à la demande de rapport avec ses stats locales
|
||||
- [x] Créer le fichier avec horaires de sollicitation des rapports par agent
|
||||
- [x] Agent1 : commande !reports pour afficher, PENDING_CONFIG pour confirmation modification
|
||||
- [x] Agent1 : planifier les demandes de rapport selon ce fichier (APScheduler)
|
||||
- [x] Chaque agent2_* : répondre à la demande de rapport avec ses stats locales
|
||||
|
||||
### `tasks_schedule.json` _(géré par agent1 avec confirmation utilisateur)_
|
||||
- [ ] Créer le fichier avec les tâches planifiées par agent
|
||||
- [ ] Agent1 : commande XMPP pour ajouter/modifier/supprimer une tâche (confirmation avant écriture)
|
||||
- [ ] Agent1 : charger ce fichier au démarrage et planifier les tâches via APScheduler
|
||||
- [ ] Remplacer / compléter l'actuel `SCHEDULE:` skill
|
||||
- [x] Créer le fichier avec les tâches planifiées par agent
|
||||
- [x] Agent1 : !tasks pour afficher, PENDING_CONFIG pour modification avec confirmation
|
||||
- [x] Agent1 : charger ce fichier au démarrage et planifier les tâches via APScheduler
|
||||
|
||||
### `blackout_hours.json` _(édité directement par l'utilisateur)_
|
||||
- [ ] Créer le fichier avec plage(s) horaire(s) de blackout (aucun agent ne travaille)
|
||||
- [ ] Format : `[{"start": "02:00", "end": "05:00", "label": "maintenance"}]`
|
||||
- [ ] Agent1 : vérifier ce fichier avant chaque délégation de tâche
|
||||
- [ ] Agent1 : vérifier ce fichier avant chaque tâche planifiée
|
||||
- [x] Créer le fichier avec plage(s) horaire(s) de blackout (aucun agent ne travaille)
|
||||
- [x] Agent1 : vérifier ce fichier avant chaque délégation de tâche (delegate.py)
|
||||
|
||||
---
|
||||
|
||||
## 2. Plages horaires par agent (`work_hours` dans `agents_registry.json`)
|
||||
- [ ] Ajouter `work_hours: {start, end, days}` pour chaque agent dans `agents_registry.json`
|
||||
- [ ] Modifier `skills/delegate.py` : vérifier plage horaire avant d'envoyer la tâche
|
||||
- [ ] Si hors plage → retourner message d'indisponibilité (pas d'exécution)
|
||||
- [ ] Prendre en compte le blackout_hours.json également dans delegate.py
|
||||
- [x] Ajouter `work_hours: {start, end, days}` pour chaque agent dans `agents_registry.json`
|
||||
- [x] Modifier `skills/delegate.py` : vérifier plage horaire avant d'envoyer la tâche
|
||||
- [x] Si hors plage → retourner message d'indisponibilité (pas d'exécution)
|
||||
- [x] Prendre en compte le blackout_hours.json également dans delegate.py
|
||||
|
||||
---
|
||||
|
||||
## 3. File d'attente locale par agent (SQLite)
|
||||
|
||||
### Pour chaque agent2_* (`/opt/agent2_*/queue.db`)
|
||||
- [ ] Créer table `tasks_queue(id, received_at, started_at, completed_at, task, status, result, duration_s)`
|
||||
- [ ] `on_mqtt_message()` : sauvegarder immédiatement le message en base (status: `pending`)
|
||||
- [ ] Worker FIFO dans un thread séparé : traiter les tâches une par une
|
||||
- [ ] Si paused : worker s'arrête, tâches s'accumulent en base
|
||||
- [ ] Au resume : worker reprend depuis les tâches `pending`
|
||||
- [ ] MQTT : passer à `clean_session=False` + `QoS=1` pour ne pas perdre les messages offline
|
||||
- [x] Créer table `tasks_queue(id, received_at, started_at, completed_at, task, status, result, duration_s)`
|
||||
- [x] `on_mqtt_message()` : sauvegarder immédiatement le message en base (status: `pending`)
|
||||
- [x] Worker FIFO dans un thread séparé : traiter les tâches une par une
|
||||
- [x] Si paused : worker s'arrête, tâches s'accumulent en base
|
||||
- [x] Au resume : worker reprend depuis les tâches `pending`
|
||||
- [x] MQTT : passer à `clean_session=False` + `QoS=1` pour ne pas perdre les messages offline
|
||||
|
||||
---
|
||||
|
||||
## 4. Mode pause / veille par agent
|
||||
|
||||
### Nouveau topic MQTT : `agents/<name>/control`
|
||||
- [ ] Chaque agent2_* : s'abonner à `agents/<name>/control`
|
||||
- [ ] Payload `{"command": "pause"}` → flag `self.paused = True`, stopper le worker
|
||||
- [ ] Payload `{"command": "resume"}` → flag `self.paused = False`, relancer le worker
|
||||
- [ ] Agent1 en veille : rester connecté XMPP, n'accepter que `!agentsON` / `!agentON agent1`
|
||||
### Topic MQTT : `agents/<name>/control`
|
||||
- [x] Chaque agent2_* : s'abonner à `agents/<name>/control`
|
||||
- [x] pause → worker stoppé, resume → worker relancé
|
||||
- [x] Agent1 en veille : n'accepte que `!agentsON` / `!agentON agent1`
|
||||
|
||||
### Commandes XMPP (sylvain → agent1)
|
||||
- [ ] `!agentOFF <nom>` → envoyer pause à l'agent ciblé
|
||||
- [ ] `!agentON <nom>` → envoyer resume à l'agent ciblé
|
||||
- [ ] `!agentsOFF` → agent1 en veille + pause à tous les agent2_*
|
||||
- [ ] `!agentsON` → agent1 sort de veille + resume à tous les agent2_*
|
||||
- [ ] `!agentOFF agent1` → agent1 en veille uniquement
|
||||
- [ ] Mettre à jour `agents_online.json` à chaque changement d'état
|
||||
- [x] `!agentOFF <nom>` / `!agentON <nom>`
|
||||
- [x] `!agentsOFF` / `!agentsON`
|
||||
- [x] `!agentOFF agent1` / `!agentON agent1` (veille agent1 uniquement)
|
||||
|
||||
---
|
||||
|
||||
## 5. Rapports journaliers
|
||||
|
||||
### Chaque agent2_* (stats locales)
|
||||
- [ ] Tracker en mémoire : `tasks_total`, `tasks_success`, `tasks_error`, `avg_duration_s`, `uptime_s`, `last_error`
|
||||
- [ ] Alimenter ces stats depuis la `queue.db`
|
||||
- [ ] Répondre à la demande de rapport d'agent1 via MQTT (`agents/<name>/control` payload `{"command": "report"}`)
|
||||
- [ ] Publier le rapport sur `agents/daily_report` avec toutes les stats
|
||||
|
||||
### Agent1 (compilation)
|
||||
- [ ] S'abonner à `agents/daily_report`
|
||||
- [ ] Stocker les rapports reçus en mémoire + SQLite (`daily_reports` table dans `executions.db`)
|
||||
- [ ] Nouveau skill `skills/daily_report.py` : trigger `DAILY_REPORT:`
|
||||
- [ ] Compiler les rapports de tous les agents
|
||||
- [ ] Formater un résumé lisible
|
||||
- [ ] Envoyer à `sylvain@xmpp.ovh` via XMPP
|
||||
- [ ] Stocker pour historique
|
||||
- [ ] Planifier `DAILY_REPORT:` dans `tasks_schedule.json` (ex: 22:30)
|
||||
- [ ] Disponible aussi à la demande : `DAILY_REPORT:` depuis CLI
|
||||
- [x] Chaque agent2_* : stats depuis queue.db, envoi sur `agents/daily_report`
|
||||
- [x] Agent1 : souscription `agents/daily_report`, stockage en mémoire
|
||||
- [x] `skills/daily_report.py` : DAILY_REPORT: [agent]
|
||||
- [x] APScheduler : sollicitation agents à 22:00/22:05/22:10, rapport journalier à 22:30
|
||||
|
||||
---
|
||||
|
||||
@@ -92,6 +72,58 @@
|
||||
|
||||
---
|
||||
|
||||
---
|
||||
|
||||
## 6. Gestion des mises à jour depuis les dépôts git
|
||||
|
||||
### Commandes XMPP (sylvain → agent1)
|
||||
|
||||
| Commande | Effet |
|
||||
|---|---|
|
||||
| `!agentUPDATE <nom>` | Vérifie si une mise à jour est disponible sur le dépôt git de l'agent (git fetch + comparaison) |
|
||||
| `!agentsUPDATE` | Vérifie les dépôts de tous les agents enregistrés |
|
||||
| `!agentUPGRADE <nom>` | Tire les modifications (git pull) + redémarre le service systemd de l'agent |
|
||||
| `!agentsUPGRADE` | Git pull + restart de tous les agents |
|
||||
|
||||
### Comportement attendu
|
||||
|
||||
- `!agentUPDATE` : affiche le résultat de `git fetch` + `git log HEAD..origin/main --oneline`
|
||||
- Réponse : "Mise à jour disponible : X commit(s)" ou "Déjà à jour"
|
||||
- `!agentUPGRADE` :
|
||||
1. `git pull origin main` dans le répertoire de l'agent
|
||||
2. `systemctl restart <service_name>` pour relancer le service
|
||||
3. Confirmation ou erreur envoyée via XMPP
|
||||
4. Attendre que le service soit de nouveau EN LIGNE (statut MQTT) avant de confirmer
|
||||
|
||||
### Informations nécessaires par agent
|
||||
|
||||
Ajouter dans `agents_registry.json` :
|
||||
```json
|
||||
"agent2_debian13": {
|
||||
"install_path" : "/opt/agent2_debian13",
|
||||
"service_name" : "agent2_debian13",
|
||||
"git_branch" : "main"
|
||||
}
|
||||
```
|
||||
|
||||
### Fichiers à modifier / créer
|
||||
|
||||
| Fichier | Action |
|
||||
|---|---|
|
||||
| `config/agents_registry.json` | Ajouter `install_path`, `service_name`, `git_branch` par agent |
|
||||
| `agent1.py` | Gérer `!agentUPDATE/UPGRADE` et `!agentsUPDATE/UPGRADE` |
|
||||
| `skills/agent_update.py` | **Créer** — logique git fetch/pull + systemctl restart |
|
||||
|
||||
### Points d'attention
|
||||
|
||||
- [x] Exécuter `git` et `systemctl` via subprocess
|
||||
- [x] Timeout sur les commandes git/systemctl
|
||||
- [x] `!agentUPGRADE agent1` : git pull + réponse XMPP envoyée avant `systemctl restart agent`
|
||||
- [x] Gérer le cas où `install_path` n'est pas dans le registre
|
||||
- [x] `!agentUPDATE` suggère `!agentUPGRADE` si commits disponibles
|
||||
|
||||
---
|
||||
|
||||
## Fichiers impactés
|
||||
|
||||
| Fichier | Action |
|
||||
|
||||
@@ -137,10 +137,11 @@ def _get_all_agents() -> list:
|
||||
return []
|
||||
|
||||
# ── COMMANDES !agent ──────────────────────────────────────────────────────
|
||||
def _handle_agent_command(text: str) -> str | None:
|
||||
def _handle_agent_command(text: str) -> tuple:
|
||||
"""
|
||||
Gère les commandes !agentON/OFF et !agentsON/OFF.
|
||||
Retourne la réponse ou None si ce n'est pas une commande agent.
|
||||
Gère toutes les commandes !agent*.
|
||||
Retourne (handled: bool, reply: str|None).
|
||||
reply=None signifie que la réponse XMPP a déjà été envoyée (ex: self-upgrade).
|
||||
"""
|
||||
global SLEEP_MODE
|
||||
|
||||
@@ -152,7 +153,7 @@ def _handle_agent_command(text: str) -> str | None:
|
||||
for a in agents:
|
||||
_send_control(a, "pause")
|
||||
SLEEP_MODE = True
|
||||
return "[VEILLE] Agent1 en veille. {} agent(s) mis en pause.\nEnvoyez !agentsON ou !agentON agent1 pour reprendre.".format(len(agents))
|
||||
return True, "[VEILLE] Agent1 en veille. {} agent(s) mis en pause.\nEnvoyez !agentsON ou !agentON agent1 pour reprendre.".format(len(agents))
|
||||
|
||||
# !agentsON — sortie veille agent1 + resume tous les agents
|
||||
if t == "!agentsON":
|
||||
@@ -160,27 +161,145 @@ def _handle_agent_command(text: str) -> str | None:
|
||||
for a in agents:
|
||||
_send_control(a, "resume")
|
||||
SLEEP_MODE = False
|
||||
return "[ACTIF] Agent1 actif. {} agent(s) relancés.".format(len(agents))
|
||||
return True, "[ACTIF] Agent1 actif. {} agent(s) relancés.".format(len(agents))
|
||||
|
||||
# !agentOFF <nom>
|
||||
if t.startswith("!agentOFF "):
|
||||
name = t[len("!agentOFF "):].strip()
|
||||
if name == "agent1":
|
||||
SLEEP_MODE = True
|
||||
return "[VEILLE] Agent1 en veille. Envoyez !agentON agent1 pour reprendre."
|
||||
return True, "[VEILLE] Agent1 en veille. Envoyez !agentON agent1 pour reprendre."
|
||||
_send_control(name, "pause")
|
||||
return "[PAUSE] Commande pause envoyée à {}.".format(name)
|
||||
return True, "[PAUSE] Commande pause envoyée à {}.".format(name)
|
||||
|
||||
# !agentON <nom>
|
||||
if t.startswith("!agentON "):
|
||||
name = t[len("!agentON "):].strip()
|
||||
if name == "agent1":
|
||||
SLEEP_MODE = False
|
||||
return "[ACTIF] Agent1 actif."
|
||||
return True, "[ACTIF] Agent1 actif."
|
||||
_send_control(name, "resume")
|
||||
return "[ACTIF] Commande resume envoyée à {}.".format(name)
|
||||
return True, "[ACTIF] Commande resume envoyée à {}.".format(name)
|
||||
|
||||
return None
|
||||
# !agentsUPDATE — vérifier les mises à jour de tous les agents
|
||||
if t == "!agentsUPDATE":
|
||||
return True, _handle_update_all()
|
||||
|
||||
# !agentUPDATE <nom>
|
||||
if t.startswith("!agentUPDATE "):
|
||||
name = t[len("!agentUPDATE "):].strip()
|
||||
return True, _handle_update_one(name)
|
||||
|
||||
# !agentsUPGRADE — mettre à jour tous les agents
|
||||
if t == "!agentsUPGRADE":
|
||||
return True, _handle_upgrade_all()
|
||||
|
||||
# !agentUPGRADE <nom>
|
||||
if t.startswith("!agentUPGRADE "):
|
||||
name = t[len("!agentUPGRADE "):].strip()
|
||||
return True, _handle_upgrade_one(name)
|
||||
|
||||
return False, None
|
||||
|
||||
# ── MISE À JOUR DEPUIS GIT ───────────────────────────────────────────────
|
||||
def _get_agent_git_info(name: str) -> dict:
|
||||
"""
|
||||
Retourne {install_path, service_name, git_branch} pour un agent.
|
||||
Priorité : registre → convention /opt/<nom> (fallback automatique).
|
||||
"""
|
||||
try:
|
||||
registry = json.loads(REGISTRY_FILE.read_text(encoding="utf-8"))
|
||||
agent = registry.get(name, {})
|
||||
except Exception:
|
||||
agent = {}
|
||||
|
||||
# Fallback : déduire depuis le nom de l'agent (convention /opt/<nom>)
|
||||
# agent1 est un cas particulier : /opt/agent, service "agent"
|
||||
if name == "agent1":
|
||||
default_path = "/opt/agent"
|
||||
default_service = "agent"
|
||||
else:
|
||||
default_path = "/opt/{}".format(name)
|
||||
default_service = name
|
||||
|
||||
return {
|
||||
"install_path": agent.get("install_path", default_path),
|
||||
"service_name": agent.get("service_name", default_service),
|
||||
"git_branch" : agent.get("git_branch", "main"),
|
||||
"ssh_host" : agent.get("ssh_host"),
|
||||
"ssh_user" : agent.get("ssh_user", "root"),
|
||||
}
|
||||
|
||||
def _handle_update_one(name: str) -> str:
|
||||
from skills.agent_update import check_update
|
||||
info = _get_agent_git_info(name)
|
||||
return check_update(name, info["install_path"], info["git_branch"],
|
||||
info["ssh_host"], info["ssh_user"])
|
||||
|
||||
def _handle_update_all() -> str:
|
||||
from skills.agent_update import check_update
|
||||
try:
|
||||
registry = json.loads(REGISTRY_FILE.read_text(encoding="utf-8"))
|
||||
except Exception:
|
||||
return "Erreur lecture registre."
|
||||
results = []
|
||||
for name in registry:
|
||||
info = _get_agent_git_info(name)
|
||||
results.append(check_update(name, info["install_path"], info["git_branch"],
|
||||
info["ssh_host"], info["ssh_user"]))
|
||||
return "\n\n".join(results) if results else "Aucun agent dans le registre."
|
||||
|
||||
def _handle_upgrade_one(name: str) -> str:
|
||||
from skills.agent_update import do_upgrade
|
||||
info = _get_agent_git_info(name)
|
||||
self_upgrade = (name == "agent1")
|
||||
msg = do_upgrade(name, info["install_path"], info["service_name"],
|
||||
info["git_branch"], self_upgrade=self_upgrade,
|
||||
ssh_host=info["ssh_host"], ssh_user=info["ssh_user"])
|
||||
if self_upgrade and "Redémarrage en cours" in msg:
|
||||
# Envoyer le message XMPP avant le restart
|
||||
if xmpp_bot:
|
||||
xmpp_bot.send_message(mto=ADMIN_JID, mbody=msg, mtype='chat')
|
||||
import subprocess
|
||||
subprocess.Popen(["systemctl", "restart", info["service_name"]])
|
||||
return None # Réponse déjà envoyée manuellement
|
||||
return msg
|
||||
|
||||
def _handle_upgrade_all() -> str:
|
||||
from skills.agent_update import do_upgrade
|
||||
try:
|
||||
registry = json.loads(REGISTRY_FILE.read_text(encoding="utf-8"))
|
||||
except Exception:
|
||||
return "Erreur lecture registre."
|
||||
|
||||
results = []
|
||||
agent1_info = None
|
||||
|
||||
for name in registry:
|
||||
info = _get_agent_git_info(name)
|
||||
if name == "agent1":
|
||||
agent1_info = (name, info) # traiter en dernier
|
||||
continue
|
||||
msg = do_upgrade(name, info["install_path"],
|
||||
info["service_name"], info["git_branch"],
|
||||
ssh_host=info["ssh_host"], ssh_user=info["ssh_user"])
|
||||
results.append(msg)
|
||||
|
||||
summary = "\n\n".join(results) if results else "Aucun agent mis à jour."
|
||||
|
||||
if agent1_info:
|
||||
name, info = agent1_info
|
||||
pull_msg = do_upgrade(name, info["install_path"],
|
||||
info["service_name"], info["git_branch"],
|
||||
self_upgrade=True)
|
||||
summary += "\n\n" + pull_msg
|
||||
if xmpp_bot:
|
||||
xmpp_bot.send_message(mto=ADMIN_JID, mbody=summary, mtype='chat')
|
||||
import subprocess
|
||||
subprocess.Popen(["systemctl", "restart", info.get("service_name", "agent")])
|
||||
return None # Réponse déjà envoyée
|
||||
|
||||
return summary
|
||||
|
||||
# ── GESTION CONFIGS AVEC CONFIRMATION ────────────────────────────────────
|
||||
def _handle_config_command(text: str) -> str | None:
|
||||
@@ -495,11 +614,12 @@ class AgentBot(ClientXMPP):
|
||||
|
||||
user_input = msg['body'].strip()
|
||||
|
||||
# ── Commandes !agentON/OFF (prioritaires, toujours traitées) ──────
|
||||
agent_reply = _handle_agent_command(user_input)
|
||||
if agent_reply is not None:
|
||||
self.send_message(mto=ADMIN_JID, mbody=agent_reply, mtype='chat')
|
||||
return
|
||||
# ── Commandes !agent* (prioritaires, toujours traitées) ──────────
|
||||
handled, agent_reply = _handle_agent_command(user_input)
|
||||
if handled:
|
||||
if agent_reply is not None:
|
||||
self.send_message(mto=ADMIN_JID, mbody=agent_reply, mtype='chat')
|
||||
return # None = réponse déjà envoyée manuellement (ex: self-upgrade)
|
||||
|
||||
# ── Mode veille : ignorer tout sauf commandes agent ───────────────
|
||||
if SLEEP_MODE:
|
||||
|
||||
@@ -4,26 +4,86 @@
|
||||
"mqtt_inbox": "agents/agent2_debian13/inbox",
|
||||
"mqtt_outbox": "agents/agent1/inbox",
|
||||
"speciality": "Administration Debian : apt, dpkg, systemd, conteneurs LXC/Docker, KVM, réseau, sécurité système",
|
||||
"work_hours": { "start": "07:00", "end": "23:00", "days": ["mon","tue","wed","thu","fri","sat","sun"], "enabled": true }
|
||||
"work_hours": {
|
||||
"start": "07:00",
|
||||
"end": "23:00",
|
||||
"days": [
|
||||
"mon",
|
||||
"tue",
|
||||
"wed",
|
||||
"thu",
|
||||
"fri",
|
||||
"sat",
|
||||
"sun"
|
||||
],
|
||||
"enabled": true
|
||||
},
|
||||
"install_path": "/opt/agent2_debian13",
|
||||
"service_name": "agent2_debian13",
|
||||
"git_branch": "main"
|
||||
},
|
||||
"agent2_ansible": {
|
||||
"jid": "agent2_ansible@xmpp.ovh",
|
||||
"mqtt_inbox": "agents/agent2_ansible/inbox",
|
||||
"mqtt_outbox": "agents/agent1/inbox",
|
||||
"speciality": "Automatisation infrastructure via Ansible : playbooks, commandes ad-hoc, déploiement multi-hôtes, gestion de configuration sur le réseau local",
|
||||
"work_hours": { "start": "07:00", "end": "23:00", "days": ["mon","tue","wed","thu","fri","sat","sun"], "enabled": true }
|
||||
"work_hours": {
|
||||
"start": "07:00",
|
||||
"end": "23:00",
|
||||
"days": [
|
||||
"mon",
|
||||
"tue",
|
||||
"wed",
|
||||
"thu",
|
||||
"fri",
|
||||
"sat",
|
||||
"sun"
|
||||
],
|
||||
"enabled": true
|
||||
},
|
||||
"install_path": "/opt/agent2_ansible",
|
||||
"service_name": "agent2_ansible",
|
||||
"git_branch": "main"
|
||||
},
|
||||
"agent2_deploy": {
|
||||
"jid": "agent2_deploy@xmpp.ovh",
|
||||
"mqtt_inbox": "agents/agent2_deploy/inbox",
|
||||
"mqtt_outbox": "agents/agent1/inbox",
|
||||
"speciality": "Déploiement d'agents : installe et configure d'autres agents sur des machines distantes ou locales via SSH",
|
||||
"work_hours": { "start": "08:00", "end": "20:00", "days": ["mon","tue","wed","thu","fri"], "enabled": true }
|
||||
"work_hours": {
|
||||
"start": "08:00",
|
||||
"end": "20:00",
|
||||
"days": [
|
||||
"mon",
|
||||
"tue",
|
||||
"wed",
|
||||
"thu",
|
||||
"fri"
|
||||
],
|
||||
"enabled": true
|
||||
},
|
||||
"install_path": "/opt/agent2_deploy",
|
||||
"service_name": "agent2_deploy",
|
||||
"git_branch": "main"
|
||||
},
|
||||
"agent2_test": {
|
||||
"jid": "agent2_test@xmpp.ovh",
|
||||
"mqtt_inbox": "agents/agent2_test/inbox",
|
||||
"mqtt_outbox": "agents/agent1/inbox",
|
||||
"speciality": "Spécialiste Debian : apt, systemd, conteneurs LXC/Docker, KVM, réseau, sécurité"
|
||||
"speciality": "Administration Debian : apt, dpkg, systemd, conteneurs LXC/Docker, KVM, réseau, sécurité système",
|
||||
"install_path": "/opt/agent2_test",
|
||||
"service_name": "agent2_test",
|
||||
"git_branch": "main",
|
||||
"ssh_host": "192.168.7.13",
|
||||
"ssh_user": "root"
|
||||
},
|
||||
"agent1": {
|
||||
"jid": "agent1@xmpp.ovh",
|
||||
"mqtt_inbox": "agents/agent1/inbox",
|
||||
"mqtt_outbox": "agents/agent1/inbox",
|
||||
"speciality": "Orchestrateur principal",
|
||||
"install_path": "/opt/agent",
|
||||
"service_name": "agent",
|
||||
"git_branch": "main"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
"""
|
||||
Utilitaire : vérification et application des mises à jour git pour les agents.
|
||||
|
||||
Fonctions appelées directement depuis agent1.py (pas de trigger LLM).
|
||||
|
||||
check_update(name, install_path, branch, ssh_host, ssh_user)
|
||||
do_upgrade(name, install_path, service, branch, ssh_host, ssh_user, self_upgrade)
|
||||
|
||||
Si ssh_host est défini, les commandes sont exécutées via SSH sur la machine distante.
|
||||
"""
|
||||
import subprocess
|
||||
import shlex
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def _run_local(cmd: str, cwd: str = None, timeout: int = 30) -> tuple:
|
||||
"""Lance une commande locale, retourne (stdout, stderr, returncode)."""
|
||||
try:
|
||||
result = subprocess.run(
|
||||
shlex.split(cmd),
|
||||
cwd=cwd,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=timeout
|
||||
)
|
||||
return result.stdout.strip(), result.stderr.strip(), result.returncode
|
||||
except subprocess.TimeoutExpired:
|
||||
return "", "Timeout ({} s)".format(timeout), -1
|
||||
except Exception as e:
|
||||
return "", str(e), -1
|
||||
|
||||
|
||||
def _run_ssh(cmd: str, ssh_host: str, ssh_user: str = "root",
|
||||
cwd: str = None, timeout: int = 30) -> tuple:
|
||||
"""Lance une commande via SSH, retourne (stdout, stderr, returncode)."""
|
||||
if cwd:
|
||||
cmd = "cd {} && {}".format(cwd, cmd)
|
||||
ssh_cmd = "sshpass -p 'Matador3721' ssh -o StrictHostKeyChecking=no -o ConnectTimeout=10 {}@{} {}".format(
|
||||
ssh_user, ssh_host, shlex.quote(cmd))
|
||||
try:
|
||||
result = subprocess.run(
|
||||
shlex.split(ssh_cmd),
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=timeout
|
||||
)
|
||||
return result.stdout.strip(), result.stderr.strip(), result.returncode
|
||||
except subprocess.TimeoutExpired:
|
||||
return "", "Timeout SSH ({} s)".format(timeout), -1
|
||||
except Exception as e:
|
||||
return "", str(e), -1
|
||||
|
||||
|
||||
def _run(cmd: str, cwd: str = None, timeout: int = 30,
|
||||
ssh_host: str = None, ssh_user: str = "root") -> tuple:
|
||||
"""Dispatche local ou SSH selon ssh_host."""
|
||||
if ssh_host:
|
||||
return _run_ssh(cmd, ssh_host, ssh_user, cwd=cwd, timeout=timeout)
|
||||
return _run_local(cmd, cwd=cwd, timeout=timeout)
|
||||
|
||||
|
||||
def check_update(agent_name: str, install_path: str, branch: str = "main",
|
||||
ssh_host: str = None, ssh_user: str = "root") -> str:
|
||||
"""
|
||||
Vérifie si une mise à jour est disponible sur le dépôt distant.
|
||||
Effectue un git fetch puis compare HEAD avec origin/<branch>.
|
||||
"""
|
||||
remote = " [{}]".format(ssh_host) if ssh_host else ""
|
||||
|
||||
# Vérifier que le répertoire existe
|
||||
if ssh_host:
|
||||
out, _, rc = _run("test -d {}".format(install_path),
|
||||
ssh_host=ssh_host, ssh_user=ssh_user)
|
||||
if rc != 0:
|
||||
return "[{}{}] Répertoire introuvable : {}".format(
|
||||
agent_name, remote, install_path)
|
||||
else:
|
||||
if not Path(install_path).is_dir():
|
||||
return "[{}] Répertoire introuvable : {}".format(agent_name, install_path)
|
||||
|
||||
# git fetch
|
||||
out, err, rc = _run("git fetch origin {}".format(branch),
|
||||
cwd=install_path, timeout=20,
|
||||
ssh_host=ssh_host, ssh_user=ssh_user)
|
||||
if rc != 0:
|
||||
return "[{}{}] Erreur git fetch : {}".format(agent_name, remote, err or out)
|
||||
|
||||
# Compter les commits disponibles
|
||||
out, err, rc = _run(
|
||||
"git log HEAD..origin/{} --oneline".format(branch),
|
||||
cwd=install_path, ssh_host=ssh_host, ssh_user=ssh_user)
|
||||
if rc != 0:
|
||||
return "[{}{}] Erreur git log : {}".format(agent_name, remote, err or out)
|
||||
|
||||
commits = [l for l in out.splitlines() if l.strip()]
|
||||
if not commits:
|
||||
return "[{}{}] Déjà à jour.".format(agent_name, remote)
|
||||
|
||||
lines = ["[{}{}] {} commit(s) disponible(s) :".format(
|
||||
agent_name, remote, len(commits))]
|
||||
for c in commits[:10]:
|
||||
lines.append(" {}".format(c))
|
||||
if len(commits) > 10:
|
||||
lines.append(" ... et {} autre(s)".format(len(commits) - 10))
|
||||
lines.append("Lancez !agentUPGRADE {} pour appliquer.".format(agent_name))
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def do_upgrade(agent_name: str, install_path: str,
|
||||
service_name: str, branch: str = "main",
|
||||
self_upgrade: bool = False,
|
||||
ssh_host: str = None, ssh_user: str = "root") -> str:
|
||||
"""
|
||||
Applique la mise à jour : git pull + systemctl restart.
|
||||
Pour agent1 (self_upgrade=True), retourne le message AVANT le restart.
|
||||
"""
|
||||
remote = " [{}]".format(ssh_host) if ssh_host else ""
|
||||
|
||||
# Vérifier que le répertoire existe
|
||||
if ssh_host:
|
||||
_, _, rc = _run("test -d {}".format(install_path),
|
||||
ssh_host=ssh_host, ssh_user=ssh_user)
|
||||
if rc != 0:
|
||||
return "[{}{}] Répertoire introuvable : {}".format(
|
||||
agent_name, remote, install_path)
|
||||
else:
|
||||
if not Path(install_path).is_dir():
|
||||
return "[{}] Répertoire introuvable : {}".format(agent_name, install_path)
|
||||
|
||||
# git pull
|
||||
out, err, rc = _run(
|
||||
"git pull origin {}".format(branch),
|
||||
cwd=install_path, timeout=60,
|
||||
ssh_host=ssh_host, ssh_user=ssh_user)
|
||||
if rc != 0:
|
||||
return "[{}{}] Erreur git pull : {}".format(agent_name, remote, err or out)
|
||||
|
||||
pull_msg = out or "Déjà à jour."
|
||||
|
||||
if self_upgrade:
|
||||
return "[{}] {}\nRedémarrage en cours...".format(agent_name, pull_msg)
|
||||
|
||||
# systemctl restart
|
||||
_, err, rc = _run("systemctl restart {}".format(service_name),
|
||||
timeout=15, ssh_host=ssh_host, ssh_user=ssh_user)
|
||||
if rc != 0:
|
||||
return "[{}{}] Pull OK mais restart échoué : {}".format(
|
||||
agent_name, remote, err)
|
||||
|
||||
# Vérifier que le service est bien remonté
|
||||
out, _, _ = _run("systemctl is-active {}".format(service_name),
|
||||
timeout=5, ssh_host=ssh_host, ssh_user=ssh_user)
|
||||
state = out.strip()
|
||||
if state == "active":
|
||||
return "[{}{}] Mise à jour appliquée. Service actif.\n{}".format(
|
||||
agent_name, remote, pull_msg)
|
||||
else:
|
||||
return "[{}{}] Pull OK, service état : {}. Vérifiez les logs.".format(
|
||||
agent_name, remote, state)
|
||||
Reference in New Issue
Block a user