Section 11.5.6 : Développer le backend et l'API
🎯 Objectif pédagogique
Structurer le backend de l'application IA : modulariser le code, créer une couche API propre, gérer la configuration et les erreurs. Vous serez capable d'organiser le code backend d'un projet IA de manière professionnelle.
Du script au backend structuré
Marc a du code qui fonctionne. Mais tout est mélangé. Un backend professionnel est modulaire : chaque fichier a une responsabilité claire, les données circulent proprement, et les erreurs sont gérées.
config.py — Le cerveau de la configuration
# config.py — Configuration centralisée
import os
from dotenv import load_dotenv
load_dotenv()
# API Keys
OPENAI_API_KEY = os.environ.get("OPENAI_API_KEY")
ANTHROPIC_API_KEY = os.environ.get("ANTHROPIC_API_KEY")
# Modèles
DEFAULT_MODEL = "gpt-4o-mini"
PREMIUM_MODEL = "gpt-4o"
EMBEDDING_MODEL = "text-embedding-3-small"
# Limites
MAX_INPUT_LENGTH = 5000
MAX_HISTORY_MESSAGES = 20
SUMMARY_THRESHOLD = 15
MAX_TOKENS_RESPONSE = 1000
# ChromaDB
CHROMA_DB_PATH = "./data/chroma_db"
KNOWLEDGE_COLLECTION = "career_knowledge"
# System Prompt
SYSTEM_PROMPT = """Tu es CareerBot Pro, un assistant expert en reconversion tech/data/IA.
## Personnalité
- Ton : professionnel, chaleureux, encourageant
- Tu tutoies l'utilisateur
- Direct, concret, orienté action
## Compétences
- Analyse de profils et offres d'emploi
- Conseil en formation et montée en compétences
- Préparation aux entretiens
- Données marché (salaires, tendances)
## Règles
- Base tes réponses sur la base de connaissances quand disponible
- Cite tes sources quand tu utilises la base
- Limite tes réponses à 300 mots max
- Termine par une question ou suggestion d'action
- NE JAMAIS inventer d'offres d'emploi
- NE JAMAIS donner de conseil juridique ou médical
"""
# App Config
APP_CONFIG = \{
"title": "CareerBot Pro — Assistant Reconversion Tech",
"icon": "🤖",
"theme_color": "#0891B2",
\}
knowledge.py — Gestion de la base de connaissances
# knowledge.py — Indexation et recherche
import chromadb
from config import CHROMA_DB_PATH, KNOWLEDGE_COLLECTION
from pathlib import Path
class KnowledgeBase:
"""Gestion de la base de connaissances vectorielle."""
def __init__(self):
self.client = chromadb.PersistentClient(path=CHROMA_DB_PATH)
self.collection = self.client.get_or_create_collection(
name=KNOWLEDGE_COLLECTION,
metadata=\{"hnsw:space": "cosine"\}
)
def index_directory(self, directory="data/knowledge"):
"""Indexer tous les fichiers .txt d'un dossier."""
knowledge_dir = Path(directory)
if not knowledge_dir.exists():
print(f"Dossier \{directory\} non trouvé.")
return
all_chunks = []
all_ids = []
all_metadatas = []
for filepath in knowledge_dir.glob("*.txt"):
content = filepath.read_text(encoding="utf-8")
chunks = self._chunk_text(content)
for i, chunk in enumerate(chunks):
all_chunks.append(chunk)
all_ids.append(f"\{filepath.stem\}_chunk_\{i\}")
all_metadatas.append(\{
"source": filepath.stem,
"chunk_index": i
\})
if all_chunks:
self.collection.upsert(
documents=all_chunks,
ids=all_ids,
metadatas=all_metadatas
)
print(f"Indexé \{len(all_chunks)\} chunks depuis \{len(list(knowledge_dir.glob('*.txt')))\} fichiers.")
def search(self, query, n_results=3):
"""Recherche sémantique."""
results = self.collection.query(
query_texts=[query],
n_results=n_results
)
return [
\{"text": doc, "source": meta["source"]\}
for doc, meta in zip(results["documents"][0], results["metadatas"][0])
]
def _chunk_text(self, text, chunk_size=500, overlap=50):
"""Découper un texte en chunks."""
words = text.split()
chunks = []
for i in range(0, len(words), chunk_size - overlap):
chunk = " ".join(words[i:i + chunk_size])
if chunk.strip():
chunks.append(chunk)
return chunks
def get_stats(self):
"""Statistiques de la base."""
return \{
"total_documents": self.collection.count(),
"collection_name": KNOWLEDGE_COLLECTION
\}
tools.py — Définition des outils de l'agent
# tools.py — Fonctions et définitions OpenAI
import json
from knowledge import KnowledgeBase
from openai import OpenAI
from config import OPENAI_API_KEY, DEFAULT_MODEL
client = OpenAI(api_key=OPENAI_API_KEY)
kb = KnowledgeBase()
# Définitions OpenAI format
TOOL_DEFINITIONS = [
\{
"type": "function",
"function": \{
"name": "search_knowledge",
"description": "Recherche dans la base de connaissances carrière.",
"parameters": \{
"type": "object",
"properties": \{
"query": \{"type": "string", "description": "Question à rechercher"\}
\},
"required": ["query"]
\}
\}
\},
\{
"type": "function",
"function": \{
"name": "analyze_profile",
"description": "Analyse la compatibilité d'un profil avec un poste.",
"parameters": \{
"type": "object",
"properties": \{
"profile": \{"type": "string"\},
"target_role": \{"type": "string"\}
\},
"required": ["profile", "target_role"]
\}
\}
\},
\{
"type": "function",
"function": \{
"name": "create_action_plan",
"description": "Crée un plan d'action personnalisé.",
"parameters": \{
"type": "object",
"properties": \{
"goal": \{"type": "string"\},
"timeline": \{"type": "string"\}
\},
"required": ["goal"]
\}
\}
\}
]
# Implémentations
def search_knowledge(query):
chunks = kb.search(query, n_results=3)
if not chunks:
return "Aucune information trouvée dans la base."
return "\n".join([f"[\{c['source']\}] \{c['text']\}" for c in chunks])
def analyze_profile(profile, target_role):
context = search_knowledge(f"compétences \{target_role\}")
response = client.chat.completions.create(
model=DEFAULT_MODEL,
messages=[\{
"role": "user",
"content": f"Analyse profil vs poste :\nProfil: \{profile\}\nPoste: \{target_role\}\nContexte: \{context\}\nScore 1-10, Forces, Gaps, Actions."
\}],
max_tokens=500
)
return response.choices[0].message.content
def create_action_plan(goal, timeline="3 mois"):
context = search_knowledge(f"formation \{goal\}")
response = client.chat.completions.create(
model=DEFAULT_MODEL,
messages=[\{
"role": "user",
"content": f"Plan d'action :\nObjectif: \{goal\}\nDurée: \{timeline\}\nContexte: \{context\}\nFormat: semaine par semaine, actions concrètes."
\}],
max_tokens=800
)
return response.choices[0].message.content
# Mapping
TOOL_MAP = \{
"search_knowledge": search_knowledge,
"analyze_profile": analyze_profile,
"create_action_plan": create_action_plan,
\}
Convention de noms
Chaque fichier = une responsabilité. config.py ne fait que de la configuration. knowledge.py ne fait que du RAG. tools.py ne fait que des tools. Quand un bug survient, vous savez IMMÉDIATEMENT dans quel fichier chercher.
🏋️ Exercice pratique (30 minutes)
- →Créez les 3 fichiers :
config.py,knowledge.py,tools.py - →Testez
knowledge.pyen isolation (indexer 5 docs, chercher) - →Testez
tools.pyen isolation (appeler chaque outil) - →Vérifiez que les imports fonctionnent entre modules
Pourquoi séparer config.py des autres fichiers plutôt que mettre les constantes directement dans chaque fichier ?