Skip to content

Commit 6c0c1d6

Browse files
committed
reorganize the various steps in chatbot
reorder and split some steps in substeps
1 parent b5f019f commit 6c0c1d6

20 files changed

+1254
-717
lines changed

notebooks/tps/chatbot/README-chatbot-nb.md

Lines changed: 152 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -162,20 +162,23 @@ pour vous familiariser avec le modèle de lignes et colonnes de `flet`, **ajoute
162162

163163
+++ {"slideshow": {"slide_type": ""}, "tags": []}
164164

165-
## v03: avec un peu de classe
165+
## v03: avec un peu de classe: `ChatbotApp`
166166

167167
ceci est une étape **totalement optionnelle**, mais je vous recommande de **créer une classe**, qui pourrait s'appeler **`ChatbotApp`**, pour regrouper la logique de notre application, et éviter de mettre tout notre code en vrac dans le `main`
168168

169169
- on pourrait envisager par exemple que `ChatbotApp` hérite de `ft.Column`
170170
- de cette façon on se retrouverait avec un `main` qui ne fait plus que
171-
```{literalinclude} chatbot-10.py
172-
:start-after: def main
171+
```{literalinclude} chatbot-03a.py
172+
:start-after: show the code in the instructions
173173
```
174-
````{admonition} page.update()
175-
:class: tip
176-
on choisit de passer `page` au constructeur de l'objet, car avec *flet* il faut penser à *flush* les changements avec un `page.update()` - sinon les changements que l'on fait en mémoire ne sont pas répercutés dans l'affichage
177-
et donc il faut qu'on puisse accéder à cette `page` depuis la classe `ChatbotApp` !
178-
````
174+
175+
Je vous propose de procéder en deux temps
176+
177+
- étape 3a: on crée la classe `ChatbotApp`;
178+
le code de `main` se retrouve essentiellement dans le constructeur de `ChatbotApp`
179+
(et souvenez-vous comment on utilise `super()` pour initialiser la superclasse, ici `Column`
180+
- étape 3b: la fonction `send_request` devient une méthode de la classe
181+
(au lieu d'être une fontion incluse dans le constructeur)
179182

180183
+++
181184

@@ -186,75 +189,125 @@ ceci est une étape **totalement optionnelle**, mais je vous recommande de **cr
186189
:align: right
187190
```
188191

189-
toujours pour éviter de finir avec un gros paquet de spaguettis, on pourrait imaginer à ce stade **écrire une classe `History`** (tout ceci est totalement indicatif...) qui:
192+
toujours pour éviter de finir avec un gros paquet de spaguettis, on va imaginer à ce stade d'**écrire une classe `History`** ( nouveau <!-- -->tout ceci est totalement indicatif...) qui:
190193

191194
- hérite, là encore de `ft.Column`
192-
- et est responsable de la partie "dialogue" entre humain et robot
193-
- et qui du coup crée la zone de prompt,
194-
- et possède une méthode `current_prompt()` qui renvoie le prompt tapé par l'utilisateur
195-
- et une méthode `add_message()` pour insérer les questions et les réponses au fur et à mesure
196-
(à ce stade on ne fait pas encore la différence entre prompt et réponse)
195+
- c'est elle qui est responsable de **créer la zone de prompt**
196+
- et d'afficher au fur et à mesure, et au bon endroit, **les échanges avec le robot**
197+
- de cette façon on pourra l'insérer simplement en bas dans l'objet `ChatbotApp`
198+
ainsi cet objet - qui rappelons-le est une `Column` - va voir maintenant 3 fils:
197199

