Projet (1/2) : cahier des charges et outils
Le cahier des charges
Une boutique en ligne veut un assistant conversationnel (le même métier qu'un agent de commande sur WhatsApp comme Watpilot). Il doit :
- chercher un produit dans le catalogue par mot-clé (outil 1) ;
- consulter le stock d'une référence (outil 2) ;
- créer une commande (outil 3), uniquement après un récapitulatif et un « oui » explicite du client, avec au plus 50 unités et un stock suffisant ;
- transférer à un humain sur réclamation, demande hors périmètre ou demande explicite, et quand la boucle n'aboutit pas ;
- ne jamais inventer un prix, une référence ou un stock ;
- garder le contexte d'un message à l'autre (« oui » doit avoir un sens).
flowchart TB U["Client"] --> A["Assistant : boucle + historique"] A --> L["call_llm (module 2)"] A --> T1["chercher_produit"] A --> T2["consulter_stock"] A --> T3["creer_commande : confirmation, plafond, stock"] A --> H["transferer_a_un_humain"] A -->|"max_steps atteint"| H H --> Q["File d'attente des conseillers"]
Les étapes
- Reprenez
agent.pydu module 2 (call_llmet les deux premiers outils), et ajoutez au catalogue"P3": ("Chocolat noir 100 g", 3.2)avec un stock de 40. - Écrivez
creer_commandeettransferer_a_un_humain, puis les 4 schémas et le prompt système (cette partie). - Assemblez la classe
Assistant, testez et évaluez (partie 2).
| Règle métier | Où la mettre | Pourquoi |
|---|---|---|
| Pas de commande sans confirmation | paramètre confirme + description de l'outil | oblige le modèle à récapituler d'abord |
| Au plus 50 unités | dans le code de creer_commande | un prompt peut être contourné |
| Stock suffisant | dans le code de creer_commande | donnée exacte, pas une estimation du modèle |
| Commande au nom du bon client | client_id injecté par le code | le modèle ne doit pas pouvoir le choisir |
Le modèle doit-il fournir le client_id en argument de creer_commande ?
Si le modèle pouvait choisir le client_id, une injection de prompt suffirait à commander au nom d'un autre client. Le code lie l'outil au client de la session ; le modèle ne voit que ref, quantite et confirme.
Le paramètre confirme=true de creer_commande est rempli par le modèle. Quelles affirmations sont justes ? (plusieurs réponses)
confirme est un garde-fou utile mais déclaratif : le modèle pourrait se tromper. Une vraie garantie passe par une validation hors du LLM. Les contrôles déterministes (plafond, stock) sont toujours faits par le code.
Exercice : les outils, les schémas et le prompt
Dans assistant_commandes.py (qui fait from agent import call_llm, ainsi que vos CATALOGUE, STOCK, chercher_produit et consulter_stock), écrivez :
creer_commande(client_id, ref, quantite, confirme)qui applique les règles du tableau, décrémente le stock, enregistre la commande dans un dictionnaireCOMMANDESet renvoie{"numero", "total"};transferer_a_un_humain(client_id, motif)qui ajoute une entrée à une listeFILE_HUMAINE;- la liste
TOOLSdes 4 schémas (sansclient_id) et le promptSYSTEM.
Chaque règle violée doit lever une exception au message clair : run_tool la renverra au modèle.
Voir la solution
import json
import uuid
COMMANDES = {} # simplification : une vraie base en production
FILE_HUMAINE = [] # simplification : un outil de ticketing en production
QUANTITE_MAX = 50
def creer_commande(client_id: str, ref: str, quantite: int, confirme: bool) -> dict:
if not confirme:
raise ValueError("Récapitule (produit, quantité, total) et demande "
"confirmation au client avant de créer la commande.")
if ref not in CATALOGUE:
raise ValueError(f"Référence inconnue : {ref}")
if not 1 <= quantite <= QUANTITE_MAX:
raise ValueError(f"Quantité hors limites (1 à {QUANTITE_MAX}) : "
"propose le transfert à un conseiller.")
if STOCK[ref] < quantite:
raise ValueError(f"Stock insuffisant : {STOCK[ref]} disponible(s).")
STOCK[ref] -= quantite
numero = "CMD-" + uuid.uuid4().hex[:8].upper()
total = round(CATALOGUE[ref][1] * quantite, 2)
COMMANDES[numero] = {"client": client_id, "ref": ref,
"quantite": quantite, "total": total}
return {"numero": numero, "total": total}
def transferer_a_un_humain(client_id: str, motif: str) -> dict:
FILE_HUMAINE.append({"client": client_id, "motif": motif})
return {"transfere": True, "message": "Un conseiller reprend la conversation."}
def schema(name, description, **props):
return {"type": "function", "function": {
"name": name, "description": description,
"parameters": {"type": "object", "properties": props,
"required": list(props)}}}
TOOLS = [
schema("chercher_produit", "Cherche des produits du catalogue par mot-clé. "
"Renvoie référence, nom et prix.", texte={"type": "string"}),
schema("consulter_stock", "Quantité en stock d'une référence (ex. P1). "
"Appeler après chercher_produit.", ref={"type": "string"}),
schema("creer_commande", "Crée la commande. Mettre confirme=true UNIQUEMENT si le "
"client vient de dire explicitement oui au récapitulatif.",
ref={"type": "string"}, quantite={"type": "integer", "minimum": 1},
confirme={"type": "boolean"}),
schema("transferer_a_un_humain", "Passe la main à un conseiller : réclamation, "
"demande hors catalogue, client mécontent, ou demande explicite.",
motif={"type": "string"}),
]
SYSTEM = (
"Tu es l'assistant de commandes d'une boutique en ligne. Tu aides à trouver un "
"produit, vérifier le stock et passer commande. Règles : n'invente jamais un prix, "
"une référence ni un stock, utilise les outils ; avant de créer une commande, "
"récapitule produit, quantité et total et attends un oui explicite ; hors de ce "
"périmètre, ou si le client le demande, transfère à un humain. Réponds en "
"français, en phrases courtes."
)
ESCALADE = "Je vous mets en relation avec un conseiller."
Le petit utilitaire schema() évite de répéter la structure JSON quatre fois ; tous les paramètres y sont obligatoires, ce qui convient ici.
Je sais faire
- Traduire un cahier des charges en outils, paramètres et règles
- Décider pour chaque règle si elle va dans le prompt, le schéma ou le code
- Écrire des outils qui lèvent des erreurs lisibles par le modèle
- Cacher au modèle les paramètres qu'il ne doit pas contrôler (
client_id)