opsiom-cli 1.0.0__tar.gz → 1.0.2__tar.gz
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.
- {opsiom_cli-1.0.0 → opsiom_cli-1.0.2}/PKG-INFO +1 -1
- {opsiom_cli-1.0.0 → opsiom_cli-1.0.2}/pyproject.toml +1 -1
- {opsiom_cli-1.0.0 → opsiom_cli-1.0.2}/src/opsiom_cli/cli.py +103 -35
- {opsiom_cli-1.0.0 → opsiom_cli-1.0.2}/src/opsiom_cli.egg-info/PKG-INFO +1 -1
- {opsiom_cli-1.0.0 → opsiom_cli-1.0.2}/README.md +0 -0
- {opsiom_cli-1.0.0 → opsiom_cli-1.0.2}/setup.cfg +0 -0
- {opsiom_cli-1.0.0 → opsiom_cli-1.0.2}/src/opsiom_cli/__init__.py +0 -0
- {opsiom_cli-1.0.0 → opsiom_cli-1.0.2}/src/opsiom_cli.egg-info/SOURCES.txt +0 -0
- {opsiom_cli-1.0.0 → opsiom_cli-1.0.2}/src/opsiom_cli.egg-info/dependency_links.txt +0 -0
- {opsiom_cli-1.0.0 → opsiom_cli-1.0.2}/src/opsiom_cli.egg-info/entry_points.txt +0 -0
- {opsiom_cli-1.0.0 → opsiom_cli-1.0.2}/src/opsiom_cli.egg-info/requires.txt +0 -0
- {opsiom_cli-1.0.0 → opsiom_cli-1.0.2}/src/opsiom_cli.egg-info/top_level.txt +0 -0
|
@@ -33,8 +33,53 @@ except ImportError:
|
|
|
33
33
|
print("Ce CLI nécessite le paquet 'requests'. Installe-le avec : pip install requests")
|
|
34
34
|
sys.exit(1)
|
|
35
35
|
|
|
36
|
-
|
|
36
|
+
try:
|
|
37
|
+
import readline # active l'historique flèche-haut et son édition de ligne
|
|
38
|
+
except ImportError:
|
|
39
|
+
readline = None # non disponible nativement sur certains Windows (pyreadline3 comble le manque)
|
|
40
|
+
|
|
41
|
+
VERSION = "1.0.2"
|
|
37
42
|
CONFIG_PATH = Path.home() / ".config" / "opsiom" / "config.json"
|
|
43
|
+
HISTORY_PATH = Path.home() / ".local" / "share" / "opsiom" / "history"
|
|
44
|
+
TRANSCRIPTS_DIR = Path.home() / "opsiom-conversations"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def setup_history():
|
|
48
|
+
if not readline:
|
|
49
|
+
return
|
|
50
|
+
HISTORY_PATH.parent.mkdir(parents=True, exist_ok=True)
|
|
51
|
+
try:
|
|
52
|
+
readline.read_history_file(HISTORY_PATH)
|
|
53
|
+
except OSError:
|
|
54
|
+
pass
|
|
55
|
+
readline.set_history_length(500)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def save_history():
|
|
59
|
+
if not readline:
|
|
60
|
+
return
|
|
61
|
+
try:
|
|
62
|
+
readline.write_history_file(HISTORY_PATH)
|
|
63
|
+
except OSError:
|
|
64
|
+
pass
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def save_transcript(transcript, filename=None):
|
|
68
|
+
TRANSCRIPTS_DIR.mkdir(parents=True, exist_ok=True)
|
|
69
|
+
if not filename:
|
|
70
|
+
filename = f"opsiom-{time.strftime('%Y%m%d-%H%M%S')}"
|
|
71
|
+
if not filename.endswith(".md"):
|
|
72
|
+
filename += ".md"
|
|
73
|
+
path = TRANSCRIPTS_DIR / filename
|
|
74
|
+
lines = ["# Conversation Opsiom", ""]
|
|
75
|
+
for entry in transcript:
|
|
76
|
+
if entry["role"] == "user":
|
|
77
|
+
lines.append(f"**Vous :** {entry['text']}")
|
|
78
|
+
else:
|
|
79
|
+
lines.append(f"**Opsiom ({entry['model']}) :** {entry['text']} _({entry['elapsed_ms']} ms)_")
|
|
80
|
+
lines.append("")
|
|
81
|
+
path.write_text("\n".join(lines), encoding="utf-8")
|
|
82
|
+
return path
|
|
38
83
|
|
|
39
84
|
|
|
40
85
|
# ----------------------------------------------------------------------------
|
|
@@ -132,34 +177,34 @@ MOUTH_SAD = "⌢"
|
|
|
132
177
|
MOUTH_OPEN = "○"
|
|
133
178
|
|
|
134
179
|
TENTACLES_REST = [
|
|
135
|
-
" / /
|
|
136
|
-
" ( (
|
|
137
|
-
" \\ \\
|
|
138
|
-
" `._\\
|
|
180
|
+
" / / | \\ \\",
|
|
181
|
+
" ( ( | ) )",
|
|
182
|
+
" \\ \\ | / /",
|
|
183
|
+
" `._\\ | /_.'",
|
|
139
184
|
]
|
|
140
185
|
TENTACLES_REST_CUPS = [
|
|
141
|
-
" / /
|
|
142
|
-
" ( (
|
|
143
|
-
" \\ \\
|
|
144
|
-
" `._\\
|
|
186
|
+
" / / o|o \\ \\",
|
|
187
|
+
" ( ( | ) )",
|
|
188
|
+
" \\ \\ o|o / /",
|
|
189
|
+
" `._\\ | /_.'",
|
|
145
190
|
]
|
|
146
191
|
TENTACLES_LEFT = [
|
|
147
|
-
"
|
|
148
|
-
"
|
|
149
|
-
"
|
|
150
|
-
"
|
|
192
|
+
" \\ / | \\ /",
|
|
193
|
+
" ( ( | ) )",
|
|
194
|
+
" / \\ | / \\",
|
|
195
|
+
" -' `._ | _.' `- ",
|
|
151
196
|
]
|
|
152
197
|
TENTACLES_RIGHT = [
|
|
153
|
-
" / \\
|
|
154
|
-
" (
|
|
155
|
-
" \\ /
|
|
156
|
-
"
|
|
198
|
+
" / \\ | / \\",
|
|
199
|
+
" ( ) | ( ) ",
|
|
200
|
+
" \\ / | \\ /",
|
|
201
|
+
" `-' _|_ `-'",
|
|
157
202
|
]
|
|
158
203
|
TENTACLES_DROOP = [
|
|
159
|
-
"
|
|
160
|
-
"
|
|
161
|
-
"
|
|
162
|
-
"
|
|
204
|
+
" ~ / \\ | / \\ ~",
|
|
205
|
+
" ~ ( ) | ( ) ~",
|
|
206
|
+
" ~ \\ / | \\ / ~",
|
|
207
|
+
" * `.|.' *",
|
|
163
208
|
]
|
|
164
209
|
|
|
165
210
|
|
|
@@ -220,10 +265,10 @@ FRAMES_GREETING = [
|
|
|
220
265
|
" | ⌣ |",
|
|
221
266
|
" \\ /",
|
|
222
267
|
" '─..-----..─'",
|
|
223
|
-
" / /
|
|
224
|
-
" ( (
|
|
225
|
-
" \\ \\
|
|
226
|
-
" `._\\
|
|
268
|
+
" / / | \\ \\",
|
|
269
|
+
" ( ( | ) )",
|
|
270
|
+
" \\ \\ | / /",
|
|
271
|
+
" `._\\ | /_.'",
|
|
227
272
|
"",
|
|
228
273
|
],
|
|
229
274
|
[
|
|
@@ -235,10 +280,10 @@ FRAMES_GREETING = [
|
|
|
235
280
|
" | ⌣ |",
|
|
236
281
|
" \\ /",
|
|
237
282
|
" '─..-----..─'",
|
|
238
|
-
" / /
|
|
239
|
-
" ( (
|
|
240
|
-
" \\ \\
|
|
241
|
-
" `._\\
|
|
283
|
+
" / / | \\ \\",
|
|
284
|
+
" ( ( | ) )",
|
|
285
|
+
" \\ \\ | / /",
|
|
286
|
+
" `._\\ | /_.'",
|
|
242
287
|
"",
|
|
243
288
|
],
|
|
244
289
|
build_frame("", EYES_WINK, MOUTH_NEUTRAL, TENTACLES_REST),
|
|
@@ -270,11 +315,12 @@ TITLE = [
|
|
|
270
315
|
# ----------------------------------------------------------------------------
|
|
271
316
|
|
|
272
317
|
class OctopusAnimation:
|
|
273
|
-
def __init__(self, frames, label=None, interval=0.2, loop=True):
|
|
318
|
+
def __init__(self, frames, label=None, interval=0.2, loop=True, dynamic=None):
|
|
274
319
|
self.frames = frames
|
|
275
320
|
self.label = label
|
|
276
321
|
self.interval = interval
|
|
277
322
|
self.loop = loop
|
|
323
|
+
self.dynamic = dynamic # callable optionnel -> suffixe texte (ex: minuteur en direct)
|
|
278
324
|
self._stop_event = threading.Event()
|
|
279
325
|
self._thread = None
|
|
280
326
|
|
|
@@ -282,7 +328,7 @@ class OctopusAnimation:
|
|
|
282
328
|
return itertools.cycle(self.frames) if self.loop else iter(self.frames)
|
|
283
329
|
|
|
284
330
|
def _run(self):
|
|
285
|
-
dots = itertools.cycle([" ", ". ", ".. ", "..."]) if self.label else None
|
|
331
|
+
dots = itertools.cycle([" ", ". ", ".. ", "..."]) if (self.label and not self.dynamic) else None
|
|
286
332
|
n_lines = FRAME_HEIGHT + (1 if self.label else 0)
|
|
287
333
|
print(HIDE_CURSOR, end="")
|
|
288
334
|
first = True
|
|
@@ -295,7 +341,8 @@ class OctopusAnimation:
|
|
|
295
341
|
for line in gradient_lines(frame):
|
|
296
342
|
sys.stdout.write(line + "\033[K\n")
|
|
297
343
|
if self.label:
|
|
298
|
-
|
|
344
|
+
suffix = self.dynamic() if self.dynamic else next(dots)
|
|
345
|
+
sys.stdout.write(rgb(ACCENT, f" {self.label}{suffix}") + "\033[K\n")
|
|
299
346
|
sys.stdout.flush()
|
|
300
347
|
time.sleep(self.interval)
|
|
301
348
|
sys.stdout.write(f"\033[{n_lines}A")
|
|
@@ -438,9 +485,10 @@ Commandes disponibles :
|
|
|
438
485
|
/model <id|num> change directement de modèle (ex: /model small, /model 2)
|
|
439
486
|
/models liste les modèles disponibles
|
|
440
487
|
/status réaffiche le panneau de connexion
|
|
488
|
+
/save [nom] exporte la conversation en Markdown (~/opsiom-conversations/)
|
|
441
489
|
/clear efface l'écran
|
|
442
490
|
/help affiche cette aide
|
|
443
|
-
/quit, /exit quitte le CLI
|
|
491
|
+
/quit, /exit quitte le CLI (Ctrl+C pendant une réponse : annule juste cette requête)
|
|
444
492
|
"""
|
|
445
493
|
|
|
446
494
|
|
|
@@ -524,6 +572,8 @@ def select_model_interactively(models_payload, current_model):
|
|
|
524
572
|
|
|
525
573
|
|
|
526
574
|
def run_chat(client: OpsiomClient, url, api_key):
|
|
575
|
+
setup_history()
|
|
576
|
+
transcript = []
|
|
527
577
|
current_model = None
|
|
528
578
|
models_payload = {"models": []}
|
|
529
579
|
connected = False
|
|
@@ -574,6 +624,12 @@ def run_chat(client: OpsiomClient, url, api_key):
|
|
|
574
624
|
sys.stdout.flush()
|
|
575
625
|
elif cmd == "status":
|
|
576
626
|
print_status_panel(url, api_key, connected, models_loaded)
|
|
627
|
+
elif cmd == "save":
|
|
628
|
+
if not transcript:
|
|
629
|
+
print(rgb(MUTED, " rien à enregistrer pour l'instant."))
|
|
630
|
+
else:
|
|
631
|
+
path = save_transcript(transcript, arg or None)
|
|
632
|
+
print(rgb(ACCENT, f"→ conversation enregistrée dans {path}"))
|
|
577
633
|
elif cmd == "models":
|
|
578
634
|
try:
|
|
579
635
|
models_payload = client.list_models()
|
|
@@ -591,11 +647,20 @@ def run_chat(client: OpsiomClient, url, api_key):
|
|
|
591
647
|
print_error(f"commande inconnue : /{cmd}")
|
|
592
648
|
continue
|
|
593
649
|
|
|
594
|
-
|
|
595
|
-
|
|
650
|
+
transcript.append({"role": "user", "text": user_input})
|
|
651
|
+
|
|
596
652
|
start_time = time.perf_counter()
|
|
653
|
+
anim = OctopusAnimation(
|
|
654
|
+
FRAMES_THINKING, label="Opsiom réfléchit", interval=0.2,
|
|
655
|
+
dynamic=lambda: f" {time.perf_counter() - start_time:0.1f}s",
|
|
656
|
+
)
|
|
657
|
+
anim.start()
|
|
597
658
|
try:
|
|
598
659
|
result = client.chat(user_input, model=current_model)
|
|
660
|
+
except KeyboardInterrupt:
|
|
661
|
+
anim.stop()
|
|
662
|
+
print(rgb(MUTED, " (requête annulée — le serveur continue peut-être de calculer en arrière-plan)"))
|
|
663
|
+
continue
|
|
599
664
|
except requests.RequestException as e:
|
|
600
665
|
anim.stop()
|
|
601
666
|
show_error_animation()
|
|
@@ -609,6 +674,9 @@ def run_chat(client: OpsiomClient, url, api_key):
|
|
|
609
674
|
used_model = result.get("model", current_model or "?")
|
|
610
675
|
print_assistant_message(response_text, used_model, elapsed_ms)
|
|
611
676
|
print_separator()
|
|
677
|
+
transcript.append({"role": "assistant", "text": response_text, "model": used_model, "elapsed_ms": elapsed_ms})
|
|
678
|
+
|
|
679
|
+
save_history()
|
|
612
680
|
|
|
613
681
|
|
|
614
682
|
# ----------------------------------------------------------------------------
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|