198-
````{admonition} alternance de questions / réponses
200+
- le titre
201+
- la `Row` avec les différents réglages
202+
- et une instance de `History()`
203+
204+
`````{admonition} la logique de la classe History
199205
:class: tip
206+
pour fixer les idées, disons qu'à ce stade cette classe possède les méthodes
200207
201-
comme la logique du dialogue c'est d'alterner les questions et les réponses, on peut tout à fait considérer que c'est un fait acquis, et du coup admettre que:
202-
- le dernier élément dans la colonne History est toujours le dernier prompt;
203-
- et les autres éléments sont alternativement, en commençant du début: prompt, réponse, prompt, réponse, etc...
204-
````
208+
- `current_prompt()` qui renvoie le prompt tapé par l'utilisateur
209+
- `add_message(some_text)` pour insérer les questions et les réponses au fur et à mesure
210+
211+
l'idée est que l'objet `History` possède:
212+
213+
- en dernier (tout en bas donc) un objet de type `ft.TextField` (qui est éditable); dans lequel on va taper notre prompt
214+
- et au dessus on va conserver la trace des échanges: question1, réponse1, etc...
215+
et pour cela on utilisera `add_message(some_text)`, dont le job donc est d'insérer un objet `ft.Text`
216+
(non modifiable par l'utilisateur cette fois)
217+
**en avant-dernière position** - c'est-à-dire juste au dessus du prompt
218+
`````
219+
220+
pour être bien clair, à ce stade on ne fait pas encore usage du réseau pour quoi que ce soit, on veut juste mettre en place la structure de l'UI
205221

206-
pour être bien clair, à ce stade on ne fait pas encore usage du réseau pour quoi que ce soit
222+
ici encore je vous conseille de procéder par petites étapes:
223+
224+
- 4a: la trame de la classe `History`
225+
- 4b: faites en sorte que le fait de taper "Entrée" dans la zone de prompt fasse le même effet que le bouton "Send"
207226

208227
+++
209228

210229
## v05: un peu de réseau
211230

212231
c'est seulement maintenant que l'on va effectivement **interagir via le réseau avec les serveurs** ollama
213-
je vous propose pour commencer de simplement fabriquer la requête, et pour commencer de simplement afficher la réponse sur le terminal
232+
je vous propose pour commencer de simplement:
233+
234+
- fabriquer la requête,
235+
- et simplement afficher la réponse **dans le terminal**
214236

215237
quelques indices:
216238

217239
- la librairie qu'on va utiliser pour cela s'appelle `requests`;
218240
- vous pouvez commencer par regarder ceci pour quelques exemples <https://requests.readthedocs.io/en/latest/user/quickstart/>
219-
- je vous recommande de vous concentrer pour l'instant sur le serveur CPU, ce qui vous évite pour l'instant de vous embêter avec les authentifications
220241
- notre objectif ici et de bien comprendre la structure de la réponse
221242
posez-vous notamment la question de savoir quand est-ce que c'est terminé, et regardez bien la fin de la réponse
222243
- pour l'instant aussi, on ignore le flag *streaming*: on poste une requête et on attend le retour
223244

245+
à nouveau on pourra procéder par étapes:
246+
247+
- 5a: en commençant par le serveur CPU uniquement
248+
- 5b: ajouter l'authentification lorsque c'est nécessaire, de façon à pouvoir utiliser indifféremment les deux serveurs
249+
224250
+++
225251

