@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,177 @@
|
|
|
1
|
+
"""Barre d'état, spinner et affichage dans la conversation."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import random
|
|
5
|
+
import time
|
|
6
|
+
|
|
7
|
+
from rich.text import Text
|
|
8
|
+
from textual.widgets import Markdown, Static
|
|
9
|
+
|
|
10
|
+
from ..accueil import Accueil
|
|
11
|
+
from ..textes import SPINNER, VERBS
|
|
12
|
+
from ..theme import AGENT_COLOR, CODE_COLOR, DIM, MEMORY_COLOR, ORANGE, PLAN_COLOR, RED, SHELL_COLOR, YELLOW
|
|
13
|
+
from ..widgets import AgentView, AssistantMessage, SearchView, ToolCallView
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Ecran:
|
|
17
|
+
"""Partie de PrometheeApp : barre d'état, spinner et affichage dans la conversation."""
|
|
18
|
+
|
|
19
|
+
# ------------------------------------------------------------------ barre d'état
|
|
20
|
+
def set_hints(self, text: str | None = None, warn: bool = False) -> None:
|
|
21
|
+
hints = self.query_one("#hints", Static)
|
|
22
|
+
if text:
|
|
23
|
+
hints.update(Text(text, style=f"bold {YELLOW}" if warn else DIM))
|
|
24
|
+
elif self._perm is not None:
|
|
25
|
+
hints.update(Text(self._perm.hint, style=DIM))
|
|
26
|
+
elif self._comp_items:
|
|
27
|
+
what = "lancer" if self._comp_kind == "/" else "joindre"
|
|
28
|
+
hints.update(Text(f"↑↓ choisir · Tab compléter · Entrée {what} · Esc fermer", style=DIM))
|
|
29
|
+
elif self.input.text.startswith("!"):
|
|
30
|
+
hints.update(Text("! mode commande : une lecture part directement, le reste te demande une confirmation",
|
|
31
|
+
style=SHELL_COLOR))
|
|
32
|
+
elif self._agent_draft is not None:
|
|
33
|
+
what = ("Nouvel agent : décris son rôle et quand l'utiliser" if self._draft_kind == "agent"
|
|
34
|
+
else "Nouvelle commande : décris ce qu'elle doit faire")
|
|
35
|
+
hints.update(Text(f"{what}, puis Entrée · Esc annuler", style=AGENT_COLOR))
|
|
36
|
+
elif self.input.text.startswith("#"):
|
|
37
|
+
hints.update(Text("# mode mémoire : Entrée enregistre cette note (mémoire globale ou du dossier)",
|
|
38
|
+
style=MEMORY_COLOR))
|
|
39
|
+
elif self.plan_mode:
|
|
40
|
+
hints.update(Text("⏸ mode plan : Prométhée propose un plan sans rien exécuter · Shift+Tab pour quitter",
|
|
41
|
+
style=PLAN_COLOR))
|
|
42
|
+
else:
|
|
43
|
+
hints.update(Text("/aide · Shift+Tab plan · Esc interrompre · Ctrl+C ×2 quitter", style=DIM))
|
|
44
|
+
|
|
45
|
+
def update_status(self) -> None:
|
|
46
|
+
parts = Text(style=DIM)
|
|
47
|
+
if self.generating and self._turn_model and self._turn_model != self.model:
|
|
48
|
+
parts.append(f"{self._turn_model} (code)", style=CODE_COLOR)
|
|
49
|
+
else:
|
|
50
|
+
parts.append(self.model, style=ORANGE)
|
|
51
|
+
if self.model_state:
|
|
52
|
+
parts.append(f" · {self.model_state}")
|
|
53
|
+
elif not self.supports_tools:
|
|
54
|
+
parts.append(" · sans commandes")
|
|
55
|
+
# Politique : « stricte » au repos, « souple » et « validation manuelle » signalées (un mot, pas seulement une couleur)
|
|
56
|
+
politique = self.politique
|
|
57
|
+
relaxed = politique.libelle() != "politique stricte"
|
|
58
|
+
parts.append(" · ")
|
|
59
|
+
parts.append(("▲ " if relaxed else "") + politique.libelle(), style=YELLOW if relaxed else DIM)
|
|
60
|
+
if self.num_ctx:
|
|
61
|
+
pct = min(100, round(100 * self.ctx_used / self.num_ctx))
|
|
62
|
+
style = f"bold {RED}" if pct >= 90 else YELLOW if pct >= 75 else DIM
|
|
63
|
+
parts.append(" · ")
|
|
64
|
+
if self.size.width >= 132:
|
|
65
|
+
filled = min(8, round(8 * pct / 100))
|
|
66
|
+
parts.append("▰" * filled, style=RED if pct >= 90 else YELLOW if pct >= 75 else ORANGE)
|
|
67
|
+
parts.append("▱" * (8 - filled) + " ", style=DIM)
|
|
68
|
+
parts.append(f"contexte {pct} %", style=style)
|
|
69
|
+
if pct >= 90:
|
|
70
|
+
parts.append(" (/compact conseillé)", style=f"bold {RED}")
|
|
71
|
+
if self.last_tps:
|
|
72
|
+
parts.append(f" · {self.last_tps:.0f} tok/s")
|
|
73
|
+
self.query_one("#status", Static).update(parts)
|
|
74
|
+
for panel in self.query(Accueil):
|
|
75
|
+
panel.rafraichir() # l'accueil montre le même état : modèle, politique
|
|
76
|
+
|
|
77
|
+
def _tick_spinner(self) -> None:
|
|
78
|
+
if not self.generating or self._perm is not None:
|
|
79
|
+
return
|
|
80
|
+
self._follow()
|
|
81
|
+
now = time.monotonic()
|
|
82
|
+
frame = SPINNER[int(now * 8) % len(SPINNER)]
|
|
83
|
+
elapsed = int(now - self.spin_start)
|
|
84
|
+
if self.spin_mode == "command":
|
|
85
|
+
verb, detail, stop = "Exécution de la commande", f"{elapsed}s", "Esc pour l'arrêter"
|
|
86
|
+
elif self.spin_mode == "check":
|
|
87
|
+
verb, detail, stop = self.spin_verb, f"{elapsed}s", "Esc pour interrompre"
|
|
88
|
+
elif self.spin_mode == "agent":
|
|
89
|
+
verb = self.spin_verb
|
|
90
|
+
detail = f"{elapsed}s" + (f" · ↓ {self.spin_tokens} tokens" if self.spin_tokens else "")
|
|
91
|
+
stop = "Esc pour interrompre"
|
|
92
|
+
else:
|
|
93
|
+
verb = "Réflexion" if self.spin_thinking and not self.spin_tokens else self.spin_verb
|
|
94
|
+
detail = f"{elapsed}s" + (f" · ↓ {self.spin_tokens} tokens" if self.spin_tokens else "")
|
|
95
|
+
stop = "Esc pour interrompre"
|
|
96
|
+
self.query_one("#spinner", Static).update(
|
|
97
|
+
Text.assemble((f"{frame} ", ORANGE), (f"{verb}…", f"bold {ORANGE}"), (f" ({detail} · {stop})", DIM))
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
def _spinner(self, active: bool, mode: str | None = None, verb: str = "") -> None:
|
|
101
|
+
if mode:
|
|
102
|
+
self.spin_mode = mode
|
|
103
|
+
self.spin_start = time.monotonic()
|
|
104
|
+
if mode == "model":
|
|
105
|
+
self.spin_tokens = 0
|
|
106
|
+
self.spin_thinking = False
|
|
107
|
+
self.spin_verb = random.choice(VERBS)
|
|
108
|
+
elif mode == "check":
|
|
109
|
+
self.spin_verb = verb or "Vérification"
|
|
110
|
+
elif mode == "agent":
|
|
111
|
+
self.spin_tokens = 0
|
|
112
|
+
self.spin_verb = verb or "Agent au travail"
|
|
113
|
+
spinner = self.query_one("#spinner", Static)
|
|
114
|
+
spinner.set_class(active, "-active")
|
|
115
|
+
if active:
|
|
116
|
+
self._tick_spinner()
|
|
117
|
+
|
|
118
|
+
# ------------------------------------------------------------------ affichage
|
|
119
|
+
@staticmethod
|
|
120
|
+
def user_widget(text: str) -> Static:
|
|
121
|
+
lines = text.splitlines() or [text]
|
|
122
|
+
shown = lines if len(lines) <= 12 else lines[:8]
|
|
123
|
+
body = Text()
|
|
124
|
+
for i, line in enumerate(shown):
|
|
125
|
+
body.append("> " if i == 0 else " ", style=f"bold {DIM}")
|
|
126
|
+
body.append(line + ("\n" if i < len(shown) - 1 else ""), style=DIM)
|
|
127
|
+
if len(lines) > len(shown):
|
|
128
|
+
body.append(f"\n … [+{len(lines) - len(shown)} lignes]", style=f"italic {DIM}")
|
|
129
|
+
return Static(body, classes="user")
|
|
130
|
+
|
|
131
|
+
def add_user(self, text: str) -> None:
|
|
132
|
+
self.chat.mount(self.user_widget(text))
|
|
133
|
+
self.call_after_refresh(self._follow)
|
|
134
|
+
|
|
135
|
+
def add_notice(self, text: str, style: str = DIM) -> None:
|
|
136
|
+
self.chat.mount(Static(Text.assemble(("⎿ ", DIM), (text, style)), classes="notice"))
|
|
137
|
+
self.call_after_refresh(self._follow)
|
|
138
|
+
|
|
139
|
+
def add_notice_md(self, markdown: str) -> None:
|
|
140
|
+
self.chat.mount(Markdown(markdown, classes="notice-md"))
|
|
141
|
+
self.call_after_refresh(self._follow)
|
|
142
|
+
self.set_timer(0.2, self._follow) # un tableau Markdown s'affiche un peu après : suivre sa vraie hauteur
|
|
143
|
+
|
|
144
|
+
async def _new_bubble(self, text: str = "") -> AssistantMessage:
|
|
145
|
+
bubble = AssistantMessage(text)
|
|
146
|
+
await self.chat.mount(bubble)
|
|
147
|
+
self.call_after_refresh(self._follow)
|
|
148
|
+
return bubble
|
|
149
|
+
|
|
150
|
+
async def _new_tool_view(self, command: str, label: str = "PowerShell",
|
|
151
|
+
waiting: str = "En attente de ta validation…") -> ToolCallView:
|
|
152
|
+
view = ToolCallView(command, label, waiting)
|
|
153
|
+
# Pendant le travail d'un agent, ses outils s'affichent en retrait dans son cadre
|
|
154
|
+
await (self._tool_parent or self.chat).mount(view)
|
|
155
|
+
self.call_after_refresh(self._follow)
|
|
156
|
+
return view
|
|
157
|
+
|
|
158
|
+
async def _new_search_view(self, requete: str) -> SearchView:
|
|
159
|
+
view = SearchView(requete)
|
|
160
|
+
await self.chat.mount(view)
|
|
161
|
+
self.call_after_refresh(self._follow)
|
|
162
|
+
return view
|
|
163
|
+
|
|
164
|
+
async def _new_agent_view(self, name: str, task: str) -> AgentView:
|
|
165
|
+
view = AgentView(name, task)
|
|
166
|
+
await self.chat.mount(view)
|
|
167
|
+
self.call_after_refresh(self._follow)
|
|
168
|
+
return view
|
|
169
|
+
|
|
170
|
+
def action_toggle_details(self) -> None:
|
|
171
|
+
"""Ctrl+O : afficher ou replier le détail du travail des agents."""
|
|
172
|
+
views = [v for v in self.query(AgentView) if v.done]
|
|
173
|
+
if not views:
|
|
174
|
+
return
|
|
175
|
+
self.show_details = not self.show_details
|
|
176
|
+
for view in views:
|
|
177
|
+
view.set_expanded(self.show_details)
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
"""Envoi d'un message, mode « ! », message système et estimation du contexte."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import datetime as dt
|
|
5
|
+
import json
|
|
6
|
+
import time
|
|
7
|
+
|
|
8
|
+
from textual import work
|
|
9
|
+
|
|
10
|
+
from ..agents import agent_tool_def, agents_guide, list_agents
|
|
11
|
+
from ..completion import expand_attachments
|
|
12
|
+
from ..consignes import TOOLS, TOOL_GUIDE
|
|
13
|
+
from ..memoire import MEMORY_INTRO, read_memory
|
|
14
|
+
from ..recherche import search_answer_rules
|
|
15
|
+
from ..securite import is_dangerous
|
|
16
|
+
from ..shell import shell_message
|
|
17
|
+
from ..theme import DIM, YELLOW
|
|
18
|
+
from ..widgets import ToolCallView
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class Envoi:
|
|
22
|
+
"""Partie de PrometheeApp : envoi d'un message, mode « ! », message système et estimation du contexte."""
|
|
23
|
+
|
|
24
|
+
def send_message(self, text: str, content: str | None = None, intent: str | None = None,
|
|
25
|
+
mode: str | None = None) -> None:
|
|
26
|
+
"""Envoie un message (text = ce qui s'affiche ; content = ce que reçoit le modèle, si différent ;
|
|
27
|
+
intent = ce que lit l'interpréteur à la place du texte affiché ; mode = « action » imposé)."""
|
|
28
|
+
if self.generating:
|
|
29
|
+
self.notify("Prométhée travaille encore. Esc pour l'interrompre.", severity="warning")
|
|
30
|
+
return
|
|
31
|
+
if self.tasks and all(t["state"] == "done" for t in self.tasks):
|
|
32
|
+
self.tasks = [] # la tâche précédente est finie
|
|
33
|
+
self.add_user(text)
|
|
34
|
+
if not self.session_title:
|
|
35
|
+
self.session_title = " ".join(text.split())[:70]
|
|
36
|
+
expanded, notes = expand_attachments(content or text, self.cwd)
|
|
37
|
+
for shown, summary, ok in notes:
|
|
38
|
+
self.add_notice(f"Lu {shown} ({summary})" if ok else f"@{shown} : {summary}", DIM if ok else YELLOW)
|
|
39
|
+
message = {"role": "user", "content": expanded}
|
|
40
|
+
if expanded != text:
|
|
41
|
+
message["display"] = text # affiché (et utilisé par l'interpréteur) à la place du contenu complet
|
|
42
|
+
if intent:
|
|
43
|
+
message["intent"] = intent # commande personnalisée, /init : l'interpréteur lit la vraie demande
|
|
44
|
+
if mode:
|
|
45
|
+
message["mode"] = mode
|
|
46
|
+
self.messages.append(message)
|
|
47
|
+
self.generating = True
|
|
48
|
+
self.cancel_event.clear()
|
|
49
|
+
self._run_plan = self.plan_mode
|
|
50
|
+
self._spinner(True, "model")
|
|
51
|
+
self.render_tasks()
|
|
52
|
+
self.run_agent(list(self.messages), self.model, self.plan_mode)
|
|
53
|
+
|
|
54
|
+
@work(thread=True, exclusive=True, group="generate")
|
|
55
|
+
def run_shell(self, command: str, view: ToolCallView) -> None:
|
|
56
|
+
"""« ! commande » : tapée par toi. Une lecture part tout de suite ; une commande sensible, risquée ou hors
|
|
57
|
+
politique te demande une confirmation ; un refus de la politique vaut aussi pour toi."""
|
|
58
|
+
verdict = self._verdict(command)
|
|
59
|
+
if verdict is not None and verdict.refusee:
|
|
60
|
+
self.call_from_thread(view.set_refused, verdict.resume(), verdict.conseils())
|
|
61
|
+
self.noter_refus(command, verdict, "toi")
|
|
62
|
+
self.call_from_thread(self.finish_shell, command, None, "", "", False)
|
|
63
|
+
return
|
|
64
|
+
if is_dangerous(command) or (verdict is not None and not verdict.lecture):
|
|
65
|
+
decision = self._wait_choice(lambda done: self.show_permission(command, "", done, verdict, direct=True))
|
|
66
|
+
if decision != "yes":
|
|
67
|
+
self.call_from_thread(view.set_denied)
|
|
68
|
+
self.noter_commande(command, None, "", "annulée par toi", time.monotonic(), self.cwd, verdict, "refus", "toi")
|
|
69
|
+
self.call_from_thread(self.finish_shell, command, None, "", "", False)
|
|
70
|
+
return
|
|
71
|
+
self.call_from_thread(view.set_running)
|
|
72
|
+
self.call_from_thread(self._spinner, True, "command")
|
|
73
|
+
started, where = time.monotonic(), self.cwd
|
|
74
|
+
code, output, note = self._execute(command, verdict)
|
|
75
|
+
self.noter_commande(command, code, output, note, started, where, verdict, "toi", "toi")
|
|
76
|
+
self.last_output = output
|
|
77
|
+
self.call_from_thread(view.set_result, code, output, note)
|
|
78
|
+
self.call_from_thread(self.finish_shell, command, code, output, note)
|
|
79
|
+
|
|
80
|
+
def finish_shell(self, command: str, code: int | None, output: str, note: str, ran: bool = True) -> None:
|
|
81
|
+
self.generating = False
|
|
82
|
+
self._spinner(False)
|
|
83
|
+
if self._perm is not None:
|
|
84
|
+
self.hide_permission()
|
|
85
|
+
if not ran:
|
|
86
|
+
return # refusée par la politique ou annulée : rien n'a tourné, rien n'entre dans la conversation
|
|
87
|
+
self.messages.append({"role": "user", "content": shell_message(command, code, output, note)})
|
|
88
|
+
if not self.session_title:
|
|
89
|
+
self.session_title = f"! {command}"[:70]
|
|
90
|
+
self.ctx_used = max(self.ctx_used, self.estimate_tokens())
|
|
91
|
+
self.update_status()
|
|
92
|
+
self.save_session()
|
|
93
|
+
self.call_after_refresh(self._follow)
|
|
94
|
+
self._maybe_auto_compact()
|
|
95
|
+
|
|
96
|
+
def memory_text(self) -> str:
|
|
97
|
+
memory = read_memory(self.cwd)
|
|
98
|
+
if not memory:
|
|
99
|
+
return ""
|
|
100
|
+
return MEMORY_INTRO + "".join(f"\n\n### Mémoire {label}\n{text}" for label, _, text in memory)
|
|
101
|
+
|
|
102
|
+
def _system_extra(self) -> str:
|
|
103
|
+
"""Mémoire et résumé de la conversation compactée, ajoutés aux consignes du modèle."""
|
|
104
|
+
extra = self.memory_text()
|
|
105
|
+
if self.summary:
|
|
106
|
+
extra += f"\n\n## Résumé de la conversation précédente (compactée)\n{self.summary}"
|
|
107
|
+
return extra
|
|
108
|
+
|
|
109
|
+
def search_system(self, search: dict) -> dict:
|
|
110
|
+
"""Consignes de la réponse qui suit une recherche web, comme la console : celles du modèle sans le guide des
|
|
111
|
+
outils (aucune commande dans ce tour), puis les consignes de la recherche."""
|
|
112
|
+
base = (self.model_system or "") + self._system_extra()
|
|
113
|
+
rules = search_answer_rules(search)
|
|
114
|
+
return {"role": "system", "content": f"{base}\n\n{rules}" if base else rules}
|
|
115
|
+
|
|
116
|
+
def system_message(self) -> dict | None:
|
|
117
|
+
extra = self._system_extra()
|
|
118
|
+
if not self.supports_tools:
|
|
119
|
+
if not extra:
|
|
120
|
+
return None # le SYSTEM du Modelfile s'applique
|
|
121
|
+
return {"role": "system", "content": (self.model_system or "") + extra}
|
|
122
|
+
guide = TOOL_GUIDE.format(cwd=self.cwd, date=dt.date.today().strftime("%d/%m/%Y"))
|
|
123
|
+
agents = list_agents(self.cwd)
|
|
124
|
+
if agents:
|
|
125
|
+
guide += agents_guide(agents)
|
|
126
|
+
return {"role": "system", "content": (self.model_system or "") + guide + extra}
|
|
127
|
+
|
|
128
|
+
def request_tools(self) -> list[dict]:
|
|
129
|
+
"""Outils envoyés au modèle principal : commandes, fichiers, et lancer_agent s'il y a des agents."""
|
|
130
|
+
agents = list_agents(self.cwd)
|
|
131
|
+
return TOOLS + ([agent_tool_def(agents)] if agents else [])
|
|
132
|
+
|
|
133
|
+
def estimate_tokens(self) -> int:
|
|
134
|
+
"""Estimation de la taille du contexte (≈ 3 caractères par jeton + définitions des outils)."""
|
|
135
|
+
system = self.system_message() or {}
|
|
136
|
+
chars = len(system.get("content", ""))
|
|
137
|
+
for m in self.messages:
|
|
138
|
+
chars += len(str(m.get("content") or ""))
|
|
139
|
+
if m.get("tool_calls"):
|
|
140
|
+
chars += len(json.dumps(m["tool_calls"], ensure_ascii=False))
|
|
141
|
+
return int(chars / 3.0) + (1250 if self.supports_tools else 0)
|