Aller au contenu
English
Compte : non connecté, synchronisation possible Mon apprentissage
Selectionner le thème
English

Ollama avancé : SDK Python, Structured Output et embeddings

Par mis à jour le

Améliorer cette page

Logo Ollama

Vous savez déjà utiliser Ollama en ligne de commande (installation et premiers pas). Mais saviez-vous qu'Ollama dispose d'un SDK Python natif, qui donne accès directement à ses options propres ? Ce guide vous montre comment vérifier l'API REST locale, utiliser le SDK natif et le client OpenAI pointé sur Ollama, puis comment exploiter les Structured Outputs (sorties JSON typées), les embeddings pour la recherche sémantique, et comment construire un assistant DevOps complet en local.

  • API REST, SDK natif, LiteLLM et client OpenAI : quand utiliser chaque approche
  • Streaming : afficher les réponses en temps réel
  • Structured Output : obtenir du JSON typé avec Pydantic
  • Embeddings : générer des vecteurs pour la recherche sémantique
  • Conversation multi-turn : maintenir le contexte entre les échanges
  • TP fil rouge : construire DevOps Buddy, un assistant DevOps local

Avant de commencer, assurez-vous d'avoir :

  1. Ollama installé (guide d'installation)
  2. Deux modèles téléchargés : llama3.2:3b pour la conversation, et nomic-embed-text, un modèle dédié aux embeddings, indispensable au script 06 :
Fenêtre de terminal
ollama pull llama3.2:3b
ollama pull nomic-embed-text
  1. Un environnement Python dédié, avec le SDK dans des versions figées pour que les exemples se comportent à l'identique chez vous. Le répertoire ~/lab-ollama accueillera tous les scripts de ce guide :
Fenêtre de terminal
mkdir -p ~/lab-ollama && cd ~/lab-ollama
python3 -m venv .venv && source .venv/bin/activate
pip install ollama==0.6.2 pydantic==2.12.3

Vérifiez les versions :

Fenêtre de terminal
ollama --version # 0.40.x (version rejouée : 0.40.4)
pip show ollama # 0.6.x ou supérieur

Quand Ollama tourne, il expose une API REST sur le port 11434 de votre machine. N'importe quel programme peut alors dialoguer avec le modèle : le SDK Python, LiteLLM et le client OpenAI vus plus bas ne sont que des surcouches de cette API. Avant de tester un script, vérifiez donc que le serveur répond :

Fenêtre de terminal
curl http://localhost:11434/api/tags

La sortie est un objet JSON dont la clé models liste les modèles téléchargés, avec leur nom et leur taille. Si la commande échoue avec Connection refused, le serveur n'est pas démarré : voir Ollama : dépannage et configuration.

Les deux bibliothèques parlent au même serveur Ollama, la question n'est donc pas laquelle « marche » mais laquelle vous enferme. Le SDK natif expose l'API d'Ollama telle quelle, y compris les paramètres qui n'ont pas d'équivalent chez les autres fournisseurs. LiteLLM normalise les appels sur le format OpenAI, ce qui rend le code portable au prix du plus petit dénominateur commun. Dans le tableau ci-dessous, les sorties structurées existent des deux côtés : le SDK natif les demande avec format=, LiteLLM avec response_format (un schéma JSON ou une classe Pydantic, d'après la documentation LiteLLM d'Ollama). Le critère décisif est donc la portabilité contre l'accès direct aux options propres à Ollama, comme think=True ou options.

AspectSDK ollamaLiteLLM
Installationpip install ollamapip install litellm
ScopeOllama uniquement100+ providers (OpenAI, Anthropic...)
Structured OutputOui, natif avec format=Oui, via response_format (schéma JSON)
EmbeddingsOui, embed() intégréOui, embedding()
Pensée (Thinking)Oui, think=TrueNon documenté pour Ollama
Cas d'usageScripts dédiés OllamaCode portable multi-providers

Recommandation : Utilisez le SDK natif si vous ciblez uniquement Ollama et voulez exploiter ses fonctionnalités spécifiques. Utilisez LiteLLM si vous voulez pouvoir basculer vers d'autres providers. Un troisième cas existe : le code qui utilise déjà la bibliothèque openai, traité juste en dessous.

Ollama expose aussi un endpoint compatible OpenAI sous le chemin /v1/ (complétions de chat, embeddings, liste des modèles). Si votre code appelle déjà la bibliothèque openai, il n'y a pas à le réécrire : il suffit de changer l'URL de base pour viser votre serveur local. Le client exige une clé d'API, mais Ollama l'ignore : n'importe quelle chaîne convient, ici ollama.

Fenêtre de terminal
pip install openai==3.31.0
00_client_openai.py
from openai import OpenAI
client = OpenAI(base_url="http://localhost:11434/v1/", api_key="ollama")
response = client.chat.completions.create(
model="llama3.2:3b",
messages=[{"role": "user", "content": "Explique une API REST en 3 phrases simples"}],
)
print(response.choices[0].message.content)

Remarquez la différence de syntaxe : la réponse suit le format OpenAI (response.choices[0].message.content), alors que le SDK natif renvoie directement response.message.content, comme dans tous les scripts qui suivent. Les options propres à Ollama, comme think=True ou options, restent le terrain du SDK natif. Choisissez donc le client OpenAI pour brancher Ollama sur du code déjà écrit pour un service OpenAI, et le SDK natif pour tout nouveau script dédié à Ollama.

Tout au long de ce guide, nous construisons DevOps Buddy, un assistant DevOps local qui :

  • Répond aux questions DevOps en streaming
  • Utilise une base de connaissances avec embeddings (mini-RAG)
  • Retourne des commandes structurées avec Pydantic

Les scripts numérotés (01_... à 07_...) se placent tous dans le répertoire ~/lab-ollama créé dans les prérequis, avec l'environnement virtuel activé. Si vous avez fermé votre terminal entre-temps, réactivez-le avec cd ~/lab-ollama && source .venv/bin/activate.

La fonction chat() du SDK est l'équivalent de litellm.completion(), mais avec une API légèrement différente.

Ce premier script sert surtout à découvrir la forme de l'objet de réponse. Regardez la partie « Métadonnées » plutôt que le texte généré : prompt_eval_count et eval_count comptent les tokens en entrée et en sortie, total_duration s'exprime en nanosecondes, d'où la division par 1e9. Ces trois valeurs sont votre seul moyen de mesurer le coût réel d'un appel local, puisqu'aucune facturation ne vient vous le rappeler.

01_chat_basique.py
#!/usr/bin/env python3
"""
TP 01 : Appel basique avec le SDK Ollama natif
Objectif : Comprendre la différence avec litellm
"""
from ollama import chat
# Appel simple avec le SDK natif
response = chat(
model='llama3.2:3b',
messages=[
{'role': 'user', 'content': 'Explique Docker en une phrase pour un débutant.'}
]
)
print("=== Réponse du modèle ===")
print(response.message.content)
print("\n=== Métadonnées ===")
print(f"Modèle utilisé : {response.model}")
print(f"Tokens prompt : {response.prompt_eval_count}")
print(f"Tokens réponse : {response.eval_count}")
print(f"Durée totale : {response.total_duration / 1e9:.2f}s")

Exécutez le script :

Fenêtre de terminal
python 01_chat_basique.py

Sortie attendue :

=== Réponse du modèle ===
Docker est une technologie qui permet de créer des conteneurs virtuels, qui sont
des environnements isolés et sécurisés où les applications peuvent être exécutées
sans nécessiter d'installations supplémentaires sur l'hôte.
=== Métadonnées ===
Modèle utilisé : llama3.2:3b
Tokens prompt : 36
Tokens réponse : 54
Durée totale : 5.88s

Le streaming affiche la réponse mot par mot au fur et à mesure de la génération. C'est essentiel pour une bonne expérience utilisateur.

Avec stream=True, chat() ne renvoie plus un objet mais un générateur : la boucle for consomme les fragments au rythme de la génération. Deux conséquences pratiques. Les métadonnées de fin (eval_count, total_duration) n'arrivent que dans le dernier fragment, pas au début. Et si vous avez besoin du texte complet ensuite, il faut le concaténer vous-même pendant la boucle, comme le fait DevOps Buddy plus bas.

02_streaming.py
#!/usr/bin/env python3
"""
TP 02 : Streaming avec le SDK Ollama
Objectif : Afficher la réponse en temps réel, mot par mot
"""
from ollama import chat
# Streaming : la réponse arrive progressivement
print("=== Réponse en streaming ===")
stream = chat(
model='llama3.2:3b',
messages=[
{'role': 'user', 'content': 'Donne 5 bonnes pratiques Dockerfile, très bref.'}
],
stream=True # Active le streaming
)
# Afficher chaque chunk au fur et à mesure
for chunk in stream:
print(chunk.message.content, end='', flush=True)
print("\n\n=== Fin du streaming ===")

Le paramètre flush=True force l'affichage immédiat de chaque caractère.

C'est la fonctionnalité la plus puissante du SDK Ollama. Au lieu d'obtenir du texte libre, vous obtenez du JSON typé qui correspond exactement à un schéma Pydantic.

Sans schéma, un modèle à qui l'on demande « réponds en JSON » produit un JSON valide la plupart du temps, et du texte accompagné de trois backticks le reste du temps. Ce taux d'échec résiduel suffit à casser un script en production. Avec format=, Ollama contraint le décodage token par token : la sortie ne peut pas dévier du schéma. Les quatre bénéfices ci-dessous découlent tous de cette garantie côté serveur, pas d'une consigne dans le prompt.

  • Parsing fiable : plus besoin de regex ou de parsing manuel
  • Validation automatique : Pydantic vérifie que la réponse est conforme
  • Typage : votre IDE connaît les types des champs
  • Intégration : les données sont directement utilisables dans votre code

Deux appels font tout le travail, et il ne faut pas les confondre. DockerCommand.model_json_schema() convertit la classe Pydantic en schéma JSON transmis au serveur, qui s'en sert pour brider la génération. DockerCommand.model_validate_json() relit la réponse et instancie l'objet Python. Le schéma contraint la forme, pas le contenu : le champ level sera bien une chaîne, mais rien n'empêche le modèle d'y écrire intermédiaire au lieu des trois valeurs attendues. Pour verrouiller aussi les valeurs, déclarez un Literal["debutant", "intermediaire", "avance"] plutôt qu'un str.

03_structured_output.py
#!/usr/bin/env python3
"""
TP 03 : Structured Output avec Pydantic
Objectif : Obtenir des réponses JSON typées et validées automatiquement
Feature : Disponible depuis Ollama 0.5.0+
"""
from ollama import chat
from pydantic import BaseModel
# Définir le schéma de sortie avec Pydantic
class DockerCommand(BaseModel):
"""Une commande Docker expliquée"""
command: str # La commande complète
description: str # Explication en une phrase
example: str # Un exemple concret
level: str # "debutant", "intermediaire", "avance"
# Appeler le modèle avec le schéma
response = chat(
model='llama3.2:3b',
messages=[
{
'role': 'user',
'content': 'Explique la commande docker build'
}
],
format=DockerCommand.model_json_schema() # Forcer le format JSON
)
# Valider et parser la réponse avec Pydantic
result = DockerCommand.model_validate_json(response.message.content)
# Afficher le résultat structuré
print("=== Sortie structurée (Pydantic) ===")
print(f"Commande : {result.command}")
print(f"Description : {result.description}")
print(f"Exemple : {result.example}")
print(f"Niveau : {result.level}")
print("\n=== JSON brut ===")
print(response.message.content)

Sortie attendue :

=== Sortie structurée (Pydantic) ===
Commande : docker build
Description : Crée une image Docker à partir d'un fichier Dockerfile.
Exemple : docker build -t mon-image .
Niveau : debutant
=== JSON brut ===
{"command": "docker build", "description": "Crée une image Docker...", ...}

Le texte généré change à chaque exécution : seule la forme (quatre champs présents, validés par Pydantic) est garantie. Le champ level est un simple str, rien ne force le modèle à répondre « debutant » : lors du rejeu sur Ollama 0.40.4, il a renvoyé tour à tour Administrateur, avancé et Intermediate, et une fois un fragment sans sens. Pour contraindre les valeurs, utilisez une énumération (voir la leçon sur les sorties structurées citée plus bas).

Un schéma Pydantic peut contenir d'autres schémas : Tutorial référence une liste de Step, et Pydantic génère un schéma JSON imbriqué qu'Ollama sait respecter. Deux options méritent attention dans l'appel. temperature: 0 rend la sortie déterministe, ce qui compte quand on extrait des données plutôt qu'on ne rédige. num_predict: 500 plafonne le nombre de tokens générés : une structure imbriquée coupée en plein milieu produit un JSON invalide et fait échouer la validation, c'est la panne la plus fréquente sur ce type de schéma.

04_structured_avance.py
#!/usr/bin/env python3
"""
TP 04 : Structured Output avancé (listes et objets imbriqués)
Objectif : Extraire plusieurs éléments structurés en une seule requête
"""
from ollama import chat
from pydantic import BaseModel
class Step(BaseModel):
"""Une étape d'un processus"""
number: int
action: str
command: str
class Tutorial(BaseModel):
"""Un tutoriel complet"""
title: str
steps: list[Step]
total_time: str
print("Définition du schéma Pydantic...")
print("\nEnvoi de la requête à Ollama (llama3.2:3b)...")
# Demander un tutoriel structuré
response = chat(
model='llama3.2:3b',
messages=[
{
'role': 'user',
'content': 'Donne un tutoriel Docker en 2 étapes avec titre, steps (number, action, command) et total_time'
}
],
format=Tutorial.model_json_schema(),
options={'temperature': 0, 'num_predict': 500}
)
print("Réponse reçue!")
print(f" Durée: {response.total_duration / 1e9:.2f}s")
# Parser la réponse
tutorial = Tutorial.model_validate_json(response.message.content)
# Afficher le tutoriel formaté
print(f"\nTutoriel : {tutorial.title}")
print(f"Durée : {tutorial.total_time}")
for step in tutorial.steps:
print(f" {step.number}. {step.action} → {step.command}")

Sortie attendue :

Tutoriel : Tutoriel Docker en 2 étapes
Durée : > 30 minutes
1. Créer un nouveau projet Docker → docker run -it ubuntu /bin/bash
2. Créer une image personnalisée → docker build -t mon-image .

Comme pour le script 03, le contenu varie d'une exécution à l'autre : avec un modèle de 3 milliards de paramètres, le champ de durée peut revenir avec une valeur inattendue (} lors du rejeu sur Ollama 0.40.4). La structure, elle, reste valide.