226252
````{admonition} un petit exemple
227253
:class: dropdown tip
228254
229-
voici comment on pourrait dire bonjour au modèle `gemma2:2b`
255+
voici comment on pourrait dire `hey` au modèle `gemma2:2b`
256+
ce code peut s'exécuter par exemple directement dans ipython
230257
231258
```python
232259
import requests
233260
import json
234261
235262
url = "http://ollama.pl.sophia.inria.fr:8080/api/generate"
236263
237-
# envoyer une requête POST avec comme paramètre un dictionnaire
238-
# encodé en JSON
264+
# c'est expliqué dans la doc ollama: l'API /api/generate
265+
# s'attend à ce qu'on lui passe ces deux paramètres:
266+
payload = {'model': 'gemma2:2b', 'prompt': 'hey'}
267+
268+
# pour envoyer une requête POST
269+
# avec comme paramètre ce payload encodé en JSON:
270+
239271
# cette ligne peut prendre un moment à s'exécuter...
240-
response = requests.post(url, json={'model': 'gemma2:2b', 'prompt': 'hey'})
272+
response = requests.post(url, json=payload)
241273
242274
# pour voir le status HTTP (devrait être 200)
243275
response.status_code
244276
245277
# pour accéder au corps de la réponse (sans les headers HTTP)
246278
body = response.text
247279
248-
# comme c'est aussi du JSON on doit le décoder
249-
# mais attention, regardez bien le contenu
250-
# il y a plusieurs lignes et chacune est un JSON
280+
# et regardez bien à quoi ça ressemble
281+
print(body)
282+
```
283+
````
284+
285+
+++
286+
287+
````{admonition} avec authentification
288+
:class: dropdown tip
289+
290+
dans le cas du serveur GPU qui attend une authentification:
291+
vous pouvez simplement aménager le code ci-dessus en remplaçant cette ligne
292+
293+
```python
294+
response = requests.post(url, json=payload)
295+
```
251296
252-
lines = body.split("\n")
297+
par celles-ci
298+
```python
299+
login_password = ('the-login', 'the-password')
300+
response = requests.post(url, json=payload, auth=login_password)
301+
```
253302
254-
for line in lines:
255-
# le dernier élément de lines peut être une ligne vide
256-
if line:
257-
print(f"reçu la ligne: {json.loads(line)}")
303+
si bien que vous pouvez envisager un code un peu unifié en faisant quelque chose dans le genre de
304+
```python
305+
auth_args = {}
306+
if need_authentication:
307+
auth_args['auth'] = ('the-login', 'the-password')
308+
# voir le cours: on ajoute les éléments du dictionnaire
309+
# sous la forme d'arguments nommés dans l'appel de la fonction
310+
response = requests.post(url, json=payload, **auth_args)
258311
```
259312
````
260313

@@ -269,16 +322,61 @@ for line in lines:
269322

270323
dans cette version, on utilise la réponse du serveur pour *afficher le dialogue **dans notre application*** et non plus dans le terminal
271324

272-
pour cela on va devoir faire quelques modifications à la classe `History`; en effet vous devez avoir observé à ce stade que la réponse vient "en petits morceaux", ce qui fait qu'on pourrait avoir envie de modifier un peu la classe `History` de sorte qu'elle expose à présent les méthodes
325+
pour cela on va devoir faire quelques modifications à la classe `History`;
326+
en effet vous devez avoir observé à ce stade que la réponse vient "en petits morceaux", ce que l'on n'a pas encore prévu
327+
328+
du coup pour aboutir à une version à peu près fonctionnelle il devrait vous suffire de
273329

274-
- `add_prompt()` et `add_answer()` pour distinguer entre les deux types d'entrée
275-
- et surtout `add_chunk()` qui permet d'ajouter *juste un mot* dans la réponse du robot, pour nous ajuster avec le format de la réponse
330+
- ajouter à la classe `History` une méthode `add_chunk(token)`, qui permet d'ajouter *juste un mot* dans la réponse du robot
331+
- et au lieu d'afficher la réponse du robot en bloc, de la traiter proprement pour en extraire les différents petits morceaux, puis les afficher dans l'interface grâce donc à `add_chunk()`
276332

