Ajoute le skill MountyHall (lecture + confirmation avant action)
Scripts Python (requests + BeautifulSoup) pour un agent LLM local (Hermes-agent) : lecture structuree du lieu du troll (description + formulaires d'action disponibles), et soumission d'une action precise uniquement apres confirmation explicite. Corrige le probleme observe avec un agent 9B qui ne detectait pas les boutons d'action en lisant le HTML brut.
This commit is contained in:
+114
@@ -0,0 +1,114 @@
|
||||
# Skill "MountyHall" pour Hermes-agent
|
||||
|
||||
Ce skill apprend à un agent LLM (Hermes, via LM Studio ou autre) à consulter et,
|
||||
sur confirmation uniquement, à agir sur le compte MountyHall de Perco.
|
||||
|
||||
Il corrige un problème observé avec un premier prompt "brut" (demander au modèle
|
||||
de lire le HTML lui-même) : selon comment l'agent lit un fichier, un outil de
|
||||
rendu/résumé HTML→texte peut **supprimer silencieusement les balises `<form>`
|
||||
et `<button>`**, rendant le modèle aveugle aux actions disponibles même s'il
|
||||
affirme avoir "tout lu". La solution ici : un vrai parsing HTML (BeautifulSoup)
|
||||
fait par du code déterministe, qui renvoie un JSON structuré que le modèle n'a
|
||||
plus qu'à interpréter — impossible de "rater" un formulaire.
|
||||
|
||||
---
|
||||
|
||||
## 0. Installation (une seule fois)
|
||||
|
||||
```bash
|
||||
cd skill-mh
|
||||
python3 -m venv .venv
|
||||
.venv/bin/pip install -r requirements.txt
|
||||
```
|
||||
|
||||
(Si `python3 -m venv` échoue faute de `pip` système, c'est normal — le venv
|
||||
embarque son propre pip, voir `requirements.txt`.)
|
||||
|
||||
---
|
||||
|
||||
## 1. Fichiers fournis
|
||||
|
||||
- `mh_common.py` — connexion (login) au compte MountyHall de Perco, renvoie une
|
||||
session `requests` authentifiée. Identifiants déjà configurés dedans.
|
||||
- `mh_lieu.py` — lit une page de jeu (par défaut : le lieu actuel du troll) et
|
||||
renvoie un **JSON structuré** : titre, description texte, et la liste de
|
||||
tous les formulaires d'action présents (avec leurs champs cachés et le
|
||||
libellé de leur bouton).
|
||||
- `mh_submit.py` — soumet **une** action précise, identifiée par un bout de
|
||||
texte de son bouton (ex: `"Traverser le Portail"`). Refait un login + une
|
||||
lecture fraîche de la page juste avant de soumettre, pour toujours utiliser
|
||||
un token anti-CSRF (`cp`) à jour.
|
||||
|
||||
---
|
||||
|
||||
## 2. Rôle de l'agent
|
||||
|
||||
Tu es un assistant qui aide Perco à suivre son troll sur MountyHall. Ton
|
||||
fonctionnement doit **toujours** suivre ce cycle :
|
||||
|
||||
### Étape A — Consulter
|
||||
|
||||
Exécute :
|
||||
```bash
|
||||
.venv/bin/python3 mh_lieu.py
|
||||
```
|
||||
(Tu peux passer `type`, `id`, `service` en arguments pour consulter une autre
|
||||
page que le lieu actuel — voir `Play_menu.php` dans le tuto général `../mh`
|
||||
pour la liste des types de pages.)
|
||||
|
||||
Tu reçois un JSON avec :
|
||||
- `titre` : nom/type du lieu
|
||||
- `description` : texte complet du lieu (coordonnées, contexte, objets, PNJ...)
|
||||
- `forms` : liste des actions disponibles, chacune avec ses `buttons` (au
|
||||
moins un `label` = le texte du bouton tel qu'affiché en jeu)
|
||||
|
||||
### Étape B — Rapporter à Perco
|
||||
|
||||
Résume-lui `titre` + `description` en français, clairement.
|
||||
|
||||
**Si `forms` n'est pas vide**, liste chaque bouton disponible (son `label`,
|
||||
qui contient en général le coût en PA entre parenthèses) et **demande
|
||||
explicitement** :
|
||||
> "Je vois le bouton **[label]** — veux-tu que je l'utilise ?"
|
||||
|
||||
**Si `forms` est vide**, dis-le simplement : aucune action n'est disponible
|
||||
sur cette page pour l'instant.
|
||||
|
||||
### Étape C — Attendre la confirmation
|
||||
|
||||
- Si Perco ne répond pas clairement "oui" (ou répond "non" / rien / autre
|
||||
chose) : **ne fais rien de plus**, n'exécute pas `mh_submit.py`.
|
||||
- Si Perco confirme explicitement "oui" pour un bouton précis : exécute
|
||||
```bash
|
||||
.venv/bin/python3 mh_submit.py "texte (ou début du texte) du bouton"
|
||||
```
|
||||
en utilisant un extrait du `label` exact vu à l'étape A (le script fait une
|
||||
recherche insensible à la casse, un sous-texte suffit, ex: `"Traverser"`).
|
||||
|
||||
### Étape D — Rapporter le résultat
|
||||
|
||||
`mh_submit.py` renvoie un JSON `{"resultat": "..."}` avec le texte de la page
|
||||
de résultat du jeu. Résume ce résultat à Perco en français.
|
||||
|
||||
---
|
||||
|
||||
## Règle absolue de sécurité
|
||||
|
||||
**Ne jamais exécuter `mh_submit.py` sans une confirmation explicite et récente
|
||||
de Perco pour CE bouton précis.** Une soumission = une action réelle en jeu
|
||||
(déplacement, combat, dépense de PA, etc.), potentiellement irréversible. En
|
||||
cas de doute sur ce que Perco a voulu dire, redemande plutôt que de supposer.
|
||||
|
||||
Ne jamais inventer un bouton ou une action qui n'apparaît pas dans le `forms`
|
||||
renvoyé par `mh_lieu.py` — la liste vient toujours d'une lecture fraîche du
|
||||
jeu, jamais de mémoire/supposition.
|
||||
|
||||
---
|
||||
|
||||
## Référence complémentaire
|
||||
|
||||
Le repo `mh` (projet compagnon MountyHall de Perco, plus large) contient
|
||||
`LLM_TUTORIAL.md` avec le détail des endpoints du jeu (Play_menu, Play2,
|
||||
sélection de cibles pour les sorts, table des `ai_ToDo` connus type
|
||||
Attaque/Vampirisme/etc.). Utile si Perco demande d'étendre ce skill à d'autres
|
||||
pages que "Lieu".
|
||||
@@ -0,0 +1,24 @@
|
||||
"""Connexion partagee au jeu MountyHall (compte de Perco)."""
|
||||
import requests
|
||||
|
||||
BASE = "https://mh2.mh.raistlin.fr/mountyhall/MH_Play"
|
||||
LOGIN = "Perco"
|
||||
PASSWORD = "taotd"
|
||||
|
||||
|
||||
def get_session():
|
||||
"""Se connecte et renvoie une requests.Session authentifiee (cookie PHPSESSID)."""
|
||||
s = requests.Session()
|
||||
s.headers.update({"User-Agent": "Mozilla/5.0"})
|
||||
r = s.post(
|
||||
f"{BASE}/PlayStart2.php",
|
||||
data={
|
||||
"as_Login": LOGIN,
|
||||
"as_Pwd": PASSWORD,
|
||||
"ai_design": "30",
|
||||
"as_Action": "login", # obligatoire, sinon le serveur reaffiche juste le formulaire
|
||||
},
|
||||
)
|
||||
if "Bienvenue" not in r.text:
|
||||
raise RuntimeError("Echec de connexion MountyHall - identifiants invalides ou serveur indisponible")
|
||||
return s
|
||||
@@ -0,0 +1,86 @@
|
||||
"""Lit et decrit le lieu actuel du troll, en extrayant proprement (via BeautifulSoup)
|
||||
la description ET les formulaires d'action disponibles.
|
||||
|
||||
Usage:
|
||||
python3 mh_lieu.py [type] [id] [service]
|
||||
|
||||
Par defaut : type=L id=-3 service=0 (lieu actuel du troll).
|
||||
|
||||
Sortie : un JSON structure sur stdout, avec les cles:
|
||||
- titre : titre du lieu (ex: "Portail : [4366144] Portail de Teleportation")
|
||||
- description : texte descriptif du lieu (hors menus/formulaires)
|
||||
- forms : liste des formulaires d'action detectes, chacun avec:
|
||||
- action : URL cible du formulaire
|
||||
- method : GET ou POST
|
||||
- hidden_fields : dict des champs caches (type, id, cp, ...)
|
||||
- buttons : liste des boutons soumission {name, value, label}
|
||||
|
||||
IMPORTANT pour l'agent qui utilise ce script : ne JAMAIS soumettre un des
|
||||
formulaires listes ici sans confirmation explicite de Perco. Chaque formulaire
|
||||
represente une action qui coute des Points d'Action (PA) ou a un effet en jeu.
|
||||
"""
|
||||
import json
|
||||
import sys
|
||||
|
||||
from bs4 import BeautifulSoup
|
||||
|
||||
from mh_common import BASE, get_session
|
||||
|
||||
|
||||
def fetch_lieu(session, type_="L", id_="-3", service="0"):
|
||||
r = session.get(
|
||||
f"{BASE}/Play_a_Action.php",
|
||||
params={"type": type_, "id": id_, "service": service},
|
||||
)
|
||||
return r.text
|
||||
|
||||
|
||||
def parse_lieu(html):
|
||||
# Titre + description : on part d'une copie du HTML dont on retire nav/forms/scripts
|
||||
soup = BeautifulSoup(html, "html.parser")
|
||||
main = soup.find("main") or soup
|
||||
|
||||
h2 = main.find("h2")
|
||||
titre = h2.get_text(strip=True) if h2 else None
|
||||
|
||||
for tag in main.find_all(["nav", "form", "script", "style"]):
|
||||
tag.decompose()
|
||||
description = main.get_text("\n", strip=True)
|
||||
|
||||
# Formulaires : sur une deuxieme copie intacte du HTML
|
||||
soup2 = BeautifulSoup(html, "html.parser")
|
||||
forms = []
|
||||
for form in soup2.find_all("form"):
|
||||
hidden = {
|
||||
inp.get("name"): inp.get("value", "")
|
||||
for inp in form.find_all("input", type="hidden")
|
||||
if inp.get("name")
|
||||
}
|
||||
buttons = []
|
||||
for btn in form.find_all(["button", "input"]):
|
||||
if btn.name == "button" or btn.get("type") == "submit":
|
||||
label = btn.get_text(strip=True) if btn.name == "button" else btn.get("value", "")
|
||||
buttons.append({
|
||||
"name": btn.get("name"),
|
||||
"value": btn.get("value", ""),
|
||||
"label": label,
|
||||
})
|
||||
forms.append({
|
||||
"action": form.get("action"),
|
||||
"method": (form.get("method") or "get").upper(),
|
||||
"hidden_fields": hidden,
|
||||
"buttons": buttons,
|
||||
})
|
||||
|
||||
return {"titre": titre, "description": description, "forms": forms}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
type_ = sys.argv[1] if len(sys.argv) > 1 else "L"
|
||||
id_ = sys.argv[2] if len(sys.argv) > 2 else "-3"
|
||||
service = sys.argv[3] if len(sys.argv) > 3 else "0"
|
||||
|
||||
session = get_session()
|
||||
html = fetch_lieu(session, type_, id_, service)
|
||||
data = parse_lieu(html)
|
||||
print(json.dumps(data, ensure_ascii=False, indent=2))
|
||||
@@ -0,0 +1,84 @@
|
||||
"""Soumet UNE action precise vue sur la page du lieu (ou toute autre page type/id),
|
||||
identifiee par un extrait du texte de son bouton.
|
||||
|
||||
Ce script refait un login + une lecture FRAICHE de la page avant de soumettre,
|
||||
pour toujours utiliser un token anti-CSRF (`cp`) a jour (il change a chaque
|
||||
chargement de page - reutiliser un ancien token fait echouer l'action).
|
||||
|
||||
Usage:
|
||||
python3 mh_submit.py "texte du bouton" [type] [id] [service]
|
||||
|
||||
Exemple:
|
||||
python3 mh_submit.py "Traverser le Portail"
|
||||
python3 mh_submit.py "Attaquer" A -6
|
||||
|
||||
REGLE ABSOLUE : ce script ne doit JAMAIS etre appele par l'agent sans que
|
||||
Perco ait dit explicitement "oui" a la question "veux-tu que j'utilise le
|
||||
bouton [X] ?". Une soumission = une action reelle en jeu (PA depenses,
|
||||
deplacement, combat...). En cas de doute, ne pas executer.
|
||||
"""
|
||||
import json
|
||||
import sys
|
||||
|
||||
from bs4 import BeautifulSoup
|
||||
|
||||
from mh_common import BASE, get_session
|
||||
from mh_lieu import fetch_lieu, parse_lieu
|
||||
|
||||
|
||||
def find_form_by_label(forms, label_substring):
|
||||
needle = label_substring.lower()
|
||||
for form in forms:
|
||||
for btn in form["buttons"]:
|
||||
if needle in (btn["label"] or "").lower():
|
||||
return form, btn
|
||||
return None, None
|
||||
|
||||
|
||||
def resolve_action_url(action):
|
||||
if action.startswith("http"):
|
||||
return action
|
||||
if action.startswith("/"):
|
||||
return "https://mh2.mh.raistlin.fr" + action
|
||||
return f"{BASE}/{action}"
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) < 2:
|
||||
print("Usage: python3 mh_submit.py \"texte du bouton\" [type] [id] [service]", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
label = sys.argv[1]
|
||||
type_ = sys.argv[2] if len(sys.argv) > 2 else "L"
|
||||
id_ = sys.argv[3] if len(sys.argv) > 3 else "-3"
|
||||
service = sys.argv[4] if len(sys.argv) > 4 else "0"
|
||||
|
||||
session = get_session()
|
||||
html = fetch_lieu(session, type_, id_, service)
|
||||
data = parse_lieu(html)
|
||||
|
||||
form, btn = find_form_by_label(data["forms"], label)
|
||||
if not form:
|
||||
print(json.dumps({
|
||||
"erreur": f"Aucun bouton contenant '{label}' trouve sur cette page",
|
||||
"forms_disponibles": data["forms"],
|
||||
}, ensure_ascii=False, indent=2))
|
||||
sys.exit(1)
|
||||
|
||||
payload = dict(form["hidden_fields"])
|
||||
if btn["name"]:
|
||||
payload[btn["name"]] = btn["value"]
|
||||
|
||||
action_url = resolve_action_url(form["action"])
|
||||
method = form["method"]
|
||||
|
||||
if method == "POST":
|
||||
r = session.post(action_url, data=payload)
|
||||
else:
|
||||
r = session.get(action_url, params=payload)
|
||||
|
||||
soup = BeautifulSoup(r.text, "html.parser")
|
||||
for tag in soup.find_all(["nav", "script", "style"]):
|
||||
tag.decompose()
|
||||
main = soup.find("main") or soup
|
||||
print(json.dumps({"resultat": main.get_text("\n", strip=True)}, ensure_ascii=False, indent=2))
|
||||
@@ -0,0 +1,2 @@
|
||||
requests
|
||||
beautifulsoup4
|
||||
Reference in New Issue
Block a user