Cas Concret : CRM Automatisé
Un CRM piloté par la conversation
Imaginez un CRM où les commerciaux n’ont plus besoin de naviguer dans des interfaces complexes. Ils parlent à un assistant qui crée, modifie et consulte les contacts automatiquement. C’est exactement ce que le function calling permet de construire.
Dans cette leçon, vous allez implémenter un CRM conversationnel avec des opérations CRUD (Create, Read, Update, Delete) sur une base de contacts.
La base de données simulée
import json
import os
from mistralai import Mistral
# Base de contacts en mémoire (en production : PostgreSQL, MongoDB, etc.)
contacts_db = {
"CT001": {
"name": "Marie Dupont",
"email": "[email protected]",
"company": "TechCorp",
"role": "Directrice Technique",
"phone": "+33 6 12 34 56 78",
"status": "client",
"notes": "Intéressée par la solution enterprise"
},
"CT002": {
"name": "Pierre Martin",
"email": "[email protected]",
"company": "StartupIO",
"role": "CEO",
"phone": "+33 6 98 76 54 32",
"status": "prospect",
"notes": "Rendez-vous prévu en avril"
}
}
next_id = 3 # Compteur pour les nouveaux contacts
Les fonctions CRUD
def get_contact(contact_id: str) -> str:
"""Récupère les informations d'un contact."""
if contact_id in contacts_db:
contact = contacts_db[contact_id].copy()
contact["contact_id"] = contact_id
return json.dumps(contact, ensure_ascii=False)
return json.dumps({"error": f"Contact {contact_id} introuvable"})
def search_contacts(query: str, field: str = "name") -> str:
"""Recherche des contacts par nom, entreprise ou statut."""
results = []
for cid, contact in contacts_db.items():
if field in contact and query.lower() in contact[field].lower():
entry = contact.copy()
entry["contact_id"] = cid
results.append(entry)
if results:
return json.dumps({"results": results, "count": len(results)},
ensure_ascii=False)
return json.dumps({"results": [], "count": 0,
"message": f"Aucun contact trouvé pour '{query}' dans '{field}'"})
def create_contact(name: str, email: str, company: str,
role: str = "", phone: str = "",
status: str = "prospect", notes: str = "") -> str:
"""Crée un nouveau contact dans le CRM."""
global next_id
# Vérifier si l'email existe déjà
for cid, contact in contacts_db.items():
if contact["email"].lower() == email.lower():
return json.dumps({
"error": "duplicate_email",
"message": f"Un contact avec cet email existe déjà : {cid}"
})
contact_id = f"CT{next_id:03d}"
next_id += 1
contacts_db[contact_id] = {
"name": name,
"email": email,
"company": company,
"role": role,
"phone": phone,
"status": status,
"notes": notes
}
return json.dumps({
"success": True,
"contact_id": contact_id,
"message": f"Contact {name} créé avec l'identifiant {contact_id}"
}, ensure_ascii=False)
def update_contact(contact_id: str, **fields) -> str:
"""Met à jour un ou plusieurs champs d'un contact existant."""
if contact_id not in contacts_db:
return json.dumps({"error": f"Contact {contact_id} introuvable"})
updated_fields = []
for key, value in fields.items():
if key in contacts_db[contact_id]:
contacts_db[contact_id][key] = value
updated_fields.append(key)
if updated_fields:
return json.dumps({
"success": True,
"contact_id": contact_id,
"updated_fields": updated_fields,
"message": f"Contact {contact_id} mis à jour : {', '.join(updated_fields)}"
}, ensure_ascii=False)
return json.dumps({"error": "Aucun champ valide à mettre à jour"})
Les spécifications JSON des tools
tools = [
{
"type": "function",
"function": {
"name": "get_contact",
"description": "Récupère toutes les informations d'un contact par son identifiant",
"parameters": {
"type": "object",
"properties": {
"contact_id": {
"type": "string",
"description": "Identifiant du contact (ex: CT001)"
}
},
"required": ["contact_id"]
}
}
},
{
"type": "function",
"function": {
"name": "search_contacts",
"description": "Recherche des contacts par nom, entreprise ou statut",
"parameters": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "Terme de recherche"
},
"field": {
"type": "string",
"enum": ["name", "company", "status", "role"],
"description": "Champ dans lequel chercher (défaut: name)"
}
},
"required": ["query"]
}
}
},
{
"type": "function",
"function": {
"name": "create_contact",
"description": "Crée un nouveau contact dans le CRM",
"parameters": {
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "Nom complet du contact"
},
"email": {
"type": "string",
"description": "Adresse email du contact"
},
"company": {
"type": "string",
"description": "Nom de l'entreprise"
},
"role": {
"type": "string",
"description": "Poste ou fonction dans l'entreprise"
},
"phone": {
"type": "string",
"description": "Numéro de téléphone"
},
"status": {
"type": "string",
"enum": ["prospect", "client", "partenaire", "inactif"],
"description": "Statut du contact"
},
"notes": {
"type": "string",
"description": "Notes libres sur le contact"
}
},
"required": ["name", "email", "company"]
}
}
},
{
"type": "function",
"function": {
"name": "update_contact",
"description": "Met à jour les informations d'un contact existant",
"parameters": {
"type": "object",
"properties": {
"contact_id": {
"type": "string",
"description": "Identifiant du contact à modifier (ex: CT001)"
},
"name": {"type": "string", "description": "Nouveau nom"},
"email": {"type": "string", "description": "Nouvel email"},
"company": {"type": "string", "description": "Nouvelle entreprise"},
"role": {"type": "string", "description": "Nouveau poste"},
"phone": {"type": "string", "description": "Nouveau téléphone"},
"status": {
"type": "string",
"enum": ["prospect", "client", "partenaire", "inactif"],
"description": "Nouveau statut"
},
"notes": {"type": "string", "description": "Nouvelles notes"}
},
"required": ["contact_id"]
}
}
}
]
Le dispatch et la boucle
available_functions = {
"get_contact": get_contact,
"search_contacts": search_contacts,
"create_contact": create_contact,
"update_contact": update_contact,
}
client = Mistral(api_key=os.environ["MISTRAL_API_KEY"])
model = "mistral-large-latest"
messages = [
{
"role": "system",
"content": "Vous êtes un assistant CRM. Vous aidez les commerciaux "
"à gérer leurs contacts. Répondez en français. "
"Quand vous créez ou modifiez un contact, confirmez "
"l'action avec les détails. Demandez les informations "
"manquantes avant de créer un contact."
}
]
Scénarios de conversation
# Scénario 1 : Rechercher un contact
chat("Trouve-moi les contacts chez TechCorp")
# -> Le modèle appelle search_contacts(query="TechCorp", field="company")
# -> "J'ai trouvé 1 contact chez TechCorp : Marie Dupont (CT001),
# Directrice Technique, statut client."
# Scénario 2 : Créer un contact
chat("Ajoute un nouveau contact : Sophie Laurent, [email protected], "
"DataViz, Data Scientist")
# -> Le modèle appelle create_contact(name="Sophie Laurent",
# email="[email protected]", company="DataViz", role="Data Scientist")
# -> "Contact créé avec succès ! Sophie Laurent (CT003) a été ajoutée
# comme prospect chez DataViz."
# Scénario 3 : Modifier un statut
chat("Passe Pierre Martin en client, il vient de signer")
# -> Le modèle cherche d'abord Pierre Martin, puis appelle
# update_contact(contact_id="CT002", status="client",
# notes="Contrat signé en avril 2026")
# -> "Pierre Martin (CT002) est maintenant marqué comme client.
# J'ai ajouté une note sur la signature du contrat."
# Scénario 4 : Question sans fonction
chat("Combien de prospects avons-nous ?")
# -> Le modèle appelle search_contacts(query="prospect", field="status")
# -> "Vous avez actuellement 1 prospect : Sophie Laurent chez DataViz."
Architecture pour la production
En production, remplacez le dictionnaire en mémoire par une vraie base de données :
# Les fonctions appellent l'ORM ou l'API de votre CRM
def get_contact_production(contact_id: str) -> str:
"""Version production avec PostgreSQL."""
contact = db.session.query(Contact).filter_by(id=contact_id).first()
if contact:
return json.dumps(contact.to_dict(), ensure_ascii=False)
return json.dumps({"error": f"Contact {contact_id} introuvable"})
Le reste du code (tools, dispatch, boucle de conversation) reste identique. C’est la force de cette architecture : seules les fonctions métier changent.
Points clés à retenir
- Le function calling transforme un LLM en interface naturelle pour un CRM
- Les opérations CRUD se mappent directement sur des fonctions avec JSON Schema
- Le modèle peut enchaîner recherche puis modification dans une même conversation
- L’architecture est découplée : changez la base de données sans toucher à la logique de conversation
- Le message système est crucial pour guider le modèle sur les confirmations et les informations manquantes