277-
````{admonition} le scrolling
278-
:class: tip dropdown
333+
````{admonition} update()
334+
:class: tip
335+
336+
avec *flet* il faut penser à *flush* les changements avec un `flet_object.update()`
337+
car sinon les changements que l'on fait en mémoire ne sont pas répercutés dans l'affichage
338+
(si vous avez le TP sur le snake, c'est la même logique ici avec `flet` que ça l'était avec `pygame`)
339+
**il faut rafraichir explicitement** la page pour que vos modifications se voient à l'écran
340+
341+
pour faire ça `flet` fournit sur tous ses objets une méthode `update()`
342+
et comme notre `History` hérite de `ft.Column`, vous pouvez simplement lui envoyer la méthode `update()`
343+
````
344+
345+
+++
346+
347+
## v07: un peu de cosmétique
348+
349+
```{image} media/chatbot-07.png
350+
:width: 400px
351+
:align: right
352+
```
353+
354+
ici on va simplement ajouter un peu de relief pour qu'on s'y retrouve entre les questions et les réponses
355+
356+
ici aussi on peut imaginer procéder en deux étapes
357+
358+
- 7a: juste la cosmétique: montrer de manière plus distinte les 3 groupes (questions, réponses, et prompt)
359+
- 7b: faire en sorte que la fenêtre *scroll* automatiquement vers le bas, lorsque le dialogue remplit toute la page
360+
- 7c: faire en sorte qu'on ne puisse pas envoyer plusieurs requêtes en parallèle
361+
362+
+++
363+
364+
````{admonition} pour 7a: de la couleur
365+
:class: dropdown tip
366+
367+
- mettre un fond de couleur à notre `TextField` (le prompt)
368+
- enrichir un peu l'interface de `History`, et remplacer l'unique méthode `add_message()`
369+
par deux méthodes différentes `add_prompt(text)` et `add_answer(text)`
370+
````
371+
372+
+++
373+
374+
````{admonition} pour 7b: le scrolling
375+
:class: dropdown tip
376+
377+
j'ai eu un peu du mal avec cette partie; (revenez dessus plus tard si nécessaire), mais il est important que notre chatbot *scroll* correctement:
378+
c'est-à-dire qu'après plusieurs questions/réponses on voie toujours **le bas du dialogue**
279379
280-
peut-être un peu prématuré (revenez dessus plus tard si nécessaire), mais il est important que notre chatbot *scroll* correctement:
281-
c'est-à-dire qu'après plusisurs questions/réponses on voie toujours le bas du dialogue
282380
et pour ça sachez qu'il faut procéder comme ceci
283381
```python
284382
cl = ft.Column(
@@ -294,19 +392,25 @@ cl = ft.Column(
294392
295393
enfin, remarquez qu'on peut avoir envie d'activer le scrolling
296394
297-
- sur la `Column` principale (notre `ChatbotApp`), mais dans ce cas les widgets de mode (streaming, server...) vont scroller aussi
395+
- sur la `Column` principale (notre `ChatbotApp`), mais dans ce cas les widgets de mode (streaming, server...) vont scroller aussi...
298396
c'est mieux que pas de scroll, mais pas forcément idéal encore
299397
- sur la `History`, et dans ce cas les widgets de mode vont rester fixes;
300398
dans ce cas-là toutefois, pensez à mettre tout de même `expand=True` sur la `ChatbotApp` pour que les changements de la taille de l'app se propagent jusqu'à l'`History`
301-
302399
````
303400

304401
+++
305402

306-
## v07: pas de multiples requêtes
403+
````{admonition} pour 7c: éviter plusieurs Send en parallèle
404+
:class: dropdown tip
405+
406+
le sujet c'est que si le code ne fait rien de particulier, rien n'empêche l'utilisateur de cliquer 3 fois de suite sur le bouton Send, et que ça envoie 3 requêtes essentiellement *en même temps*
307407
308-
à ce stade il est utile d'ajouter un peu de logique pour éviter que l'on puisse poster deux requêtes "en même temps": on **rend l'UI inactive** jusqu'à réception de la réponse
309-
pour cela voyez dans `flet` l'attribut `disabled`
408+
pour éviter ça, vous faites en sorte de *disable* les deux moyens d'envoyer la requête (le bouton *Send* et la touche *Entrée* dans le prompt)
409+
410+
je vous recommande du coup d'ajouter les méthodes `enable_prompt()` et `disable_prompt()` dans la classe `History`
411+
412+
et ensuite d'implémenter une logique dans la méthode `send_request()` pour désactiver / réactiver l'interface au bon moment; c'est peut-être d'ailleurs le moment de couper cette méthode en plus petits morceaux..
413+
````
310414

311415
+++
312416

@@ -338,16 +442,9 @@ dans mon code j'ai conservé les deux modes (streaming et non-streaming) pour po
338442

339443
+++
340444

341-
## v09: authentification
342-
343-
à ce stade il est temps d'ajouter du code pour pouvoir s'**authentifier avec le login/password** auprès du serveur qui en a besoin
344-
c'est juste une question d'ajouter, dans l'appel à `requests.post`, un paramètre `auth=(user, password)`
345-
346-
+++
347-
348-
## v10 (optionnel): acquérir la liste des modèles
445+
## v09 (optionnel): acquérir la liste des modèles
349446

350-
```{image} media/chatbot-10.png
447+
```{image} media/chatbot-09.png
351448
:width: 400px
352449
:align: right
353450
```

0 commit comments

Comments
 (0)