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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: opsiom-cli
3
- Version: 1.0.0
3
+ Version: 1.0.2
4
4
  Summary: CLI de chat pour Opsiom, l'assistant IA francophone.
5
5
  Author-email: Ton nom <toi@example.com>
6
6
  License-Expression: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "opsiom-cli"
7
- version = "1.0.0"
7
+ version = "1.0.2"
8
8
  description = "CLI de chat pour Opsiom, l'assistant IA francophone."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -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
- VERSION = "1.0.0"
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
- " / / o \\ \\",
142
- " ( ( | ) )",
143
- " \\ \\ o / /",
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
- sys.stdout.write(rgb(ACCENT, f" {self.label}{next(dots)}") + "\033[K\n")
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
- anim = OctopusAnimation(FRAMES_THINKING, label="Opsiom réfléchit", interval=0.2)
595
- anim.start()
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
  # ----------------------------------------------------------------------------
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: opsiom-cli
3
- Version: 1.0.0
3
+ Version: 1.0.2
4
4
  Summary: CLI de chat pour Opsiom, l'assistant IA francophone.
5
5
  Author-email: Ton nom <toi@example.com>
6
6
  License-Expression: MIT
File without changes
File without changes