Pour aller plus loin sur les schémas (JSON Schema écrit à la main, énumérations, limites de la garantie), la leçon Sorties structurées avec Ollama reprend le mécanisme de format= en détail, avec des modèles récents testés.

Pour maintenir le contexte entre plusieurs échanges, vous devez gérer l'historique des messages.

Le serveur Ollama ne mémorise rien entre deux appels : la mémoire de la conversation, c'est votre liste messages. À chaque tour, vous renvoyez tout l'historique, question et réponse comprises. Cela a un coût qui grandit linéairement, visible dans prompt_eval_count qui augmente à chaque échange jusqu'à saturer la fenêtre de contexte du modèle. Sur un assistant destiné à durer, il faut donc prévoir une troncature de l'historique ou un résumé des tours anciens ; le script ci-dessous, limité à quatre échanges, n'en a pas besoin.

05_conversation.py
#!/usr/bin/env python3
"""
TP 05 : Conversation multi-turn avec historique
Objectif : Maintenir le contexte entre plusieurs échanges
"""
from ollama import chat
# Historique de la conversation
messages = []
# Contexte système
system_message = {
'role': 'system',
'content': '''Tu es DevOps Buddy, un assistant spécialisé DevOps.
Tu réponds de manière concise et technique.
Tu donnes des exemples de commandes quand c'est pertinent.'''
}
messages.append(system_message)
print("DevOps Buddy initialisé")
print("=" * 50)
def ask(question: str) -> str:
"""Pose une question et met à jour l'historique"""
print(f"\nVous : {question}")
# Ajouter la question à l'historique
messages.append({'role': 'user', 'content': question})
# Appeler le modèle avec tout l'historique
response = chat(
model='llama3.2:3b',
messages=messages,
options={'temperature': 0.3}
)
# Ajouter la réponse à l'historique
assistant_message = response.message.content
messages.append({'role': 'assistant', 'content': assistant_message})
print(f"Buddy : {assistant_message}")
return assistant_message
# Simulation d'une conversation
ask("C'est quoi un Pod Kubernetes en une phrase ?")
ask("Et un Deployment, c'est quoi par rapport à un Pod ?")
ask("Donne-moi la commande pour créer un Deployment avec 3 réplicas")
ask("Comment je scale à 5 réplicas maintenant ?")
print("\n" + "=" * 50)
print(f"Total messages dans l'historique : {len(messages)}")

