Skip to article frontmatterSkip to article content

Licence CC BY-NC-ND, Thierry Parmentelat

introduction

dans ce TP nous allons

contexte

on met à votre disposition deux serveurs ollama:

objectif

ce qu’on veut faire, c’est fabriquer une UI sommaire qui permet

ça pourrait ressembler à ceci:

v01: starter code

chatbot-01.py
"""
starter code for a chatbot - made with flet 0.25.2 (https://flet.dev)

starter code has the dialogs for choosing the model, the server, and the
streaming option, plus a button to send the request
none of this is actually connected to anything yet
"""


import flet as ft

SERVERS = {
    # this one is fast because it has GPUs,
    # but it requires a login / password
    'GPU': {
        "name": "GPU fast",
        "url": "https://ollama-sam.inria.fr",
        "username": "Bob",
        "password": "hiccup",
    },
    # this one is slow because it has no GPUs,
    # but it does not require a login / password
    'CPU': {
        "name": "CPU slow",
        "url": "http://ollama.pl.sophia.inria.fr:8080",
    },
}


# a hardwired list of models
MODELS = [
    "gemma2:2b",
    "mistral:7b",
    "deepseek-r1:7b",
]


TITLE = "My first Chatbot 01"


def main(page: ft.Page):
    # set the overall window title
    page.title = TITLE

    ### the visual pieces
    # a checkbox to select "streaming" mode or not - default is false
    streaming = ft.Checkbox(label="streaming", value=False)

    # choose the model
    model = ft.Dropdown(
        options=[ft.dropdown.Option(model) for model in MODELS],
        value=MODELS[0],
        width=300,
    )
    # choose the server
    server = ft.Dropdown(
        options=[ft.dropdown.Option(server) for server in ("CPU", "GPU")],
        value="CPU",
        width=100,
    )

    # the submit button

    # what do we want to happen when we click the button ?
    def send_request(_event):
        """
        the callback that fires when clicking the 'submit' button
        """
        # NOTE that we can use the variables that are local to 'main'
        # i.e. model, server, streaming...
        # for now, just show current settings
        print("Your current settings :")
        print(f"{streaming.value=}")
        print(f"{model.value=}")
        print(f"{server.value=}")

    # send_request is the callback function defined above
    # it MUST accept one parameter which is the event that triggered the callback
    submit = ft.ElevatedButton("Send", on_click=send_request)


    # arrange these pieces in a single row
    page.add(
        ft.Row(
            [streaming, model, server, submit],
            # for a row: main axis is horizontal
            # and cross axis is vertical
            alignment=ft.MainAxisAlignment.CENTER,
        )
    )

ft.app(main)
flet run chatbot.py

vous devez voir une UI un peu tristoune, avec seulement

à ce stade, cette UI est totalement inerte, on va la construire pas à pas

ce qu’on découvre dans la v01

dans ce code, on utilise le fait que

ce qu’on voit également dans ce code:

vous aurez envie de bookmark ces entrées dans la doc, pour plus d’info:

pour les forts

si cet énoncé vous inspire, vous pouvez simplement suivre votre voie pour développer l’application
sinon pour les autres, voici un chemin possible pour y arriver; évidemment je vous donne ces étapes entièrement à titre indicatif
bref, dans tous les cas, n’hésitez pas à faire comme vous le sentez...

v02: ajoutons un titre

pour vous familiariser avec le modèle de lignes et colonnes de flet, ajoutez un titre principal, comme sur l’illustration

v03: avec un peu de classe: ChatbotApp

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

Je vous propose de procéder en deux temps

v04: une classe History

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:

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

ici encore je vous conseille de procéder par petites étapes:

v05: un peu de réseau

c’est seulement maintenant que l’on va effectivement interagir via le réseau avec les serveurs ollama
je vous propose pour commencer de simplement:

quelques indices:

à nouveau on pourra procéder par étapes:

v06: on affiche la réponse

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

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 que l’on n’a pas encore prévu

du coup pour aboutir à une version à peu près fonctionnelle il devrait vous suffire de

v07: un peu de cosmétique

ici on va simplement ajouter un peu de relief pour qu’on s’y retrouve entre les questions et les réponses

ici aussi on peut imaginer procéder en deux étapes

v08: supporter le mode streaming

une requête HTTP “classique” est d’une grande simplicité: on envoie une requête, on reçoit une réponse
dans notre cas toutefois, ce modèle n’est pas tout à fait adapté, car l’IA met du temps à élaborer sa réponse, et on aimerait mieux voir la réponse au fur et à mesure, plutôt que de devoir attendre la fin, qui est le comportement que vous obeservez si vous avez suivi mes indications jusqu’ici
c’est ce à quoi on va s’attacher maintenant
il se trouve que le serveur ollama retourne ce qu’on appelle une réponse HTTP qui est un stream

du coup on peut facilement modifier notre code pour en tirer parti en écrivant quelque chose comme:

    with requests.post(url, json=data, stream=True) as answer:
        print("HTTP status code:", answer.status_code)
        for line in answer.iter_lines():
            # do something with the line...

v09 (optionnel): acquérir la liste des modèles

plutôt que de proposer une liste de modèles “en dur” comme dans le starter code, on pourrait à ce stade acquérir, auprès du serveur choisi, la liste des modèles connus; pour cela ollama met à notre disposition l’API /api/tags

dans mon implémentation j’ai choisi de “cacher” ce résultat, pour ne pas redemander plusieurs fois cette liste à un même serveur (cette liste bouge très très peu...); mais c’est optionnel; par contre ce serait sympa pour les utilisateurs de conserver, lorsque c’est possible, le modèle choisi lorsqu’on change de serveur...

v10 (optionnel): une classe Server

dans cette version, je vous propose de créer une classe Server abstraite, qui définit une API commune pour interagir avec un serveur d’IA; puis une classe concrète OllamaServer qui hérite de Server et qui encapsule la logique d’interaction avec l’API ollama - puisque pour l’instant nos deux serveurs offrent la même API

mais de cette façon dans le futur (étape suivante) on pourra plus simplement ajouter le code pour interagir avec d’autres types de serveurs, qui implémentent une API différente (par exemple litellm que nous avons aussi déployé à l’Inria)

c’est pourquoi dans cette v10, je vous propose de rester à fonctionnalités constantes, mais de créer une classe OllamaServer qui hérite de Server et qui implémente les méthodes suivantes:

class Server:
    """
    an abstract server class
    """
    def list_models(self) -> list[str]:
        pass
    def generate_blocking(self, prompt, model, streaming ) -> list[str]:
        """
        non-streaming generation - returns a list of text chunks
        """
        pass
    def generate_streaming(self, prompt, model, streaming) -> Iterator[str]:
        """
        streaming generation - yields text chunks
        """
        pass

v11 (optionnel): une autre API

à présent que nous avons une classe Server, on va pouvoir implémenter une autre classe qui hérite de Server, par exemple LitellmServer, qui implémente la même API mais en s’appuyant sur le serveur litellm-sam.inria.fr

l’API en question est documentée ici https://litellm-sam.inria.fr/#/model management/ (pour l’instant nécessite un VPN) - et sans doute à plein d’autres endroits publics

pour vous authentifier vous aurez également besoin d’une clé API - communiquée par un autre moyen

le but du jeu consiste donc ici à

plein d’améliorations possibles

en vrac:

pour aller plus loin