opsiom-cli 1.5.2__tar.gz → 1.6.0__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.5.2 → opsiom_cli-1.6.0}/CHANGELOG.md +17 -1
- {opsiom_cli-1.5.2 → opsiom_cli-1.6.0}/PKG-INFO +66 -7
- {opsiom_cli-1.5.2 → opsiom_cli-1.6.0}/README.md +60 -6
- opsiom_cli-1.6.0/command.txt +4 -0
- {opsiom_cli-1.5.2 → opsiom_cli-1.6.0}/pyproject.toml +11 -2
- {opsiom_cli-1.5.2 → opsiom_cli-1.6.0}/src/opsiom_cli/chat.py +85 -34
- {opsiom_cli-1.5.2 → opsiom_cli-1.6.0}/src/opsiom_cli/cli.py +126 -29
- {opsiom_cli-1.5.2 → opsiom_cli-1.6.0}/src/opsiom_cli/clients.py +8 -24
- opsiom_cli-1.6.0/src/opsiom_cli/files.py +63 -0
- opsiom_cli-1.6.0/src/opsiom_cli/prompt.py +58 -0
- opsiom_cli-1.6.0/src/opsiom_cli/recipes.py +56 -0
- {opsiom_cli-1.5.2 → opsiom_cli-1.6.0}/src/opsiom_cli/ui.py +58 -11
- {opsiom_cli-1.5.2 → opsiom_cli-1.6.0}/tests/test_core.py +1 -0
- opsiom_cli-1.6.0/tests/test_features.py +113 -0
- {opsiom_cli-1.5.2 → opsiom_cli-1.6.0}/src/opsiom_cli/__init__.py +0 -0
- {opsiom_cli-1.5.2 → opsiom_cli-1.6.0}/src/opsiom_cli/__main__.py +0 -0
- {opsiom_cli-1.5.2 → opsiom_cli-1.6.0}/src/opsiom_cli/storage.py +0 -0
- {opsiom_cli-1.5.2 → opsiom_cli-1.6.0}/src/opsiom_cli/test.txt +0 -0
- {opsiom_cli-1.5.2 → opsiom_cli-1.6.0}/src/opsiom_cli/test_quoted.txt +0 -0
- {opsiom_cli-1.5.2 → opsiom_cli-1.6.0}/tests/test_http.py +0 -0
|
@@ -1,6 +1,20 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## 1.
|
|
3
|
+
## 1.6.0
|
|
4
|
+
|
|
5
|
+
### Ajouts
|
|
6
|
+
- **Fichiers en contexte** : `/fichier <chemin>` joint un fichier texte à la prochaine question (retiré après envoi). `/fichier` liste, `/fichier effacer` retire. Option `-f/--file` (répétable) en question unique. Taille estimée en tokens affichée.
|
|
7
|
+
- **Recettes réutilisables** : `opsiom recette` liste les recettes intégrées (résumé, correction, reformulation, traduction, explication, idées) ; `opsiom recette <nom> "texte"`, `-f fichier` ou un pipe. Recettes personnelles dans `~/.config/opsiom/recettes/<nom>.md`, qui remplacent une recette intégrée de même nom.
|
|
8
|
+
- **Rendu Markdown en direct** (titres, gras, listes, tableaux, blocs de code colorés) avec l’extra `pretty` (rich). Désactivable avec `OPSIOM_PLAIN=1`.
|
|
9
|
+
- **Complétion** des commandes, des identifiants de modèles (`/model`) et des chemins (`/fichier`), avec historique (extra `pretty`, prompt_toolkit). Sans l’extra, la saisie reste celle de readline.
|
|
10
|
+
- **Jauge de quota** dans l’en-tête et dans `/quota` (orange dès 70 %, rouge dès 90 %).
|
|
11
|
+
- Aide `/help` regroupée par thème.
|
|
12
|
+
|
|
13
|
+
### Changements
|
|
14
|
+
- `-m/--model` accepte un numéro, un nom partiel ou un identifiant, comme `/model`, et refuse les modèles hors forfait.
|
|
15
|
+
- Installation : `pip install "opsiom-cli[pretty]"` pour le rendu enrichi. Le CLI fonctionne sans cet extra (texte brut, readline).
|
|
16
|
+
|
|
17
|
+
## 1.5.0
|
|
4
18
|
|
|
5
19
|
### Ajouts
|
|
6
20
|
- Sous-commandes `opsiom doctor` (diagnostic), `opsiom paths` (emplacements des fichiers) et `opsiom configure`.
|
|
@@ -16,6 +30,8 @@
|
|
|
16
30
|
- `/save` : le nom de fichier est confiné au dossier des transcriptions (plus de `../` ni de chemin absolu).
|
|
17
31
|
- Fichiers `config.json` et `usage.json` écrits de façon atomique, avec droits 600.
|
|
18
32
|
- Une connexion Octix refusée n’est plus prise pour une réussite lorsque le Space redirige vers `/login`.
|
|
33
|
+
- Connexion Octix : le formulaire `/login` est chargé puis soumis comme dans un navigateur (cookie de session et champs cachés).
|
|
34
|
+
- Les caractères de contrôle insérés par erreur (par exemple par un Ctrl+V sous Windows) sont retirés du mot de passe.
|
|
19
35
|
- Une réponse interrompue avant l’évènement de fin est signalée comme erreur.
|
|
20
36
|
- Après une reconnexion Octix, le message est renvoyé automatiquement.
|
|
21
37
|
- Les réponses conservent leurs retours à la ligne et leur indentation (listes, blocs de code).
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: opsiom-cli
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.6.0
|
|
4
4
|
Summary: Interface en ligne de commande pour discuter avec Opsiom, l'assistant IA francophone.
|
|
5
5
|
Keywords: chatbot,cli,francophone,ia,opsiom
|
|
6
6
|
Classifier: Environment :: Console
|
|
@@ -13,7 +13,12 @@ Classifier: Topic :: Communications :: Chat
|
|
|
13
13
|
Requires-Python: >=3.9
|
|
14
14
|
Requires-Dist: requests>=2.31
|
|
15
15
|
Provides-Extra: dev
|
|
16
|
+
Requires-Dist: prompt-toolkit>=3; extra == 'dev'
|
|
16
17
|
Requires-Dist: pytest>=7; extra == 'dev'
|
|
18
|
+
Requires-Dist: rich>=13; extra == 'dev'
|
|
19
|
+
Provides-Extra: pretty
|
|
20
|
+
Requires-Dist: prompt-toolkit>=3; extra == 'pretty'
|
|
21
|
+
Requires-Dist: rich>=13; extra == 'pretty'
|
|
17
22
|
Description-Content-Type: text/markdown
|
|
18
23
|
|
|
19
24
|
# opsiom-cli
|
|
@@ -62,7 +67,9 @@ opsiom
|
|
|
62
67
|
| `opsiom` | Session interactive. |
|
|
63
68
|
| `opsiom -p "Résume ce texte"` | Pose une question, affiche la réponse et quitte. |
|
|
64
69
|
| `cat notes.txt \| opsiom` | Envoie le contenu de l’entrée standard comme question. |
|
|
65
|
-
| `opsiom -
|
|
70
|
+
| `opsiom -f <fichier> -p "…"` | Joint un fichier texte à la question (répétable avec `-f`). |
|
|
71
|
+
| `opsiom recette` | Liste les recettes. `opsiom recette <nom> "texte"` applique une recette. |
|
|
72
|
+
| `opsiom -m <modèle> …` | Choisit le modèle (numéro, identifiant ou nom partiel). |
|
|
66
73
|
| `opsiom configure` | Reconfigure le serveur et la clé d’accès. |
|
|
67
74
|
| `opsiom doctor` | Vérifie l’URL, le serveur, les modèles et le quota. |
|
|
68
75
|
| `opsiom paths` | Affiche les emplacements des fichiers locaux. |
|
|
@@ -81,15 +88,50 @@ Dans une session, tapez `"""` seul sur une ligne, écrivez votre texte, puis ter
|
|
|
81
88
|
| `/save [nom]` | Exporte la conversation en Markdown dans `~/opsiom-conversations/`. |
|
|
82
89
|
| `/new` | Repart d’une conversation vide. |
|
|
83
90
|
| `/clear` | Efface l’écran. |
|
|
91
|
+
| `/fichier [chemin\|effacer]` | Joint un fichier texte à la prochaine question. Sans argument, liste les fichiers en attente. |
|
|
84
92
|
| `/model [id ou numéro]` | Change de modèle. Sans argument, affiche un menu. |
|
|
85
93
|
| `/models` | Liste les modèles disponibles. |
|
|
86
94
|
| `/status` | Rappelle la connexion, le compte et le quota. |
|
|
87
|
-
| `/quota` | Affiche le quota restant aujourd’hui. |
|
|
95
|
+
| `/quota` | Affiche la jauge et le quota restant aujourd’hui. |
|
|
88
96
|
| `/doctor` | Lance le diagnostic depuis la session. |
|
|
89
97
|
| `/help` | Affiche l’aide. |
|
|
90
98
|
| `/quit` | Quitte (aussi `/exit`, ou Ctrl+D). |
|
|
91
99
|
|
|
92
|
-
Raccourcis : **Entrée** envoie, **Ctrl+C** annule la saisie ou la réponse en cours, **Ctrl+D** quitte. L’historique des saisies se parcourt avec les flèches.
|
|
100
|
+
Raccourcis : **Entrée** envoie, **Ctrl+C** annule la saisie ou la réponse en cours, **Ctrl+D** quitte. L’historique des saisies se parcourt avec les flèches. **Tab** complète les commandes, les modèles après `/model` et les chemins après `/fichier`.
|
|
101
|
+
|
|
102
|
+
### Joindre des fichiers
|
|
103
|
+
|
|
104
|
+
Un fichier texte (UTF-8, 200 Ko au maximum) peut être envoyé avec votre question :
|
|
105
|
+
|
|
106
|
+
```
|
|
107
|
+
/fichier rapport.md
|
|
108
|
+
Résume les points faibles de ce rapport.
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Le fichier est mis en attente, envoyé avec la prochaine question, puis retiré. Une estimation du nombre de tokens est affichée (≈ caractères / 4). Les fichiers binaires, trop volumineux ou non UTF-8 sont refusés avec un message explicite.
|
|
112
|
+
|
|
113
|
+
En question unique : `opsiom -f rapport.md -p "Résume-le"`. Sans `-p`, la question par défaut est « Résume ce document. ».
|
|
114
|
+
|
|
115
|
+
### Recettes
|
|
116
|
+
|
|
117
|
+
Les recettes sont des consignes réutilisables :
|
|
118
|
+
|
|
119
|
+
| Recette | Effet |
|
|
120
|
+
|---|---|
|
|
121
|
+
| `resume` | Résumé avec les points clés en liste. |
|
|
122
|
+
| `corriger` | Correction de l’orthographe, de la grammaire et de la ponctuation. |
|
|
123
|
+
| `reformuler` | Réécriture claire et professionnelle. |
|
|
124
|
+
| `traduire-en-anglais` | Traduction en anglais, ton conservé. |
|
|
125
|
+
| `expliquer` | Explication simple. |
|
|
126
|
+
| `idees` | Dix idées en lien avec le texte. |
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
opsiom recette resume -f article.md
|
|
130
|
+
cat notes.txt | opsiom recette corriger
|
|
131
|
+
opsiom recette traduire-en-anglais "Bonjour à tous"
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Pour ajouter les vôtres, créez un fichier `<nom>.md` dans le dossier affiché par `opsiom paths` (« recettes personnelles »). Un fichier de même nom remplace la recette intégrée.
|
|
93
135
|
|
|
94
136
|
## Configuration
|
|
95
137
|
|
|
@@ -111,7 +153,18 @@ Fichiers locaux (`opsiom paths` les affiche) :
|
|
|
111
153
|
- **historique de saisie** : pour la flèche du haut.
|
|
112
154
|
- **transcriptions** : dossier des exports `/save`.
|
|
113
155
|
|
|
114
|
-
### Affichage
|
|
156
|
+
### Affichage et extras
|
|
157
|
+
|
|
158
|
+
Le CLI fonctionne seul. Deux extras enrichissent l’expérience :
|
|
159
|
+
|
|
160
|
+
```bash
|
|
161
|
+
pip install "opsiom-cli[pretty]"
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
- **rich** : rendu Markdown en direct dans le terminal.
|
|
165
|
+
- **prompt_toolkit** : complétion des commandes, des modèles et des chemins, historique.
|
|
166
|
+
|
|
167
|
+
Sans ces paquets, les réponses s’affichent en texte brut et la saisie reste celle du terminal. `OPSIOM_PLAIN=1` désactive le rendu Markdown.
|
|
115
168
|
|
|
116
169
|
- Les couleurs sont désactivées si `NO_COLOR` est défini, si `TERM=dumb`, ou si la sortie est redirigée vers un fichier ou un pipe.
|
|
117
170
|
- Sur un terminal sans UTF-8, les symboles basculent sur de l’ASCII.
|
|
@@ -133,6 +186,9 @@ Le quota se réinitialise chaque jour. Utilisez `/quota` pour voir où vous en
|
|
|
133
186
|
**« modèle inconnu » ou « hors forfait »**
|
|
134
187
|
Tapez `/models` pour la liste. Un modèle marqué « hors forfait » n’est pas inclus dans votre offre.
|
|
135
188
|
|
|
189
|
+
**« fichier introuvable », « binaire », « non UTF-8 » ou « trop volumineux »**
|
|
190
|
+
Le fichier joint doit être un texte UTF-8 de 200 Ko au maximum. Convertissez-le ou découpez-le en plusieurs fichiers.
|
|
191
|
+
|
|
136
192
|
**Les accents ou les couleurs s’affichent mal**
|
|
137
193
|
Utilisez un terminal qui gère l’UTF-8 et les couleurs 256. Sous Windows, préférez Windows Terminal.
|
|
138
194
|
|
|
@@ -160,11 +216,14 @@ Structure :
|
|
|
160
216
|
|
|
161
217
|
```
|
|
162
218
|
src/opsiom_cli/
|
|
163
|
-
├── cli.py options, configuration, question unique, sous-commandes
|
|
219
|
+
├── cli.py options, configuration, question unique, recettes, sous-commandes
|
|
164
220
|
├── chat.py session interactive, commandes, diagnostic, erreurs
|
|
165
221
|
├── clients.py clients serveur API et Space Hugging Face, flux SSE
|
|
222
|
+
├── files.py fichiers joints : lecture, contrôles, mise en forme
|
|
223
|
+
├── recipes.py recettes intégrées et personnelles
|
|
224
|
+
├── prompt.py complétion et historique (prompt_toolkit, optionnel)
|
|
166
225
|
├── storage.py chemins, configuration, quota local, transcriptions
|
|
167
|
-
└── ui.py couleurs, symboles, indicateur d’attente,
|
|
226
|
+
└── ui.py couleurs, symboles, jauge, indicateur d’attente, rendu Markdown (rich, optionnel)
|
|
168
227
|
tests/ tests unitaires et tests de bout en bout (faux serveur)
|
|
169
228
|
```
|
|
170
229
|
|
|
@@ -44,7 +44,9 @@ opsiom
|
|
|
44
44
|
| `opsiom` | Session interactive. |
|
|
45
45
|
| `opsiom -p "Résume ce texte"` | Pose une question, affiche la réponse et quitte. |
|
|
46
46
|
| `cat notes.txt \| opsiom` | Envoie le contenu de l’entrée standard comme question. |
|
|
47
|
-
| `opsiom -
|
|
47
|
+
| `opsiom -f <fichier> -p "…"` | Joint un fichier texte à la question (répétable avec `-f`). |
|
|
48
|
+
| `opsiom recette` | Liste les recettes. `opsiom recette <nom> "texte"` applique une recette. |
|
|
49
|
+
| `opsiom -m <modèle> …` | Choisit le modèle (numéro, identifiant ou nom partiel). |
|
|
48
50
|
| `opsiom configure` | Reconfigure le serveur et la clé d’accès. |
|
|
49
51
|
| `opsiom doctor` | Vérifie l’URL, le serveur, les modèles et le quota. |
|
|
50
52
|
| `opsiom paths` | Affiche les emplacements des fichiers locaux. |
|
|
@@ -63,15 +65,50 @@ Dans une session, tapez `"""` seul sur une ligne, écrivez votre texte, puis ter
|
|
|
63
65
|
| `/save [nom]` | Exporte la conversation en Markdown dans `~/opsiom-conversations/`. |
|
|
64
66
|
| `/new` | Repart d’une conversation vide. |
|
|
65
67
|
| `/clear` | Efface l’écran. |
|
|
68
|
+
| `/fichier [chemin\|effacer]` | Joint un fichier texte à la prochaine question. Sans argument, liste les fichiers en attente. |
|
|
66
69
|
| `/model [id ou numéro]` | Change de modèle. Sans argument, affiche un menu. |
|
|
67
70
|
| `/models` | Liste les modèles disponibles. |
|
|
68
71
|
| `/status` | Rappelle la connexion, le compte et le quota. |
|
|
69
|
-
| `/quota` | Affiche le quota restant aujourd’hui. |
|
|
72
|
+
| `/quota` | Affiche la jauge et le quota restant aujourd’hui. |
|
|
70
73
|
| `/doctor` | Lance le diagnostic depuis la session. |
|
|
71
74
|
| `/help` | Affiche l’aide. |
|
|
72
75
|
| `/quit` | Quitte (aussi `/exit`, ou Ctrl+D). |
|
|
73
76
|
|
|
74
|
-
Raccourcis : **Entrée** envoie, **Ctrl+C** annule la saisie ou la réponse en cours, **Ctrl+D** quitte. L’historique des saisies se parcourt avec les flèches.
|
|
77
|
+
Raccourcis : **Entrée** envoie, **Ctrl+C** annule la saisie ou la réponse en cours, **Ctrl+D** quitte. L’historique des saisies se parcourt avec les flèches. **Tab** complète les commandes, les modèles après `/model` et les chemins après `/fichier`.
|
|
78
|
+
|
|
79
|
+
### Joindre des fichiers
|
|
80
|
+
|
|
81
|
+
Un fichier texte (UTF-8, 200 Ko au maximum) peut être envoyé avec votre question :
|
|
82
|
+
|
|
83
|
+
```
|
|
84
|
+
/fichier rapport.md
|
|
85
|
+
Résume les points faibles de ce rapport.
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Le fichier est mis en attente, envoyé avec la prochaine question, puis retiré. Une estimation du nombre de tokens est affichée (≈ caractères / 4). Les fichiers binaires, trop volumineux ou non UTF-8 sont refusés avec un message explicite.
|
|
89
|
+
|
|
90
|
+
En question unique : `opsiom -f rapport.md -p "Résume-le"`. Sans `-p`, la question par défaut est « Résume ce document. ».
|
|
91
|
+
|
|
92
|
+
### Recettes
|
|
93
|
+
|
|
94
|
+
Les recettes sont des consignes réutilisables :
|
|
95
|
+
|
|
96
|
+
| Recette | Effet |
|
|
97
|
+
|---|---|
|
|
98
|
+
| `resume` | Résumé avec les points clés en liste. |
|
|
99
|
+
| `corriger` | Correction de l’orthographe, de la grammaire et de la ponctuation. |
|
|
100
|
+
| `reformuler` | Réécriture claire et professionnelle. |
|
|
101
|
+
| `traduire-en-anglais` | Traduction en anglais, ton conservé. |
|
|
102
|
+
| `expliquer` | Explication simple. |
|
|
103
|
+
| `idees` | Dix idées en lien avec le texte. |
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
opsiom recette resume -f article.md
|
|
107
|
+
cat notes.txt | opsiom recette corriger
|
|
108
|
+
opsiom recette traduire-en-anglais "Bonjour à tous"
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Pour ajouter les vôtres, créez un fichier `<nom>.md` dans le dossier affiché par `opsiom paths` (« recettes personnelles »). Un fichier de même nom remplace la recette intégrée.
|
|
75
112
|
|
|
76
113
|
## Configuration
|
|
77
114
|
|
|
@@ -93,7 +130,18 @@ Fichiers locaux (`opsiom paths` les affiche) :
|
|
|
93
130
|
- **historique de saisie** : pour la flèche du haut.
|
|
94
131
|
- **transcriptions** : dossier des exports `/save`.
|
|
95
132
|
|
|
96
|
-
### Affichage
|
|
133
|
+
### Affichage et extras
|
|
134
|
+
|
|
135
|
+
Le CLI fonctionne seul. Deux extras enrichissent l’expérience :
|
|
136
|
+
|
|
137
|
+
```bash
|
|
138
|
+
pip install "opsiom-cli[pretty]"
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
- **rich** : rendu Markdown en direct dans le terminal.
|
|
142
|
+
- **prompt_toolkit** : complétion des commandes, des modèles et des chemins, historique.
|
|
143
|
+
|
|
144
|
+
Sans ces paquets, les réponses s’affichent en texte brut et la saisie reste celle du terminal. `OPSIOM_PLAIN=1` désactive le rendu Markdown.
|
|
97
145
|
|
|
98
146
|
- Les couleurs sont désactivées si `NO_COLOR` est défini, si `TERM=dumb`, ou si la sortie est redirigée vers un fichier ou un pipe.
|
|
99
147
|
- Sur un terminal sans UTF-8, les symboles basculent sur de l’ASCII.
|
|
@@ -115,6 +163,9 @@ Le quota se réinitialise chaque jour. Utilisez `/quota` pour voir où vous en
|
|
|
115
163
|
**« modèle inconnu » ou « hors forfait »**
|
|
116
164
|
Tapez `/models` pour la liste. Un modèle marqué « hors forfait » n’est pas inclus dans votre offre.
|
|
117
165
|
|
|
166
|
+
**« fichier introuvable », « binaire », « non UTF-8 » ou « trop volumineux »**
|
|
167
|
+
Le fichier joint doit être un texte UTF-8 de 200 Ko au maximum. Convertissez-le ou découpez-le en plusieurs fichiers.
|
|
168
|
+
|
|
118
169
|
**Les accents ou les couleurs s’affichent mal**
|
|
119
170
|
Utilisez un terminal qui gère l’UTF-8 et les couleurs 256. Sous Windows, préférez Windows Terminal.
|
|
120
171
|
|
|
@@ -142,11 +193,14 @@ Structure :
|
|
|
142
193
|
|
|
143
194
|
```
|
|
144
195
|
src/opsiom_cli/
|
|
145
|
-
├── cli.py options, configuration, question unique, sous-commandes
|
|
196
|
+
├── cli.py options, configuration, question unique, recettes, sous-commandes
|
|
146
197
|
├── chat.py session interactive, commandes, diagnostic, erreurs
|
|
147
198
|
├── clients.py clients serveur API et Space Hugging Face, flux SSE
|
|
199
|
+
├── files.py fichiers joints : lecture, contrôles, mise en forme
|
|
200
|
+
├── recipes.py recettes intégrées et personnelles
|
|
201
|
+
├── prompt.py complétion et historique (prompt_toolkit, optionnel)
|
|
148
202
|
├── storage.py chemins, configuration, quota local, transcriptions
|
|
149
|
-
└── ui.py couleurs, symboles, indicateur d’attente,
|
|
203
|
+
└── ui.py couleurs, symboles, jauge, indicateur d’attente, rendu Markdown (rich, optionnel)
|
|
150
204
|
tests/ tests unitaires et tests de bout en bout (faux serveur)
|
|
151
205
|
```
|
|
152
206
|
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "opsiom-cli"
|
|
7
|
-
version = "1.
|
|
7
|
+
version = "1.6.0"
|
|
8
8
|
description = "Interface en ligne de commande pour discuter avec Opsiom, l'assistant IA francophone."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.9"
|
|
@@ -26,7 +26,16 @@ dependencies = [
|
|
|
26
26
|
]
|
|
27
27
|
|
|
28
28
|
[project.optional-dependencies]
|
|
29
|
-
|
|
29
|
+
# Rendu Markdown dans le terminal et complétion des commandes.
|
|
30
|
+
pretty = [
|
|
31
|
+
"rich>=13",
|
|
32
|
+
"prompt_toolkit>=3",
|
|
33
|
+
]
|
|
34
|
+
dev = [
|
|
35
|
+
"pytest>=7",
|
|
36
|
+
"rich>=13",
|
|
37
|
+
"prompt_toolkit>=3",
|
|
38
|
+
]
|
|
30
39
|
|
|
31
40
|
[project.scripts]
|
|
32
41
|
opsiom = "opsiom_cli.cli:main"
|
|
@@ -13,6 +13,8 @@ import requests
|
|
|
13
13
|
|
|
14
14
|
from . import __version__, storage
|
|
15
15
|
from .clients import ServerError, error_detail
|
|
16
|
+
from .files import FileError, build_message, load_text_file
|
|
17
|
+
from .prompt import CommandCompleter, formatted, make_prompt_session
|
|
16
18
|
from .storage import (
|
|
17
19
|
get_quota,
|
|
18
20
|
is_insecure,
|
|
@@ -37,12 +39,13 @@ from .ui import (
|
|
|
37
39
|
print_logo,
|
|
38
40
|
print_ok,
|
|
39
41
|
print_warn,
|
|
42
|
+
quota_bar,
|
|
40
43
|
sym,
|
|
41
44
|
term_width,
|
|
42
45
|
)
|
|
43
46
|
|
|
44
47
|
try:
|
|
45
|
-
import readline # historique
|
|
48
|
+
import readline # historique et édition de ligne, sans prompt_toolkit
|
|
46
49
|
except ImportError: # absent sur certains Windows
|
|
47
50
|
readline = None
|
|
48
51
|
|
|
@@ -56,6 +59,7 @@ HELP_GROUPS = [
|
|
|
56
59
|
("Conversation", [
|
|
57
60
|
("/retry", "", "relance la dernière question"),
|
|
58
61
|
("/copy", "", "copie la dernière réponse dans le presse-papiers"),
|
|
62
|
+
("/fichier", "[chemin|effacer]", "joint un fichier texte à la prochaine question"),
|
|
59
63
|
("/save", "[nom]", "exporte la conversation en Markdown"),
|
|
60
64
|
("/new", "", "repart d’une conversation vide"),
|
|
61
65
|
("/clear", "", "efface l’écran"),
|
|
@@ -75,6 +79,10 @@ HELP_GROUPS = [
|
|
|
75
79
|
ALIASES = {"exit": "quit"}
|
|
76
80
|
|
|
77
81
|
|
|
82
|
+
def all_commands():
|
|
83
|
+
return [command for _title, items in HELP_GROUPS for command in items]
|
|
84
|
+
|
|
85
|
+
|
|
78
86
|
# --- Erreurs -----------------------------------------------------------------
|
|
79
87
|
|
|
80
88
|
def status_of(exc):
|
|
@@ -124,6 +132,17 @@ def record_usage(client, final):
|
|
|
124
132
|
record_quota(client.identity, {"remaining": final["remaining_quota"]})
|
|
125
133
|
|
|
126
134
|
|
|
135
|
+
def quota_line(client):
|
|
136
|
+
"""Jauge et détail du quota, ou « non communiqué » si le serveur ne l'a pas donné."""
|
|
137
|
+
state = get_quota(client.identity)
|
|
138
|
+
if not state["known"]:
|
|
139
|
+
return muted("non communiqué par le serveur")
|
|
140
|
+
if state["unlimited"]:
|
|
141
|
+
return "illimité"
|
|
142
|
+
detail = f"{state['used']}/{state['limit']} tokens aujourd’hui"
|
|
143
|
+
return f"{quota_bar(state['used'], state['limit'])} {muted(detail)}"
|
|
144
|
+
|
|
145
|
+
|
|
127
146
|
# --- Authentification (Space Hugging Face) -----------------------------------
|
|
128
147
|
|
|
129
148
|
def login_interactive(client, username=None, attempts=3):
|
|
@@ -170,7 +189,7 @@ def copy_to_clipboard(text):
|
|
|
170
189
|
return False
|
|
171
190
|
|
|
172
191
|
|
|
173
|
-
# --- Historique et saisie
|
|
192
|
+
# --- Historique et saisie (readline, si prompt_toolkit est absent) -----------
|
|
174
193
|
|
|
175
194
|
def rl_safe(text):
|
|
176
195
|
"""Entoure les séquences ANSI de \\x01…\\x02 pour que readline calcule
|
|
@@ -334,16 +353,18 @@ def _truncate(text, width):
|
|
|
334
353
|
|
|
335
354
|
|
|
336
355
|
class ChatSession:
|
|
337
|
-
def __init__(self, client, model=None):
|
|
356
|
+
def __init__(self, client, model=None, attachments=None):
|
|
338
357
|
self.client = client
|
|
339
358
|
self.current_model = model
|
|
340
359
|
self.models_payload = {"models": [], "default": None}
|
|
341
360
|
self.transcript = []
|
|
361
|
+
self.pending = list(attachments or []) # fichiers joints à la prochaine question
|
|
342
362
|
self.last_user = None
|
|
343
363
|
self.last_answer = None
|
|
344
364
|
self.connected = False
|
|
345
365
|
self.models_loaded = []
|
|
346
366
|
self.problem = None # (message, indication) si le serveur pose problème
|
|
367
|
+
self.prompt_session = None
|
|
347
368
|
|
|
348
369
|
# -- démarrage -----------------------------------------------------------
|
|
349
370
|
|
|
@@ -377,7 +398,7 @@ class ChatSession:
|
|
|
377
398
|
if self.problem:
|
|
378
399
|
print_error(*self.problem)
|
|
379
400
|
self.print_header()
|
|
380
|
-
print_hint('Tapez votre message.
|
|
401
|
+
print_hint('Tapez votre message. Tab complète les commandes ; """ ouvre une saisie multiligne.')
|
|
381
402
|
print()
|
|
382
403
|
|
|
383
404
|
def print_header(self):
|
|
@@ -390,9 +411,7 @@ class ChatSession:
|
|
|
390
411
|
print(f" {paint(sym('err'), 'error')} {muted('non connecté à')} {c.base_url}")
|
|
391
412
|
print(f" {muted('accès'.ljust(8))}{c.auth_label}")
|
|
392
413
|
if c.tracks_quota:
|
|
393
|
-
|
|
394
|
-
text = quota_text(state) if state["known"] else "non communiqué"
|
|
395
|
-
print(f" {muted('quota'.ljust(8))}{paint(text, 'error' if quota_exhausted(state) else None)}")
|
|
414
|
+
print(f" {muted('quota'.ljust(8))}{quota_line(c)}")
|
|
396
415
|
if self.models_loaded:
|
|
397
416
|
names = " · ".join(self.models_loaded)
|
|
398
417
|
print(f" {muted('modèles'.ljust(8))}{muted(_truncate(names, term_width() - 14))}")
|
|
@@ -404,25 +423,36 @@ class ChatSession:
|
|
|
404
423
|
label = self.current_model or "opsiom"
|
|
405
424
|
if len(label) > 24:
|
|
406
425
|
label = label[:23] + "…"
|
|
407
|
-
return
|
|
426
|
+
return f"{muted(label)} {accent(sym('prompt'))} "
|
|
427
|
+
|
|
428
|
+
def _ask(self, text):
|
|
429
|
+
if self.prompt_session is not None:
|
|
430
|
+
return self.prompt_session.prompt(formatted(text)).strip()
|
|
431
|
+
return input(rl_safe(text)).strip()
|
|
432
|
+
|
|
433
|
+
def _model_choices(self):
|
|
434
|
+
return [(m["id"], m.get("label", m["id"])) for m in self.models_payload.get("models", [])]
|
|
408
435
|
|
|
409
436
|
# -- boucle --------------------------------------------------------------
|
|
410
437
|
|
|
411
438
|
def read_input(self):
|
|
412
|
-
text =
|
|
439
|
+
text = self._ask(self.prompt())
|
|
413
440
|
if text != '"""':
|
|
414
441
|
return text
|
|
415
442
|
print_hint('saisie multiligne : terminez par """ seul sur une ligne')
|
|
416
443
|
rows = []
|
|
417
444
|
while True:
|
|
418
|
-
row =
|
|
445
|
+
row = self._ask(muted("… "))
|
|
419
446
|
if row.strip() == '"""':
|
|
420
447
|
break
|
|
421
448
|
rows.append(row)
|
|
422
449
|
return "\n".join(rows).strip()
|
|
423
450
|
|
|
424
451
|
def run(self):
|
|
425
|
-
|
|
452
|
+
self.prompt_session = make_prompt_session(
|
|
453
|
+
CommandCompleter(all_commands(), self._model_choices), storage.HISTORY_PATH)
|
|
454
|
+
if self.prompt_session is None:
|
|
455
|
+
setup_history()
|
|
426
456
|
try:
|
|
427
457
|
while True:
|
|
428
458
|
try:
|
|
@@ -441,7 +471,8 @@ class ChatSession:
|
|
|
441
471
|
else:
|
|
442
472
|
self.send(line)
|
|
443
473
|
finally:
|
|
444
|
-
|
|
474
|
+
if self.prompt_session is None:
|
|
475
|
+
save_history()
|
|
445
476
|
return 0
|
|
446
477
|
|
|
447
478
|
def dispatch(self, line):
|
|
@@ -466,9 +497,9 @@ class ChatSession:
|
|
|
466
497
|
print_info(bold(title))
|
|
467
498
|
for name, args, desc in items:
|
|
468
499
|
usage = f"{name} {args}".rstrip()
|
|
469
|
-
print(f" {accent(usage.ljust(
|
|
500
|
+
print(f" {accent(usage.ljust(26))} {muted(desc)}")
|
|
470
501
|
print()
|
|
471
|
-
print_hint("Entrée envoie · \"\"\" saisie multiligne · Ctrl+C annule
|
|
502
|
+
print_hint("Entrée envoie · \"\"\" saisie multiligne · Tab complète · Ctrl+C annule · Ctrl+D quitte")
|
|
472
503
|
|
|
473
504
|
def cmd_clear(self, _arg):
|
|
474
505
|
sys.stdout.write("\033[H\033[2J")
|
|
@@ -482,23 +513,48 @@ class ChatSession:
|
|
|
482
513
|
if not self.client.tracks_quota:
|
|
483
514
|
print_hint("pas de quota : aucune clé API configurée.")
|
|
484
515
|
return
|
|
485
|
-
|
|
486
|
-
if not state["known"]:
|
|
487
|
-
print_hint("quota non communiqué par le serveur pour l’instant.")
|
|
488
|
-
elif state["unlimited"]:
|
|
489
|
-
print_hint("quota illimité sur ce compte.")
|
|
490
|
-
else:
|
|
491
|
-
print_hint(f"{state['used']}/{state['limit']} tokens utilisés aujourd’hui — "
|
|
492
|
-
f"{state['remaining']} restants.")
|
|
516
|
+
print(f" {quota_line(self.client)}")
|
|
493
517
|
|
|
494
518
|
def cmd_new(self, _arg):
|
|
495
519
|
self.transcript = []
|
|
520
|
+
self.pending = []
|
|
496
521
|
self.last_user = self.last_answer = None
|
|
497
522
|
if self.client.new_conversation():
|
|
498
523
|
print_ok("nouvelle conversation (historique du compte réinitialisé côté serveur).")
|
|
499
524
|
else:
|
|
500
525
|
print_ok("nouvelle conversation. Ce serveur ne garde pas d’historique.")
|
|
501
526
|
|
|
527
|
+
def cmd_fichier(self, arg):
|
|
528
|
+
if not arg:
|
|
529
|
+
if not self.pending:
|
|
530
|
+
print_hint("aucun fichier joint. Usage : /fichier <chemin>")
|
|
531
|
+
return
|
|
532
|
+
for item in self.pending:
|
|
533
|
+
print_info(f"{item.name} ≈ {item.tokens} tokens")
|
|
534
|
+
print_hint("envoyé avec votre prochaine question · /fichier effacer pour retirer")
|
|
535
|
+
return
|
|
536
|
+
if arg.lower() in ("effacer", "clear"):
|
|
537
|
+
self.pending = []
|
|
538
|
+
print_ok("pièces jointes retirées.")
|
|
539
|
+
return
|
|
540
|
+
try:
|
|
541
|
+
item = load_text_file(arg)
|
|
542
|
+
except FileError as exc:
|
|
543
|
+
print_error(str(exc))
|
|
544
|
+
return
|
|
545
|
+
self.pending.append(item)
|
|
546
|
+
print_ok(f"joint : {item.name} (≈ {item.tokens} tokens). Il sera envoyé avec votre prochaine question.")
|
|
547
|
+
|
|
548
|
+
def cmd_copy(self, _arg):
|
|
549
|
+
if not self.last_answer:
|
|
550
|
+
print_hint("aucune réponse à copier.")
|
|
551
|
+
return
|
|
552
|
+
if copy_to_clipboard(self.last_answer):
|
|
553
|
+
print_ok("réponse copiée dans le presse-papiers.")
|
|
554
|
+
else:
|
|
555
|
+
print_warn("aucun outil de presse-papiers détecté.",
|
|
556
|
+
"installez xclip, wl-copy ou xsel (Linux), ou utilisez /save.")
|
|
557
|
+
|
|
502
558
|
def cmd_save(self, arg):
|
|
503
559
|
if not self.transcript:
|
|
504
560
|
print_hint("rien à enregistrer pour l’instant.")
|
|
@@ -542,16 +598,6 @@ class ChatSession:
|
|
|
542
598
|
return
|
|
543
599
|
self.send(self.last_user)
|
|
544
600
|
|
|
545
|
-
def cmd_copy(self, _arg):
|
|
546
|
-
if not self.last_answer:
|
|
547
|
-
print_hint("aucune réponse à copier.")
|
|
548
|
-
return
|
|
549
|
-
if copy_to_clipboard(self.last_answer):
|
|
550
|
-
print_ok("réponse copiée dans le presse-papiers.")
|
|
551
|
-
else:
|
|
552
|
-
print_warn("aucun outil de presse-papiers détecté.",
|
|
553
|
-
"installez xclip, wl-copy ou xsel (Linux), ou utilisez /save.")
|
|
554
|
-
|
|
555
601
|
def cmd_doctor(self, _arg):
|
|
556
602
|
run_doctor(self.client)
|
|
557
603
|
|
|
@@ -567,15 +613,19 @@ class ChatSession:
|
|
|
567
613
|
"réessayez demain, ou changez de clé API avec : opsiom configure")
|
|
568
614
|
return
|
|
569
615
|
|
|
570
|
-
|
|
616
|
+
names = [item.name for item in self.pending]
|
|
617
|
+
payload = build_message(message, self.pending)
|
|
618
|
+
shown = message + (f" [fichier : {', '.join(names)}]" if names else "")
|
|
619
|
+
self.transcript.append({"role": "user", "text": shown})
|
|
571
620
|
self.last_user = message
|
|
621
|
+
|
|
572
622
|
spinner = Spinner("Réflexion", hint="Ctrl+C pour annuler")
|
|
573
623
|
printer = None
|
|
574
624
|
final = {}
|
|
575
625
|
started = time.perf_counter()
|
|
576
626
|
spinner.start()
|
|
577
627
|
try:
|
|
578
|
-
with closing(self.client.chat_stream(
|
|
628
|
+
with closing(self.client.chat_stream(payload, model=self.current_model)) as stream:
|
|
579
629
|
for piece in stream:
|
|
580
630
|
if isinstance(piece, dict):
|
|
581
631
|
final = piece
|
|
@@ -626,6 +676,7 @@ class ChatSession:
|
|
|
626
676
|
answer = printer.finish()
|
|
627
677
|
elapsed_ms = int((time.perf_counter() - started) * 1000)
|
|
628
678
|
self.last_answer = answer
|
|
679
|
+
self.pending = [] # les fichiers joints ont été envoyés
|
|
629
680
|
self.transcript.append({"role": "assistant", "text": answer,
|
|
630
681
|
"model": final.get("model") or self.current_model or "?",
|
|
631
682
|
"elapsed_ms": elapsed_ms})
|