Le contexte tient parce que la liste messages le renvoie à chaque appel : la 4e question, « Comment je scale à 5 réplicas », fait référence au Deployment mentionné deux tours plus tôt, et le modèle le retrouve dans l'historique.

Les embeddings transforment du texte en vecteurs numériques. Deux textes similaires auront des vecteurs proches. C'est la base du RAG.

La fonction embed() renvoie toujours une liste de vecteurs, même pour une seule chaîne en entrée, d'où le response.embeddings[0] répété dans le script. La similarité cosinus mesure l'angle entre deux vecteurs et non leur distance : elle vaut 1 pour deux textes de sens identique et tend vers 0 pour deux textes sans rapport. Ne comparez jamais un score dans l'absolu, seul le classement relatif entre documents a un sens, et ce classement dépend entièrement du modèle utilisé.

06_embeddings.py
#!/usr/bin/env python3
"""
TP 06 : Embeddings avec Ollama
Objectif : Générer des vecteurs pour la recherche sémantique
Feature : Disponible depuis Ollama 0.3+
"""
from ollama import embed
import math
def cosine_similarity(vec1: list[float], vec2: list[float]) -> float:
"""Calcule la similarité cosinus entre deux vecteurs"""
dot_product = sum(a * b for a, b in zip(vec1, vec2))
norm1 = math.sqrt(sum(a ** 2 for a in vec1))
norm2 = math.sqrt(sum(b ** 2 for b in vec2))
return dot_product / (norm1 * norm2)
# Base de connaissances DevOps
documents = [
"Docker est une plateforme de conteneurisation qui permet de packager des applications",
"Kubernetes orchestre les conteneurs à grande échelle dans un cluster",
"Ansible automatise la configuration des serveurs et le déploiement",
"Terraform gère l'infrastructure as code pour le cloud",
"GitLab CI/CD permet l'intégration et le déploiement continus"
]
print("Génération des embeddings pour 5 documents...")
# Générer les embeddings pour chaque document
doc_embeddings = []
for i, doc in enumerate(documents):
response = embed(model='nomic-embed-text', input=doc)
doc_embeddings.append(response.embeddings[0])
print(f" Doc {i+1} : {len(response.embeddings[0])} dimensions")
print(f"\nDimension des vecteurs : {len(doc_embeddings[0])}")
# Question de test
query = "Comment déployer des conteneurs en production ?"
print(f"\nRequête : {query}")
# Générer l'embedding de la requête
query_response = embed(model='nomic-embed-text', input=query)
query_embedding = query_response.embeddings[0]
# Calculer la similarité avec chaque document
print("\nScores de similarité :")
scores = []
for i, doc_emb in enumerate(doc_embeddings):
score = cosine_similarity(query_embedding, doc_emb)
scores.append((score, documents[i]))
print(f" {score:.4f} → {documents[i][:50]}...")
# Trier par score décroissant
scores.sort(key=lambda s: s[0], reverse=True)
print(f"\nDocument le plus pertinent :")
print(f" Score: {scores[0][0]:.4f}")
print(f" {scores[0][1]}")

