@nosserb/prmt 0.0.0-stage → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +191 -2
- package/bin/prmt.mjs +174 -0
- package/dist/assets/AuditModule-BOO_iRy8.js +1 -0
- package/dist/assets/Audits-yUBm0EpW.js +1 -0
- package/dist/assets/BrainPage-FIWO_9Nc.js +3 -0
- package/dist/assets/BrainPage-XQCEGRc5.css +1 -0
- package/dist/assets/Carte-CPuiR1Ha.css +1 -0
- package/dist/assets/Carte-xycu1mQn.js +1 -0
- package/dist/assets/Dashboard-DKvCQcX8.js +2 -0
- package/dist/assets/Dashboard-XdhC6_hM.css +1 -0
- package/dist/assets/Field-C7c2mCbb.js +2 -0
- package/dist/assets/Journal-BLIIlh80.css +1 -0
- package/dist/assets/Journal-BaelFjwV.js +1 -0
- package/dist/assets/Landing-B2jxV3zs.css +1 -0
- package/dist/assets/Landing-Dr612Zdu.js +26 -0
- package/dist/assets/Register-DA_Aq5f8.js +1 -0
- package/dist/assets/Reports-8wfDNSxH.js +1 -0
- package/dist/assets/Reports-B8GXN95N.css +1 -0
- package/dist/assets/SavoirPage-BTxbH3LY.css +1 -0
- package/dist/assets/SavoirPage-Dp3eUCHp.js +1 -0
- package/dist/assets/SettingsPage-BElIP46c.css +1 -0
- package/dist/assets/SettingsPage-BiQbnw3o.js +1 -0
- package/dist/assets/Shell-BAyHVljp.css +1 -0
- package/dist/assets/Shell-CQRlx9-V.js +2 -0
- package/dist/assets/Table-DUmGel5e.js +1 -0
- package/dist/assets/Tabs-CfhbELZk.js +1 -0
- package/dist/assets/Terminal-Bv1IJfi9.js +3 -0
- package/dist/assets/Terminal-CEfj38NV.css +1 -0
- package/dist/assets/arrow-right-Cx3pAEhl.js +1 -0
- package/dist/assets/auditViews-BMza2Sdz.js +1 -0
- package/dist/assets/auditViews-BePWj26h.css +1 -0
- package/dist/assets/bricolage-grotesque-latin-ext-opsz-normal-IcJDqblK.woff2 +0 -0
- package/dist/assets/bricolage-grotesque-latin-opsz-normal-Cre6nC2_.woff2 +0 -0
- package/dist/assets/bricolage-grotesque-vietnamese-opsz-normal-D9N6E8K1.woff2 +0 -0
- package/dist/assets/charts-BcijSJ4J.js +1 -0
- package/dist/assets/circle-stop-C33BAcKW.js +1 -0
- package/dist/assets/command-ChA49Xk7.js +1 -0
- package/dist/assets/command-Ck4pdQCv.css +1 -0
- package/dist/assets/download-CeuKOenD.js +1 -0
- package/dist/assets/eye-CXXHBBpr.js +1 -0
- package/dist/assets/geist-cyrillic-ext-wght-normal-DjL33-gN.woff2 +0 -0
- package/dist/assets/geist-cyrillic-wght-normal-BEAKL7Jp.woff2 +0 -0
- package/dist/assets/geist-latin-ext-wght-normal-DC-KSUi6.woff2 +0 -0
- package/dist/assets/geist-latin-wght-normal-BgDaEnEv.woff2 +0 -0
- package/dist/assets/geist-vietnamese-wght-normal-6IgcOCM7.woff2 +0 -0
- package/dist/assets/habillage-B5mMBadn.js +1 -0
- package/dist/assets/index-BH1etemR.css +1 -0
- package/dist/assets/index-COSiM-R9.js +14 -0
- package/dist/assets/jetbrains-mono-cyrillic-ext-wght-normal-EocZY2iu.woff2 +0 -0
- package/dist/assets/jetbrains-mono-cyrillic-wght-normal-D73BlboJ.woff2 +0 -0
- package/dist/assets/jetbrains-mono-greek-wght-normal-Bw9x6K1M.woff2 +0 -0
- package/dist/assets/jetbrains-mono-latin-ext-wght-normal-DBQx-q_a.woff2 +0 -0
- package/dist/assets/jetbrains-mono-latin-wght-normal-B9CIFXIH.woff2 +0 -0
- package/dist/assets/jetbrains-mono-vietnamese-wght-normal-Bt-aOZkq.woff2 +0 -0
- package/dist/assets/layers-2x-Bpkbi35X.png +0 -0
- package/dist/assets/layers-BWBAp2CZ.png +0 -0
- package/dist/assets/markdown-DF87LQh1.css +1 -0
- package/dist/assets/markdown-o5sMhPHN.js +4 -0
- package/dist/assets/marker-icon-hN30_KVU.png +0 -0
- package/dist/assets/play-Daw4K_73.js +1 -0
- package/dist/assets/plus-D_QHm8ud.js +1 -0
- package/dist/assets/trash-DLpHM-s8.js +1 -0
- package/dist/assets/web-6j1-aY67.js +1 -0
- package/dist/favicon.svg +1 -0
- package/dist/index.html +18 -0
- package/docs/ANALYSE.md +87 -0
- package/docs/SECURITE.md +188 -0
- package/lib/scripts/lier.js +15 -0
- package/lib/scripts/searxng.js +91 -0
- package/lib/server/app.js +444 -0
- package/lib/server/audits/modules/local.js +429 -0
- package/lib/server/audits/modules/network.js +331 -0
- package/lib/server/audits/modules/web.js +302 -0
- package/lib/server/audits/net.js +141 -0
- package/lib/server/audits/registry.js +202 -0
- package/lib/server/audits/types.js +58 -0
- package/lib/server/brain/brain.js +624 -0
- package/lib/server/brain/conversations.js +82 -0
- package/lib/server/brain/ollama.js +197 -0
- package/lib/server/brain/prompt.js +188 -0
- package/lib/server/brain/provider.js +1 -0
- package/lib/server/cli/bridge.js +292 -0
- package/lib/server/cli/pont.js +62 -0
- package/lib/server/config.js +68 -0
- package/lib/server/engine/analyzer.js +205 -0
- package/lib/server/engine/catalog.js +229 -0
- package/lib/server/engine/decoder.js +91 -0
- package/lib/server/engine/engine.js +194 -0
- package/lib/server/engine/paths.js +129 -0
- package/lib/server/engine/policy.js +397 -0
- package/lib/server/engine/relax.js +10 -0
- package/lib/server/engine/runner.js +193 -0
- package/lib/server/errors.js +23 -0
- package/lib/server/http/guard.js +75 -0
- package/lib/server/http/router.js +154 -0
- package/lib/server/http/static.js +76 -0
- package/lib/server/index.js +40 -0
- package/lib/server/journal.js +139 -0
- package/lib/server/lien.js +33 -0
- package/lib/server/savoir/base.js +284 -0
- package/lib/server/savoir/lecture.js +406 -0
- package/lib/server/savoir/memoire.js +115 -0
- package/lib/server/search/categories.js +179 -0
- package/lib/server/search/fiche.js +326 -0
- package/lib/server/search/lieux.js +134 -0
- package/lib/server/search/overpass.js +216 -0
- package/lib/server/search/searxng.js +223 -0
- package/lib/server/search/tiles.js +103 -0
- package/lib/server/status.js +157 -0
- package/lib/server/stores.js +141 -0
- package/lib/server/telephone/acces.js +457 -0
- package/lib/server/telephone/certificat.js +79 -0
- package/lib/server/telephone/qr.js +308 -0
- package/lib/shared/analysis.js +40 -0
- package/lib/shared/schemas.js +105 -0
- package/lib/shared/types.js +10 -0
- package/package.json +81 -4
- package/searxng/settings.yml +35 -0
- package/tui/Modelfile +23 -0
- package/tui/README.md +103 -0
- package/tui/prmt_tui.py +65 -0
- package/tui/promethee/__init__.py +1 -0
- package/tui/promethee/__main__.py +4 -0
- package/tui/promethee/accueil.py +66 -0
- package/tui/promethee/agents.py +220 -0
- package/tui/promethee/app/__init__.py +4 -0
- package/tui/promethee/app/application.py +299 -0
- package/tui/promethee/app/clavier.py +99 -0
- package/tui/promethee/app/commandes_slash.py +164 -0
- package/tui/promethee/app/contexte.py +188 -0
- package/tui/promethee/app/definitions.py +518 -0
- package/tui/promethee/app/ecran.py +177 -0
- package/tui/promethee/app/envoi.py +141 -0
- package/tui/promethee/app/historique.py +373 -0
- package/tui/promethee/app/modele.py +108 -0
- package/tui/promethee/app/notes.py +63 -0
- package/tui/promethee/app/outils.py +589 -0
- package/tui/promethee/app/plateforme.py +409 -0
- package/tui/promethee/app/saisie.py +100 -0
- package/tui/promethee/app/taches.py +92 -0
- package/tui/promethee/app/tour.py +539 -0
- package/tui/promethee/app/validation.py +210 -0
- package/tui/promethee/appels.py +35 -0
- package/tui/promethee/commandes.py +154 -0
- package/tui/promethee/completion.py +140 -0
- package/tui/promethee/config.py +12 -0
- package/tui/promethee/consignes.py +278 -0
- package/tui/promethee/conversation.py +94 -0
- package/tui/promethee/etat.py +253 -0
- package/tui/promethee/fichiers.py +354 -0
- package/tui/promethee/journal.py +116 -0
- package/tui/promethee/logo.py +132 -0
- package/tui/promethee/memoire.py +50 -0
- package/tui/promethee/ollama.py +52 -0
- package/tui/promethee/plan.py +91 -0
- package/tui/promethee/politique.py +429 -0
- package/tui/promethee/preferences.py +65 -0
- package/tui/promethee/presse_papiers.py +44 -0
- package/tui/promethee/recherche.py +581 -0
- package/tui/promethee/securite.py +204 -0
- package/tui/promethee/shell.py +190 -0
- package/tui/promethee/textes.py +190 -0
- package/tui/promethee/theme.py +92 -0
- package/tui/promethee/widgets.py +493 -0
- package/tui/requirements.txt +1 -0
|
@@ -0,0 +1,518 @@
|
|
|
1
|
+
"""/agents et /commandes : lister, voir, créer (à la main ou décrits à Prométhée), modifier, supprimer.
|
|
2
|
+
|
|
3
|
+
Un agent et une commande personnalisée sont tous deux un fichier Markdown que tu peux écrire toi-même ou
|
|
4
|
+
faire écrire par Prométhée. Leurs écrans suivent le même déroulé : il est écrit une fois, dans Definitions.
|
|
5
|
+
Ce qui les distingue (libellés, champs, façon de s'en servir) est décrit par un « genre » : AGENTS, COMMANDES.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
import os
|
|
11
|
+
import subprocess
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
from rich.text import Text
|
|
15
|
+
from textual import work
|
|
16
|
+
|
|
17
|
+
from ..agents import (
|
|
18
|
+
AGENTS_DIR, AGENT_GEN_FIX, AGENT_GEN_SYSTEM, AGENT_SCHEMA, AGENT_TEMPLATE, AGENT_TOOL_NAMES, LOCAL_AGENTS,
|
|
19
|
+
SCOPE_LABELS, TOOL_LABELS, agent_command_problems, agent_markdown, agent_slug, find_agent, list_agents,
|
|
20
|
+
unique_agent_path,
|
|
21
|
+
)
|
|
22
|
+
from ..commandes import (
|
|
23
|
+
COMMANDS_DIR, COMMAND_GEN_SYSTEM, COMMAND_SCHEMA, COMMAND_TEMPLATE, LOCAL_COMMANDS, builtin_command_names,
|
|
24
|
+
command_markdown, find_custom_command, list_custom_commands,
|
|
25
|
+
)
|
|
26
|
+
from ..config import CREATE_NO_WINDOW
|
|
27
|
+
from ..consignes import READ_TOOL, TOOL_NAME
|
|
28
|
+
from ..fichiers import new_file_format, write_text_file
|
|
29
|
+
from ..theme import AGENT_COLOR, DIM, GREEN, RED, YELLOW
|
|
30
|
+
|
|
31
|
+
CREER = ("creer", "nouveau", "new", "create", "ajouter")
|
|
32
|
+
APERCU_LIGNES = 14 # lignes de consignes montrées avant de créer
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def portee(item: dict) -> str:
|
|
36
|
+
return SCOPE_LABELS.get(item["scope"], item["scope"])
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def resume(item: dict) -> str:
|
|
40
|
+
return " ".join(item["description"].split())
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
# --------------------------------------------------------------------------- genres
|
|
44
|
+
class Genre:
|
|
45
|
+
"""Ce qui distingue un agent d'une commande personnalisée dans les écrans de gestion."""
|
|
46
|
+
|
|
47
|
+
cle = "" # « agent » ou « commande » (valeur de _draft_kind)
|
|
48
|
+
dossier: Path # définitions globales
|
|
49
|
+
local: Path # définitions d'un dossier, relatif au dossier courant
|
|
50
|
+
schema: dict = {} # forme de la réponse demandée au modèle
|
|
51
|
+
consigne = "" # consigne de génération
|
|
52
|
+
jetons = 0 # longueur maximale de la réponse
|
|
53
|
+
ferme_menu = False # après « l'utiliser », fermer le menu d'autocomplétion (une commande commence par /)
|
|
54
|
+
# Libellés (le genre grammatical change d'un genre à l'autre)
|
|
55
|
+
occupe = inconnu = titre = nouveau = titre_creation = ou_global = ou_dossier = ou_lieux = comment = decrire = ""
|
|
56
|
+
question = oui = oui_modifier = non_cree = annulee = impossible = demande = sans_texte = verification = ""
|
|
57
|
+
correction: str | None = None # texte du spinner pendant la correction (None : pas de changement de texte)
|
|
58
|
+
invite = "" # ce qui s'affiche quand tu vas décrire la définition à créer
|
|
59
|
+
|
|
60
|
+
def lister(self, cwd: Path) -> list[dict]:
|
|
61
|
+
raise NotImplementedError
|
|
62
|
+
|
|
63
|
+
def trouver(self, nom: str, cwd: Path) -> dict | None:
|
|
64
|
+
raise NotImplementedError
|
|
65
|
+
|
|
66
|
+
def markdown(self, item: dict) -> str:
|
|
67
|
+
raise NotImplementedError
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class GenreAgent(Genre):
|
|
71
|
+
cle, dossier, local = "agent", AGENTS_DIR, LOCAL_AGENTS
|
|
72
|
+
schema, consigne, jetons = AGENT_SCHEMA, AGENT_GEN_SYSTEM, 1000
|
|
73
|
+
occupe = "Attends la fin de la réponse avant de gérer les agents (ou Esc pour l'interrompre)."
|
|
74
|
+
inconnu = "Agent inconnu : {arg}."
|
|
75
|
+
titre, nouveau, titre_creation = "Agents", "Créer un nouvel agent", "Nouvel agent"
|
|
76
|
+
ou_global = "Agent global : disponible partout"
|
|
77
|
+
ou_dossier = "Agent de ce dossier : seulement quand tu travailles dans {dossier}"
|
|
78
|
+
ou_lieux = " global : {globaux}\n ce dossier : {local}"
|
|
79
|
+
comment, decrire = "Comment le créer ?", "Le décrire : Prométhée écrit l'agent pour toi (recommandé)"
|
|
80
|
+
question, oui, oui_modifier = "Créer cet agent ?", "Oui, le créer", "Oui, puis le modifier dans le Bloc-notes"
|
|
81
|
+
non_cree, annulee = "Agent non créé.", "Création de l'agent annulée."
|
|
82
|
+
impossible = "Impossible de créer l'agent : {error}. Réessaie (/agents) ou écris-le toi-même."
|
|
83
|
+
demande = "Description de l'agent voulu : « {description} »"
|
|
84
|
+
sans_texte = "le modèle n'a pas écrit de consignes"
|
|
85
|
+
verification, correction = "Vérification des commandes de l'agent", "Prométhée corrige l'agent"
|
|
86
|
+
invite = ("Décris l'agent à créer : son rôle, ce qu'il doit faire et quand l'utiliser. Par exemple : "
|
|
87
|
+
"« un agent qui audite le pare-feu Windows et liste les règles trop permissives ».")
|
|
88
|
+
|
|
89
|
+
def lister(self, cwd: Path) -> list[dict]:
|
|
90
|
+
return list_agents(cwd)
|
|
91
|
+
|
|
92
|
+
def trouver(self, nom: str, cwd: Path) -> dict | None:
|
|
93
|
+
return find_agent(nom, cwd)
|
|
94
|
+
|
|
95
|
+
def markdown(self, item: dict) -> str:
|
|
96
|
+
return agent_markdown(item)
|
|
97
|
+
|
|
98
|
+
def entete(self, items: list[dict], cwd: Path) -> Text:
|
|
99
|
+
return Text.assemble((f" {len(items)} agent{'s' if len(items) > 1 else ''}", "bold"),
|
|
100
|
+
(" · tape @agent-nom pour confier une tâche à un agent\n", DIM),
|
|
101
|
+
(f" globaux : {AGENTS_DIR}\n de ce dossier : {cwd / LOCAL_AGENTS}", DIM))
|
|
102
|
+
|
|
103
|
+
def ligne(self, item: dict) -> str:
|
|
104
|
+
desc = resume(item)
|
|
105
|
+
return f"{item['name']} · {portee(item)} · {desc[:60] + '…' if len(desc) > 60 else desc}"
|
|
106
|
+
|
|
107
|
+
def titre_fiche(self, item: dict) -> str:
|
|
108
|
+
return f"Agent {item['name']}"
|
|
109
|
+
|
|
110
|
+
def fiche(self, item: dict) -> Text:
|
|
111
|
+
path = item.get("path")
|
|
112
|
+
body = Text()
|
|
113
|
+
body.append(f" {resume(item) or '(sans description)'}\n", style=DIM)
|
|
114
|
+
body.append(f" outils : {', '.join(TOOL_LABELS.get(t, t) for t in item['tools'])}", style=DIM)
|
|
115
|
+
if item.get("model"):
|
|
116
|
+
body.append(f"\n modèle : {item['model']}", style=DIM)
|
|
117
|
+
body.append(f"\n fichier : {path}" if path else "\n agent intégré à Prométhée", style=DIM)
|
|
118
|
+
return body
|
|
119
|
+
|
|
120
|
+
def options(self, item: dict) -> list[tuple[str, str]]:
|
|
121
|
+
options = [("use", f"L'utiliser : écrire @agent-{item['name']} dans la saisie"), ("show", "Voir ses consignes")]
|
|
122
|
+
if item.get("path"):
|
|
123
|
+
options += [("edit", "Le modifier dans le Bloc-notes"), ("delete", "Le supprimer")]
|
|
124
|
+
else:
|
|
125
|
+
options.append(("copy", "Le personnaliser (copie modifiable dans tes agents globaux)"))
|
|
126
|
+
return options + [("back", "Retour à la liste"), ("no", "Fermer (Esc)")]
|
|
127
|
+
|
|
128
|
+
def saisie(self, item: dict) -> str:
|
|
129
|
+
return f"@agent-{item['name']} "
|
|
130
|
+
|
|
131
|
+
def montrer(self, item: dict) -> str:
|
|
132
|
+
tools = ", ".join(f"`{t}`" for t in item["tools"])
|
|
133
|
+
return (f"**Agent {item['name']}** ({portee(item)})\n\n*{resume(item)}*\n\nOutils : {tools}\n\n"
|
|
134
|
+
f"```text\n{item['prompt']}\n```")
|
|
135
|
+
|
|
136
|
+
def ouvert(self, item: dict) -> str:
|
|
137
|
+
return f"Agent {item['name']} ouvert dans le Bloc-notes. Les changements comptent dès sa prochaine utilisation."
|
|
138
|
+
|
|
139
|
+
def suppression(self, item: dict) -> tuple[str, str]:
|
|
140
|
+
return "Supprimer un agent", f"Supprimer l'agent {item['name']} ?"
|
|
141
|
+
|
|
142
|
+
def supprime(self, item: dict, cwd: Path) -> str:
|
|
143
|
+
back = find_agent(item["name"], cwd)
|
|
144
|
+
return f"Agent {item['name']} supprimé ({item['path']})." + (
|
|
145
|
+
" L'agent intégré du même nom est de nouveau utilisé." if back and back["scope"] == "intégré" else "")
|
|
146
|
+
|
|
147
|
+
def gabarit(self, name: str) -> tuple[str, str]:
|
|
148
|
+
"""(contenu du modèle prérempli, message une fois le fichier ouvert). {path} est remplacé par le chemin."""
|
|
149
|
+
return agent_markdown(dict(AGENT_TEMPLATE, name=name)), (
|
|
150
|
+
"Modèle d'agent créé ({path}). Remplis-le dans le Bloc-notes (nom, description, outils, consignes) puis "
|
|
151
|
+
"enregistre : il sera disponible tout de suite.")
|
|
152
|
+
|
|
153
|
+
gabarit_nom = AGENT_TEMPLATE["name"]
|
|
154
|
+
|
|
155
|
+
def construire(self, data: dict, prompt: str, scope: str) -> dict:
|
|
156
|
+
return {"name": agent_slug(str(data.get("nom") or "")) or "mon-agent", "description": " ".join(str(data.get("description") or "").split()),
|
|
157
|
+
"tools": [t for t in AGENT_TOOL_NAMES if t in (data.get("outils") or [])] or [TOOL_NAME, READ_TOOL],
|
|
158
|
+
"model": "", "prompt": prompt, "scope": scope, "path": None, "warnings": []}
|
|
159
|
+
|
|
160
|
+
def nom_libre(self, item: dict, folder: Path) -> tuple[str, Path]:
|
|
161
|
+
return unique_agent_path(folder, item["name"])
|
|
162
|
+
|
|
163
|
+
def apercu(self, item: dict) -> Text:
|
|
164
|
+
body = Text()
|
|
165
|
+
body.append(f" @agent-{item['name']}\n", style=f"bold {AGENT_COLOR}")
|
|
166
|
+
body.append(f" {item['description']}\n", style=DIM)
|
|
167
|
+
body.append(f" outils : {', '.join(TOOL_LABELS.get(t, t) for t in item['tools'])}\n\n", style=DIM)
|
|
168
|
+
return body
|
|
169
|
+
|
|
170
|
+
def cree(self, item: dict, edit: bool) -> str:
|
|
171
|
+
name = item["name"]
|
|
172
|
+
if edit:
|
|
173
|
+
return f"Agent {name} créé et ouvert dans le Bloc-notes. Utilise-le avec @agent-{name}."
|
|
174
|
+
return (f"Agent {name} créé ({item['path']}). Utilise-le avec @agent-{name} ou demande à Prométhée "
|
|
175
|
+
"de lui confier une tâche.")
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
class GenreCommande(Genre):
|
|
179
|
+
cle, dossier, local = "commande", COMMANDS_DIR, LOCAL_COMMANDS
|
|
180
|
+
schema, consigne, jetons = COMMAND_SCHEMA, COMMAND_GEN_SYSTEM, 800
|
|
181
|
+
ferme_menu = True
|
|
182
|
+
occupe = "Attends la fin de la réponse avant de gérer les commandes (ou Esc pour l'interrompre)."
|
|
183
|
+
inconnu = "Commande personnalisée inconnue : {arg}."
|
|
184
|
+
titre, nouveau, titre_creation = "Commandes personnalisées", "Créer une nouvelle commande", "Nouvelle commande"
|
|
185
|
+
ou_global = "Commande globale : disponible partout"
|
|
186
|
+
ou_dossier = "Commande de ce dossier : seulement quand tu travailles dans {dossier}"
|
|
187
|
+
ou_lieux = " globales : {globaux}\n ce dossier : {local}"
|
|
188
|
+
comment, decrire = "Comment la créer ?", "La décrire : Prométhée l'écrit pour toi (recommandé)"
|
|
189
|
+
question, oui, oui_modifier = "Créer cette commande ?", "Oui, la créer", "Oui, puis la modifier dans le Bloc-notes"
|
|
190
|
+
non_cree, annulee = "Commande non créée.", "Création de la commande annulée."
|
|
191
|
+
impossible = "Impossible de créer la commande : {error}. Réessaie (/commandes) ou écris-la toi-même."
|
|
192
|
+
demande = "Description de la commande voulue : « {description} »"
|
|
193
|
+
sans_texte = "le modèle n'a pas écrit le texte de la commande"
|
|
194
|
+
verification = "Vérification des commandes citées"
|
|
195
|
+
invite = ("Décris la commande à créer : ce qu'elle doit faire et ce que tu tapes après son nom. Par "
|
|
196
|
+
"exemple : « une commande qui scanne les ports courants d'une adresse IP et explique "
|
|
197
|
+
"chaque service trouvé ».")
|
|
198
|
+
|
|
199
|
+
def lister(self, cwd: Path) -> list[dict]:
|
|
200
|
+
return list_custom_commands(cwd)
|
|
201
|
+
|
|
202
|
+
def trouver(self, nom: str, cwd: Path) -> dict | None:
|
|
203
|
+
return find_custom_command(nom.lstrip("/"), cwd)
|
|
204
|
+
|
|
205
|
+
def markdown(self, item: dict) -> str:
|
|
206
|
+
return command_markdown(item)
|
|
207
|
+
|
|
208
|
+
@staticmethod
|
|
209
|
+
def usage(item: dict) -> str:
|
|
210
|
+
return f"/{item['name']}" + (f" {item['argument']}" if item.get("argument") else "")
|
|
211
|
+
|
|
212
|
+
def entete(self, items: list[dict], cwd: Path) -> Text:
|
|
213
|
+
s = "s" if len(items) > 1 else ""
|
|
214
|
+
return Text.assemble((f" {len(items)} commande{s} personnalisée{s}", "bold"),
|
|
215
|
+
(" · tape /nom pour en lancer une (elles apparaissent dans le menu /)\n", DIM),
|
|
216
|
+
(f" globales : {COMMANDS_DIR}\n de ce dossier : {cwd / LOCAL_COMMANDS}", DIM))
|
|
217
|
+
|
|
218
|
+
def ligne(self, item: dict) -> str:
|
|
219
|
+
desc = resume(item)
|
|
220
|
+
return f"/{item['name']} · {portee(item)} · {desc[:60] + '…' if len(desc) > 60 else desc}"
|
|
221
|
+
|
|
222
|
+
def titre_fiche(self, item: dict) -> str:
|
|
223
|
+
return f"Commande /{item['name']}"
|
|
224
|
+
|
|
225
|
+
def fiche(self, item: dict) -> Text:
|
|
226
|
+
body = Text()
|
|
227
|
+
body.append(f" {self.usage(item)}\n", style=f"bold {AGENT_COLOR}")
|
|
228
|
+
body.append(f" {resume(item) or '(sans description)'}\n", style=DIM)
|
|
229
|
+
body.append(f" fichier : {item['path']}", style=DIM)
|
|
230
|
+
return body
|
|
231
|
+
|
|
232
|
+
def options(self, item: dict) -> list[tuple[str, str]]:
|
|
233
|
+
return [("use", f"L'utiliser : écrire /{item['name']} dans la saisie"), ("show", "Voir son texte"),
|
|
234
|
+
("edit", "La modifier dans le Bloc-notes"), ("delete", "La supprimer"), ("back", "Retour à la liste"),
|
|
235
|
+
("no", "Fermer (Esc)")]
|
|
236
|
+
|
|
237
|
+
def saisie(self, item: dict) -> str:
|
|
238
|
+
return f"/{item['name']} "
|
|
239
|
+
|
|
240
|
+
def montrer(self, item: dict) -> str:
|
|
241
|
+
return (f"**Commande {self.usage(item)}** ({portee(item)})\n\n*{resume(item)}*\n\n"
|
|
242
|
+
f"```text\n{item['prompt']}\n```")
|
|
243
|
+
|
|
244
|
+
def ouvert(self, item: dict) -> str:
|
|
245
|
+
return f"Commande /{item['name']} ouverte dans le Bloc-notes. Les changements comptent dès sa prochaine utilisation."
|
|
246
|
+
|
|
247
|
+
def suppression(self, item: dict) -> tuple[str, str]:
|
|
248
|
+
return "Supprimer", f"Supprimer la commande /{item['name']} ?"
|
|
249
|
+
|
|
250
|
+
def supprime(self, item: dict, cwd: Path) -> str:
|
|
251
|
+
return f"La commande /{item['name']} : supprimée ({item['path']})."
|
|
252
|
+
|
|
253
|
+
def gabarit(self, name: str) -> tuple[str, str]:
|
|
254
|
+
return command_markdown(COMMAND_TEMPLATE), (
|
|
255
|
+
f"Modèle de commande créé ({{path}}) : le nom du fichier est le nom de la commande (/{name}). "
|
|
256
|
+
"Écris son texte puis enregistre.")
|
|
257
|
+
|
|
258
|
+
gabarit_nom = COMMAND_TEMPLATE["name"]
|
|
259
|
+
|
|
260
|
+
def construire(self, data: dict, prompt: str, scope: str) -> dict:
|
|
261
|
+
argument = str(data.get("argument") or "").strip()
|
|
262
|
+
if argument and "$ARGUMENTS" not in prompt:
|
|
263
|
+
prompt += "\n\nCible : $ARGUMENTS"
|
|
264
|
+
return {"name": agent_slug(str(data.get("nom") or "")) or "ma-commande", "description": " ".join(str(data.get("description") or "").split()),
|
|
265
|
+
"argument": argument, "prompt": prompt, "scope": scope, "path": None, "warnings": []}
|
|
266
|
+
|
|
267
|
+
def nom_libre(self, item: dict, folder: Path) -> tuple[str, Path]:
|
|
268
|
+
name = item["name"]
|
|
269
|
+
if name in builtin_command_names():
|
|
270
|
+
name = f"{name}-perso" # une commande personnalisée ne peut pas prendre le nom d'une commande intégrée
|
|
271
|
+
return unique_agent_path(folder, name)
|
|
272
|
+
|
|
273
|
+
def apercu(self, item: dict) -> Text:
|
|
274
|
+
body = Text()
|
|
275
|
+
body.append(f" {self.usage(item)}\n", style=f"bold {AGENT_COLOR}")
|
|
276
|
+
body.append(f" {item['description']}\n\n", style=DIM)
|
|
277
|
+
return body
|
|
278
|
+
|
|
279
|
+
def cree(self, item: dict, edit: bool) -> str:
|
|
280
|
+
name = item["name"]
|
|
281
|
+
if edit:
|
|
282
|
+
return f"Commande /{name} créée et ouverte dans le Bloc-notes."
|
|
283
|
+
return f"Commande /{name} créée ({item['path']}). Lance-la avec {self.usage(item)}."
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
AGENTS, COMMANDES = GenreAgent(), GenreCommande()
|
|
287
|
+
GENRES = {genre.cle: genre for genre in (AGENTS, COMMANDES)}
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
# --------------------------------------------------------------------------- écrans
|
|
291
|
+
class Definitions:
|
|
292
|
+
"""Partie de PrometheeApp : /agents et /commandes : lister, créer, modifier, supprimer."""
|
|
293
|
+
|
|
294
|
+
def cmd_agents(self, arg: str = "") -> None:
|
|
295
|
+
"""/agents : la liste des agents, puis créer, voir, modifier ou supprimer."""
|
|
296
|
+
self._gerer(AGENTS, arg)
|
|
297
|
+
|
|
298
|
+
def cmd_commands(self, arg: str = "") -> None:
|
|
299
|
+
"""/commandes : la liste des commandes personnalisées, puis créer, voir, modifier ou supprimer."""
|
|
300
|
+
self._gerer(COMMANDES, arg)
|
|
301
|
+
|
|
302
|
+
def _gerer(self, genre: Genre, arg: str = "") -> None:
|
|
303
|
+
if self.generating:
|
|
304
|
+
self.add_notice(genre.occupe, YELLOW)
|
|
305
|
+
return
|
|
306
|
+
if arg:
|
|
307
|
+
if agent_slug(arg) in CREER:
|
|
308
|
+
self._creer(genre)
|
|
309
|
+
return
|
|
310
|
+
item = genre.trouver(arg, self.cwd)
|
|
311
|
+
if item is not None:
|
|
312
|
+
self._fiche(genre, item)
|
|
313
|
+
return
|
|
314
|
+
self.add_notice(genre.inconnu.format(arg=arg), YELLOW)
|
|
315
|
+
items = genre.lister(self.cwd)
|
|
316
|
+
options = [("new", genre.nouveau)] + [(f"i{i}", genre.ligne(item)) for i, item in enumerate(items)]
|
|
317
|
+
options.append(("no", "Fermer (Esc)"))
|
|
318
|
+
|
|
319
|
+
def chosen(decision: str) -> None:
|
|
320
|
+
if decision == "new":
|
|
321
|
+
self._creer(genre)
|
|
322
|
+
elif decision.startswith("i"):
|
|
323
|
+
self._fiche(genre, items[int(decision[1:])])
|
|
324
|
+
|
|
325
|
+
self.ask(genre.titre, genre.entete(items, self.cwd), "Que veux-tu faire ?", options, chosen, esc="fermer")
|
|
326
|
+
|
|
327
|
+
def _fiche(self, genre: Genre, item: dict) -> None:
|
|
328
|
+
"""Une définition : s'en servir, voir son texte, la modifier, la supprimer."""
|
|
329
|
+
def chosen(decision: str) -> None:
|
|
330
|
+
if decision == "use":
|
|
331
|
+
self.input.load_text(genre.saisie(item))
|
|
332
|
+
self.input.move_cursor(self.input.document.end)
|
|
333
|
+
if genre.ferme_menu:
|
|
334
|
+
self.hide_completions()
|
|
335
|
+
elif decision == "show":
|
|
336
|
+
self.add_notice_md(genre.montrer(item))
|
|
337
|
+
elif decision == "edit":
|
|
338
|
+
self.open_in_notepad(item["path"], genre.ouvert(item))
|
|
339
|
+
elif decision == "delete":
|
|
340
|
+
self._supprimer(genre, item)
|
|
341
|
+
elif decision == "copy":
|
|
342
|
+
self._personnaliser(item)
|
|
343
|
+
elif decision == "back":
|
|
344
|
+
self._gerer(genre)
|
|
345
|
+
|
|
346
|
+
self.ask(genre.titre_fiche(item), genre.fiche(item), "Que veux-tu faire ?", genre.options(item), chosen, esc="fermer")
|
|
347
|
+
|
|
348
|
+
def _personnaliser(self, agent: dict) -> None:
|
|
349
|
+
"""Agent intégré : en faire une copie modifiable dans les agents globaux (elle le remplace)."""
|
|
350
|
+
name, target = agent["name"], AGENTS_DIR / f"{agent['name']}.md"
|
|
351
|
+
try:
|
|
352
|
+
if not target.exists():
|
|
353
|
+
write_text_file(target, agent_markdown(agent), *new_file_format(target))
|
|
354
|
+
except OSError as err:
|
|
355
|
+
self.add_notice(f"Impossible d'écrire {target} : {err}", f"bold {RED}")
|
|
356
|
+
return
|
|
357
|
+
self.open_in_notepad(target, f"Copie de l'agent {name} créée ({target}) : elle remplace l'agent "
|
|
358
|
+
"intégré. Modifie-la puis enregistre.")
|
|
359
|
+
|
|
360
|
+
def _supprimer(self, genre: Genre, item: dict) -> None:
|
|
361
|
+
path: Path = item["path"]
|
|
362
|
+
|
|
363
|
+
def chosen(decision: str) -> None:
|
|
364
|
+
if decision != "yes":
|
|
365
|
+
return
|
|
366
|
+
try:
|
|
367
|
+
path.unlink()
|
|
368
|
+
except OSError as err:
|
|
369
|
+
self.add_notice(f"Impossible de supprimer {path} : {err}", f"bold {RED}")
|
|
370
|
+
return
|
|
371
|
+
self.add_notice(genre.supprime(item, self.cwd), GREEN)
|
|
372
|
+
|
|
373
|
+
title, question = genre.suppression(item)
|
|
374
|
+
self.ask(title, Text(f" {path}", style="bold"), question, [("yes", "Oui, supprimer ce fichier"), ("no", "Non (Esc)")],
|
|
375
|
+
chosen, danger=True)
|
|
376
|
+
|
|
377
|
+
def open_in_notepad(self, path: Path, message: str) -> None:
|
|
378
|
+
try:
|
|
379
|
+
subprocess.Popen(["notepad.exe", str(path)], creationflags=CREATE_NO_WINDOW)
|
|
380
|
+
except OSError as err:
|
|
381
|
+
self.add_notice(f"Impossible d'ouvrir {path} : {err}", f"bold {RED}")
|
|
382
|
+
return
|
|
383
|
+
self.add_notice(message, GREEN)
|
|
384
|
+
|
|
385
|
+
# ------------------------------------------------------------------ création
|
|
386
|
+
def _dossier(self, genre: Genre, scope: str) -> Path:
|
|
387
|
+
return genre.dossier if scope == "global" else self.cwd / genre.local
|
|
388
|
+
|
|
389
|
+
def _creer(self, genre: Genre) -> None:
|
|
390
|
+
"""Créer une définition : où (globale / ce dossier), puis comment (la décrire / l'écrire soi-même)."""
|
|
391
|
+
local = self.cwd / genre.local
|
|
392
|
+
options = [("global", genre.ou_global)]
|
|
393
|
+
if os.path.normcase(str(local)) != os.path.normcase(str(genre.dossier)):
|
|
394
|
+
options.append(("dossier", genre.ou_dossier.format(dossier=self.cwd.name or self.cwd)))
|
|
395
|
+
options.append(("no", "Annuler (Esc)"))
|
|
396
|
+
|
|
397
|
+
def where(decision: str) -> None:
|
|
398
|
+
if decision in ("global", "dossier"):
|
|
399
|
+
self.call_after_refresh(self._creer_comment, genre, decision)
|
|
400
|
+
|
|
401
|
+
self.ask(genre.titre_creation, Text(genre.ou_lieux.format(globaux=genre.dossier, local=local), style=DIM),
|
|
402
|
+
"Où l'enregistrer ?", options, where)
|
|
403
|
+
|
|
404
|
+
def _creer_comment(self, genre: Genre, scope: str) -> None:
|
|
405
|
+
def how(decision: str) -> None:
|
|
406
|
+
if decision == "gen":
|
|
407
|
+
self.set_agent_draft(scope, genre.cle)
|
|
408
|
+
elif decision == "manual":
|
|
409
|
+
name, path = unique_agent_path(self._dossier(genre, scope), genre.gabarit_nom)
|
|
410
|
+
content, message = genre.gabarit(name)
|
|
411
|
+
try:
|
|
412
|
+
write_text_file(path, content, *new_file_format(path))
|
|
413
|
+
except OSError as err:
|
|
414
|
+
self.add_notice(f"Impossible d'écrire {path} : {err}", f"bold {RED}")
|
|
415
|
+
return
|
|
416
|
+
self.open_in_notepad(path, message.format(path=path))
|
|
417
|
+
|
|
418
|
+
options = [("gen", genre.decrire), ("manual", "L'écrire moi-même dans le Bloc-notes (modèle prérempli)"),
|
|
419
|
+
("no", "Annuler (Esc)")]
|
|
420
|
+
self.ask(genre.titre_creation, None, genre.comment, options, how)
|
|
421
|
+
|
|
422
|
+
def set_agent_draft(self, scope: str | None, kind: str = "agent") -> None:
|
|
423
|
+
"""En attente de la description d'un nouvel agent ou d'une nouvelle commande (la saisie devient violette)."""
|
|
424
|
+
self._agent_draft = scope
|
|
425
|
+
self._draft_kind = kind
|
|
426
|
+
self.query_one("#input-box").set_class(scope is not None, "-agent")
|
|
427
|
+
if scope is not None:
|
|
428
|
+
self.add_notice(GENRES[kind].invite, AGENT_COLOR)
|
|
429
|
+
self.set_hints()
|
|
430
|
+
|
|
431
|
+
@work(thread=True, exclusive=True, group="generate")
|
|
432
|
+
def generate_agent(self, description: str, scope: str) -> None:
|
|
433
|
+
self._generer(AGENTS, description, scope)
|
|
434
|
+
|
|
435
|
+
@work(thread=True, exclusive=True, group="generate")
|
|
436
|
+
def generate_command(self, description: str, scope: str) -> None:
|
|
437
|
+
self._generer(COMMANDES, description, scope)
|
|
438
|
+
|
|
439
|
+
def _generer(self, genre: Genre, description: str, scope: str) -> None:
|
|
440
|
+
"""Fait écrire la définition par le modèle (fil de travail). Les commandes PowerShell qu'il cite sont
|
|
441
|
+
vérifiées sur la machine : si l'une est fausse, il a une occasion de corriger."""
|
|
442
|
+
payload: dict = {"model": self.model, "stream": True, "format": genre.schema,
|
|
443
|
+
"options": {"temperature": 0.3, "num_predict": genre.jetons},
|
|
444
|
+
"messages": [{"role": "system", "content": genre.consigne},
|
|
445
|
+
{"role": "user", "content": genre.demande.format(description=description)}]}
|
|
446
|
+
if self.supports_thinking:
|
|
447
|
+
payload["think"] = False
|
|
448
|
+
item, error, fixed = None, None, ""
|
|
449
|
+
try:
|
|
450
|
+
for attempt in range(2):
|
|
451
|
+
raw = self._summarize(payload) if not self.cancel_event.is_set() else ""
|
|
452
|
+
if self.cancel_event.is_set():
|
|
453
|
+
break
|
|
454
|
+
data = json.loads(raw or "{}")
|
|
455
|
+
prompt = str(data.get("consignes") or "").strip()
|
|
456
|
+
if not prompt:
|
|
457
|
+
if item is not None:
|
|
458
|
+
break # la correction a échoué : on garde la première version
|
|
459
|
+
raise ValueError(genre.sans_texte)
|
|
460
|
+
item = genre.construire(data, prompt, scope)
|
|
461
|
+
# Les commandes qu'il cite existent-elles, avec ces paramètres ? Sinon, une demande de correction.
|
|
462
|
+
self.call_from_thread(self._spinner, True, "check", genre.verification)
|
|
463
|
+
problems = agent_command_problems(item["prompt"], self.cwd)
|
|
464
|
+
item["warnings"] = [short for short, _ in problems]
|
|
465
|
+
if not problems or attempt:
|
|
466
|
+
break
|
|
467
|
+
if genre.correction:
|
|
468
|
+
self.call_from_thread(self._spinner, True, "check", genre.correction)
|
|
469
|
+
payload["messages"] = payload["messages"][:2] + [
|
|
470
|
+
{"role": "assistant", "content": raw},
|
|
471
|
+
{"role": "user", "content": AGENT_GEN_FIX.format(problems=" ; ".join(d for _, d in problems))}]
|
|
472
|
+
fixed = ", ".join(short for short, _ in problems)
|
|
473
|
+
except Exception as err: # JSON invalide, Ollama injoignable…
|
|
474
|
+
error = str(err)
|
|
475
|
+
if item is not None:
|
|
476
|
+
item["fixed"] = fixed
|
|
477
|
+
self.call_from_thread(self._fin_generation, genre, item, error, scope)
|
|
478
|
+
|
|
479
|
+
def _fin_generation(self, genre: Genre, item: dict | None, error: str | None, scope: str) -> None:
|
|
480
|
+
"""Montre la définition écrite par le modèle : tu la crées, tu la crées puis la modifies, ou tu la refuses."""
|
|
481
|
+
self.generating = False
|
|
482
|
+
self._spinner(False)
|
|
483
|
+
if self.cancel_event.is_set():
|
|
484
|
+
self.add_notice(genre.annulee, DIM)
|
|
485
|
+
return
|
|
486
|
+
if item is None:
|
|
487
|
+
self.add_notice(genre.impossible.format(error=error), f"bold {RED}")
|
|
488
|
+
return
|
|
489
|
+
name, path = genre.nom_libre(item, self._dossier(genre, scope))
|
|
490
|
+
item.update(name=name, path=path)
|
|
491
|
+
body = genre.apercu(item)
|
|
492
|
+
lines = item["prompt"].split("\n")
|
|
493
|
+
for line in lines[:APERCU_LIGNES]:
|
|
494
|
+
body.append(f" {line}\n")
|
|
495
|
+
if len(lines) > APERCU_LIGNES:
|
|
496
|
+
body.append(f" … +{len(lines) - APERCU_LIGNES} lignes\n", style=f"italic {DIM}")
|
|
497
|
+
body.append(f"\n fichier : {path}", style=DIM)
|
|
498
|
+
if item.get("fixed") and not item.get("warnings"):
|
|
499
|
+
body.append(f"\n ✓ commandes corrigées automatiquement ({item['fixed']})", style=GREEN)
|
|
500
|
+
for warning in item.get("warnings") or []:
|
|
501
|
+
body.append(f"\n ⚠ À vérifier : {warning}", style=YELLOW)
|
|
502
|
+
|
|
503
|
+
def chosen(decision: str) -> None:
|
|
504
|
+
if decision not in ("yes", "edit"):
|
|
505
|
+
self.add_notice(genre.non_cree, DIM)
|
|
506
|
+
return
|
|
507
|
+
try:
|
|
508
|
+
write_text_file(path, genre.markdown(item), *new_file_format(path))
|
|
509
|
+
except OSError as err:
|
|
510
|
+
self.add_notice(f"Impossible d'écrire {path} : {err}", f"bold {RED}")
|
|
511
|
+
return
|
|
512
|
+
if decision == "edit":
|
|
513
|
+
self.open_in_notepad(path, genre.cree(item, True))
|
|
514
|
+
else:
|
|
515
|
+
self.add_notice(genre.cree(item, False), GREEN)
|
|
516
|
+
|
|
517
|
+
self.ask(genre.titre_creation, body, genre.question,
|
|
518
|
+
[("yes", genre.oui), ("edit", genre.oui_modifier), ("no", "Non (Esc)")], chosen)
|