Sortie attendue :

Génération des embeddings pour 5 documents...
Doc 1 : 768 dimensions
Doc 2 : 768 dimensions
...
Scores de similarité :
0.6602 → Docker est une plateforme de conteneurisation qui ...
0.6462 → Kubernetes orchestre les conteneurs à grande échel...
0.5821 → Ansible automatise la configuration des serveurs e...
0.5903 → Terraform gère l'infrastructure as code pour le cl...
0.5528 → GitLab CI/CD permet l'intégration et le déploiemen...
Document le plus pertinent :
Score: 0.6602
Docker est une plateforme de conteneurisation qui permet de packager des applications

Le classement est cohérent : à la question « comment déployer des conteneurs en production », Docker et Kubernetes arrivent en tête, devant Terraform, Ansible et GitLab CI/CD. Vos scores peuvent différer de quelques centièmes selon la version du modèle et le processeur, mais le classement doit rester le même. C'est ce qu'apporte un modèle d'embedding dédié : ses vecteurs sont entraînés pour que des textes de sens proche soient voisins. Un modèle de conversation comme llama3.2:3b n'est pas fait pour cela : sur Ollama 0.40.4, embed(model='llama3.2:3b', ...) échoue avec l'erreur HTTP 501 This server does not support embeddings. Si vous la rencontrez, remplacez le modèle par nomic-embed-text.

La leçon Embeddings et similarité reprend ces notions (dimension, similarité cosinus, choix du modèle) hors de tout assistant.

Combinons toutes les fonctionnalités dans un assistant DevOps interactif.

Le script est long, lisez-le par blocs plutôt que ligne à ligne. Trois parties portent la logique. find_relevant_context() réalise la récupération : elle compare l'embedding de la question à ceux de la base et renvoie les deux documents les plus proches. chat_with_rag() réalise l'augmentation : elle colle ce contexte devant la question avant de l'envoyer au modèle. Le reste est de l'habillage de terminal.

Trois limites assumées, qui distinguent ce prototype d'un vrai système RAG. Les embeddings sont recalculés à chaque démarrage et vivent en mémoire : aucune persistance, aucune base vectorielle. La recherche est linéaire, elle compare la question à tous les documents : sur six entrées c'est instantané, sur dix mille il faut un index dédié. Enfin, le modèle d'embedding (constante EMBED_MODEL) est distinct du modèle de conversation (MODEL), comme dans le script 06 : si vous n'avez pas tiré nomic-embed-text, le démarrage échoue.

07_devops_buddy.py
#!/usr/bin/env python3
"""
TP 07 : DevOps Buddy - Assistant local complet
Objectif : Combiner toutes les fonctionnalités Ollama dans un assistant interactif
Features utilisées : chat, streaming, structured output, embeddings
"""
from ollama import chat, embed
from pydantic import BaseModel
import math
# === Configuration ===
MODEL = 'llama3.2:3b'
EMBED_MODEL = 'nomic-embed-text' # modèle dédié aux embeddings (ollama pull nomic-embed-text)
SYSTEM_PROMPT = '''Tu es DevOps Buddy, un assistant expert DevOps/DevSecOps.
Tu réponds en français de manière concise et technique.
Tu donnes des exemples de commandes quand c'est pertinent.
Tu structures tes réponses avec des listes à puces.'''
# === Base de connaissances embarquée ===
KNOWLEDGE_BASE = [
{"topic": "docker", "content": "Docker utilise des conteneurs pour isoler les applications. Commandes: docker build, docker run, docker compose"},
{"topic": "kubernetes", "content": "Kubernetes orchestre les conteneurs. Concepts: Pod, Deployment, Service, Ingress. Commandes: kubectl get, kubectl apply"},
{"topic": "ansible", "content": "Ansible automatise la configuration. Concepts: playbook, inventory, role, task. Commande: ansible-playbook"},
{"topic": "terraform", "content": "Terraform gère l'infrastructure as code. Concepts: provider, resource, module, state. Commandes: terraform init, plan, apply"},
{"topic": "gitlab", "content": "GitLab CI/CD pipeline avec .gitlab-ci.yml. Concepts: stages, jobs, artifacts, cache, runners"},
{"topic": "prometheus", "content": "Prometheus collecte les métriques. Concepts: scraping, PromQL, alertmanager, exporters"},
]
# Pré-calculer les embeddings de la base de connaissances
print("Chargement de la base de connaissances...")
knowledge_embeddings = []
for item in KNOWLEDGE_BASE:
response = embed(model=EMBED_MODEL, input=item["content"])
knowledge_embeddings.append(response.embeddings[0])
print(f"{len(KNOWLEDGE_BASE)} documents indexés\n")
# === Fonctions utilitaires ===
def cosine_similarity(vec1: list[float], vec2: list[float]) -> float:
dot = sum(a * b for a, b in zip(vec1, vec2))
norm1 = math.sqrt(sum(a ** 2 for a in vec1))
norm2 = math.sqrt(sum(b ** 2 for b in vec2))
return dot / (norm1 * norm2) if norm1 and norm2 else 0
def find_relevant_context(query: str, top_k: int = 2) -> str:
"""Recherche les documents pertinents pour enrichir le contexte"""
query_emb = embed(model=EMBED_MODEL, input=query).embeddings[0]
scores = []
for i, doc_emb in enumerate(knowledge_embeddings):
score = cosine_similarity(query_emb, doc_emb)
scores.append((score, KNOWLEDGE_BASE[i]))
scores.sort(key=lambda s: s[0], reverse=True)
top_docs = scores[:top_k]
context = "\n".join([f"- {doc['content']}" for _, doc in top_docs])
return context
# === Schéma pour les commandes structurées ===
class CommandSuggestion(BaseModel):
command: str
description: str
warning: str | None = None
def get_structured_command(query: str) -> CommandSuggestion | None:
"""Obtenir une suggestion de commande structurée"""
try:
response = chat(
model=MODEL,
messages=[
{'role': 'system', 'content': 'Tu suggères des commandes DevOps. Réponds uniquement en JSON.'},
{'role': 'user', 'content': f'Suggère une commande pour: {query}'}
],
format=CommandSuggestion.model_json_schema(),
options={'temperature': 0, 'num_predict': 200}
)
return CommandSuggestion.model_validate_json(response.message.content)
except Exception as e:
print(f"Erreur structured output: {e}")
return None
# === Chat avec RAG ===
def chat_with_rag(question: str, history: list) -> str:
"""Répond à une question avec contexte enrichi"""
# Trouver le contexte pertinent
context = find_relevant_context(question)
# Construire le prompt enrichi
augmented_question = f"""Contexte (base de connaissances):
{context}
Question de l'utilisateur: {question}"""
# Ajouter au historique
history.append({'role': 'user', 'content': augmented_question})
# Appeler le modèle avec streaming
response_text = ""
stream = chat(
model=MODEL,
messages=history,
stream=True,
options={'temperature': 0.3}
)
for chunk in stream:
content = chunk.message.content
print(content, end='', flush=True)
response_text += content
print() # Nouvelle ligne après le streaming
# Ajouter la réponse à l'historique
history.append({'role': 'assistant', 'content': response_text})
return response_text
# === Main ===
def main():
print("=" * 60)
print("DevOps Buddy - Assistant DevOps Local (Ollama)")
print("=" * 60)
print(f"Modèle: {MODEL}")
print("Commandes spéciales:")
print(" /cmd <action> : Obtenir une commande structurée")
print(" /quit : Quitter")
print("-" * 60)
history = [{'role': 'system', 'content': SYSTEM_PROMPT}]
while True:
try:
user_input = input("\nVous: ").strip()
if not user_input:
continue
if user_input.lower() == '/quit':
print("À bientôt!")
break
if user_input.startswith('/cmd '):
# Mode commande structurée
query = user_input[5:]
print("\nRecherche de commande...")
result = get_structured_command(query)
if result:
print(f"\nCommande: {result.command}")
print(f"Description: {result.description}")
if result.warning:
print(f"Attention: {result.warning}")
else:
print("Impossible de générer la commande")
else:
# Mode conversation avec RAG
print("\nDevOps Buddy: ", end='')
chat_with_rag(user_input, history)
except EOFError:
print("\nÀ bientôt!")
break
except KeyboardInterrupt:
print("\nInterruption - À bientôt!")
break
if __name__ == "__main__":
main()

Le démarrage prend quelques secondes, le temps de calculer les six embeddings de la base. L'assistant propose ensuite deux modes qu'il est utile de comparer : une question libre passe par le RAG en streaming, tandis que le préfixe /cmd bascule sur la sortie structurée et affiche une commande, sa description et un avertissement facultatif. La différence se sent à l'usage : le mode conversation affiche les premiers mots presque tout de suite, alors que le mode /cmd reste muet jusqu'à ce que le JSON complet soit généré et validé, puisqu'un JSON partiel serait inutilisable.

Fenêtre de terminal
python 07_devops_buddy.py

Exemple de session :

Chargement de la base de connaissances...
6 documents indexés
============================================================
DevOps Buddy - Assistant DevOps Local (Ollama)
============================================================
Modèle: llama3.2:3b
Commandes spéciales:
/cmd <action> : Obtenir une commande structurée
/quit : Quitter
------------------------------------------------------------
Vous: C'est quoi un Pod ?
DevOps Buddy: Un Pod est l'unité de base dans Kubernetes...
Vous: /cmd lister les pods kubernetes
Recherche de commande...
Commande: kubectl get pods
Description: Liste tous les pods dans le namespace courant
Attention: Ajoutez -A pour lister tous les namespaces

Passez cette liste avant d'aller plus loin : chaque script isole une capacité du SDK, et les suivants s'appuient sur les précédents. Si 03_structured_output.py échoue, inutile de lancer DevOps Buddy, il utilise le même mécanisme.

Ces quatre vérifications éliminent la cause la plus fréquente d'échec, un serveur et un SDK qui ne parlent pas de la même version d'API. La première lit la version du serveur, la deuxième celle du SDK.

  • Serveur joignable et version lue (ollama --version)
  • SDK ollama installé (pip show ollama)
  • Pydantic installé (pip show pydantic)
  • Modèles téléchargés : llama3.2:3b et nomic-embed-text apparaissent tous deux dans ollama list

Exécutez-les dans l'ordre. Si 06_embeddings.py ou 07 échoue sur model not found, lancez ollama pull nomic-embed-text. Une erreur 501 signifie qu'un modèle de chat a été passé à embed(). Des scores proches (autour de 0,55 à 0,66) sont une sortie normale : le bon signe est Docker et Kubernetes en tête du classement.

  • 01_chat_basique.py : réponse + métadonnées
  • 02_streaming.py : affichage temps réel
  • 03_structured_output.py : JSON validé par Pydantic
  • 04_structured_avance.py : listes d'objets
  • 05_conversation.py : historique multi-turn
  • 06_embeddings.py : similarité cosinus
  • 07_devops_buddy.py : assistant complet
  1. SDK natif vs LiteLLM : les deux savent produire du JSON structuré (format= côté SDK, response_format côté LiteLLM). Le SDK ollama donne en plus un accès direct aux options propres à Ollama (think=True, options) mais ne supporte que Ollama
  2. format= : Passez un schéma Pydantic pour obtenir du JSON typé
  3. stream=True : Affiche la réponse en temps réel
  4. embed() : Génère des embeddings pour la recherche sémantique
  5. Historique : Passez tous les messages pour maintenir le contexte
  6. options : Contrôlez la température, le nombre de tokens, etc.
  7. RAG local : Combinez embeddings + contexte pour enrichir les réponses

Vérifiez que l'essentiel de ce guide est acquis. Les questions portent uniquement sur ce qui vient d'être expliqué ici.

Contrôle de connaissances

Validez vos connaissances avec ce quiz interactif

6 questions
6 min.
70% requis

Informations

  • Le chronomètre démarre au clic sur Démarrer
  • Questions à choix multiples, vrai/faux et réponses courtes
  • Vous pouvez naviguer entre les questions
  • Les résultats détaillés sont affichés à la fin

Lance le quiz et démarre le chronomètre

Ce projet vous est utile ?

Les guides, formations, quiz et labs restent gratuits. Si vous les utilisez régulièrement, vous pouvez contribuer à leur maintenance et à leur développement.

Devenir membre de soutien (nouvel onglet) Soutenir une fois (nouvel onglet) En savoir plus

Suivez aussi le projet sur LinkedIn