En tant qu'architecte backend ayant conçu et optimisé des systèmes de conversation IA pour des millions d'utilisateurs, je souhaite partager mon retour d'expérience concret sur la conception de bases de données adaptées aux applications LLM. Ce tutoriel couvre l'architecture production-ready, les optimisations de performance, et les stratégies de réduction des coûts opérationnels.
Architecture Générique du Système
Une application IA conversationnelle robuste nécessite une architecture multicouche. Le stockage des conversations et préférences constitue le cœur de la persistance applicative. Voici l'architecture que j'ai déployée chez plusieurs clients Fortune 500.
Conception du Schéma de Base de Données
La conception initiale conditionne les performances sur des volumes importants. J'utilise PostgreSQL pour sa robustesse et ses fonctionnalités JSON natives excellentes.
-- Schema PostgreSQL optimisé pour applications IA conversationnelles
-- Version: PostgreSQL 16+
-- Table des utilisateurs avec préférences enrichies
CREATE TABLE users (
user_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
external_id VARCHAR(255) UNIQUE NOT NULL,
email VARCHAR(255),
display_name VARCHAR(100),
preferences JSONB DEFAULT '{}',
metadata JSONB DEFAULT '{}',
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
last_seen_at TIMESTAMP WITH TIME ZONE,
is_active BOOLEAN DEFAULT true
);
-- Table des conversations avec partitionnement temporel
CREATE TABLE conversations (
conversation_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(user_id) ON DELETE CASCADE,
title VARCHAR(255),
system_prompt TEXT,
model_preference VARCHAR(50) DEFAULT 'gpt-4.1',
context_window_tokens INT DEFAULT 128000,
metadata JSONB DEFAULT '{}',
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
ended_at TIMESTAMP WITH TIME ZONE,
is_archived BOOLEAN DEFAULT false
) PARTITION BY RANGE (created_at);
-- Création des partitions mensuelles pour 2026
CREATE TABLE conversations_2026_01 PARTITION OF conversations
FOR VALUES FROM ('2026-01-01') TO ('2026-02-01');
CREATE TABLE conversations_2026_02 PARTITION OF conversations
FOR VALUES FROM ('2026-02-01') TO ('2026-03-01');
-- Ajouter les partitions jusqu'à décembre 2026
-- Table des messages avec stockage vectoriel
CREATE TABLE messages (
message_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
conversation_id UUID NOT NULL REFERENCES conversations(conversation_id) ON DELETE CASCADE,
role VARCHAR(20) NOT NULL CHECK (role IN ('system', 'user', 'assistant', 'function')),
content TEXT NOT NULL,
content_tokens INT,
embedding VECTOR(1536),
function_call JSONB,
function_response JSONB,
metadata JSONB DEFAULT '{}',
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
parent_message_id UUID REFERENCES messages(message_id),
message_index INT
);
-- Table des préférences utilisateur enrichies
CREATE TABLE user_preferences (
preference_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(user_id) ON DELETE CASCADE,
preference_type VARCHAR(50) NOT NULL,
preference_key VARCHAR(100) NOT NULL,
preference_value JSONB NOT NULL,
confidence_score DECIMAL(3,2) DEFAULT 1.00,
source VARCHAR(50) DEFAULT 'explicit',
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
UNIQUE(user_id, preference_type, preference_key)
);
-- Index optimisés pour les requêtes fréquentes
CREATE INDEX idx_conversations_user_id ON conversations(user_id);
CREATE INDEX idx_conversations_updated_at ON conversations(updated_at DESC);
CREATE INDEX idx_messages_conversation_id ON messages(conversation_id);
CREATE INDEX idx_messages_created_at ON messages(created_at DESC);
CREATE INDEX idx_user_preferences_user_id ON user_preferences(user_id);
CREATE INDEX idx_messages_embedding ON messages USING ivfflat (embedding vector_cosine_ops)
WITH (lists = 100);
-- Trigger pour mise à jour automatique des timestamps
CREATE OR REPLACE FUNCTION update_updated_at_column()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = NOW();
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER update_users_updated_at
BEFORE UPDATE ON users
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER update_conversations_updated_at
BEFORE UPDATE ON conversations
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
Intégration HolySheep pour Appels IA
Pour les appels API IA, j'utilise HolySheep AI qui offre des tarifs imbattables : 85% d'économie par rapport aux tarifs officiels, avec une latence moyenne de 45ms. Les prix 2026/MTok sont particulièrement compétitifs.
#!/usr/bin/env python3
"""
Client IA générique pour gestion de conversations avec HolySheep API
Compatible avec plusieurs modèles LLM via API unifiée.
"""
import os
import json
import time
import tiktoken
from typing import Optional, List, Dict, Any, AsyncIterator
from dataclasses import dataclass, field, asdict
from datetime import datetime, timezone
from uuid import UUID, uuid4
import httpx
from sqlalchemy import create_engine, select, update, delete
from sqlalchemy.orm import sessionmaker, Session
from sqlalchemy.dialects.postgresql import insert
from contextlib import asynccontextmanager
Configuration HolySheep API
HOLYSHEEP_BASE_URL = "https://api.holysheep.ai/v1"
HOLYSHEEP_API_KEY = os.getenv("HOLYSHEEP_API_KEY", "YOUR_HOLYSHEEP_API_KEY")
Modèles disponibles avec leurs tarifs HolySheep 2026
MODELS_CONFIG = {
"gpt-4.1": {"input_cost": 8.00, "output_cost": 24.00, "context_window": 128000},
"claude-sonnet-4.5": {"input_cost": 15.00, "output_cost": 75.00, "context_window": 200000},
"gemini-2.5-flash": {"input_cost": 2.50, "output_cost": 10.00, "context_window": 1000000},
"deepseek-v3.2": {"input_cost": 0.42, "output_cost": 2.80, "context_window": 128000},
}
@dataclass
class Message:
role: str
content: str
name: Optional[str] = None
function_call: Optional[Dict] = None
function_response: Optional[str] = None
@dataclass
class ConversationContext:
user_id: UUID
conversation_id: UUID
system_prompt: Optional[str] = None
model: str = "deepseek-v3.2" # Modèle économique par défaut
max_tokens: int = 4096
messages: List[Message] = field(default_factory=list)
class HolySheepAIClient:
"""Client optimisé pour HolySheep AI avec gestion de contexte."""
def __init__(self, api_key: str = HOLYSHEEP_API_KEY):
self.base_url = HOLYSHEEP_BASE_URL
self.api_key = api_key
self.encoder = tiktoken.get_encoding("cl100k_base")
def count_tokens(self, text: str) -> int:
"""Compte les tokens avec tiktoken."""
return len(self.encoder.encode(text))
def count_messages_tokens(self, messages: List[Message]) -> int:
"""Calcule le nombre total de tokens pour une liste de messages."""
total = 0
for msg in messages:
total += 4 # Overhead par message
total += self.count_tokens(msg.content)
if msg.name:
total += self.count_tokens(msg.name)
return total
async def chat_completion(
self,
messages: List[Dict[str, str]],
model: str = "deepseek-v3.2",
temperature: float = 0.7,
max_tokens: int = 4096,
stream: bool = False
) -> Dict[str, Any]:
"""Appel API optimisé vers HolySheep avec retry automatique."""
headers = {
"Authorization": f"Bearer {self.api_key}",
"Content-Type": "application/json"
}
payload = {
"model": model,
"messages": messages,
"temperature": temperature,
"max_tokens": max_tokens,
"stream": stream
}
async with httpx.AsyncClient(timeout=60.0) as client:
response = await client.post(
f"{self.base_url}/chat/completions",
headers=headers,
json=payload
)
response.raise_for_status()
return response.json()
def calculate_cost(self, model: str, input_tokens: int, output_tokens: int) -> float:
"""Calcule le coût en USD pour une requête."""
config = MODELS_CONFIG.get(model, MODELS_CONFIG["deepseek-v3.2"])
input_cost = (input_tokens / 1_000_000) * config["input_cost"]
output_cost = (output_tokens / 1_000_000) * config["output_cost"]
return round(input_cost + output_cost, 6)
class ConversationManager:
"""Gestionnaire de conversations avec persistance optimisée."""
def __init__(
self,
database_url: str,
ai_client: HolySheepAIClient,
max_context_tokens: int = 120000
):
self.engine = create_engine(database_url, pool_size=20, max_overflow=10)
self.SessionLocal = sessionmaker(bind=self.engine)
self.ai_client = ai_client
self.max_context_tokens = max_context_tokens
def _serialize_messages(self, messages: List[Message]) -> List[Dict[str, str]]:
"""Sérialise les messages pour l'API."""
serialized = []
for msg in messages:
msg_dict = {"role": msg.role, "content": msg.content}
if msg.name:
msg_dict["name"] = msg.name
if msg.function_call:
msg_dict["function_call"] = msg.function_call
if msg.function_response:
msg_dict["function_response"] = msg.function_response
serialized.append(msg_dict)
return serialized
def _truncate_context(
self,
messages: List[Message],
max_tokens: int
) -> List[Message]:
"""Tronque le contexte en préservant les messages les plus récents."""
truncated = []
current_tokens = 0
for msg in reversed(messages):
msg_tokens = self.ai_client.count_tokens(msg.content) + 4
if current_tokens + msg_tokens <= max_tokens:
truncated.insert(0, msg)
current_tokens += msg_tokens
else:
break
return truncated
async def send_message(
self,
context: ConversationContext,
user_content: str,
save_to_db: bool = True
) -> Dict[str, Any]:
"""Envoie un message et gère le contexte complet."""
# Ajouter le message utilisateur
context.messages.append(Message(role="user", content=user_content))
# Construire le contexte avec prompt système si présent
api_messages = []
if context.system_prompt:
api_messages.append({"role": "system", "content": context.system_prompt})
# Tronquer le contexte si nécessaire
available_tokens = context.max_tokens - 500 # Marge pour la réponse
truncated_messages = self._truncate_context(context.messages, available_tokens)
api_messages.extend(self._serialize_messages(truncated_messages))
# Mesurer les tokens d'entrée
input_tokens = self.ai_client.count_messages_tokens(
[Message(**m) for m in api_messages]
)
# Appel API HolySheep
start_time = time.time()
response = await self.ai_client.chat_completion(
messages=api_messages,
model=context.model,
max_tokens=context.max_tokens
)
latency_ms = (time.time() - start_time) * 1000
# Extraire la réponse
assistant_message = response["choices"][0]["message"]
output_tokens = response["usage"]["completion_tokens"]
# Calculer le coût
cost_usd = self.ai_client.calculate_cost(
context.model, input_tokens, output_tokens
)
# Sauvegarder en base si demandé
if save_to_db:
with self.SessionLocal() as db:
# Sauvegarder le message utilisateur
user_msg_record = {
"message_id": uuid4(),
"conversation_id": context.conversation_id,
"role": "user",
"content": user_content,
"content_tokens": self.ai_client.count_tokens(user_content),
"created_at": datetime.now(timezone.utc)
}
# INSERT en base...
# Sauvegarder la réponse assistant
assistant_msg_record = {
"message_id": uuid4(),
"conversation_id": context.conversation_id,
"role": "assistant",
"content": assistant_message["content"],
"content_tokens": output_tokens,
"created_at": datetime.now(timezone.utc)
}
# INSERT en base...
# Ajouter la réponse au contexte
context.messages.append(
Message(role="assistant", content=assistant_message["content"])
)
return {
"content": assistant_message["content"],
"model": context.model,
"usage": {
"input_tokens": input_tokens,
"output_tokens": output_tokens,
"total_tokens": input_tokens + output_tokens
},
"cost_usd": cost_usd,
"latency_ms": round(latency_ms, 2),
"context_saved": truncated_messages != context.messages[:-1]
}
Exemple d'utilisation
async def main():
client = HolySheepAIClient()
manager = ConversationManager(
database_url="postgresql://user:pass@localhost:5432/ai_conversations",
ai_client=client
)
context = ConversationContext(
user_id=uuid4(),
conversation_id=uuid4(),
system_prompt="Vous êtes un assistant IA helpful.",
model="deepseek-v3.2" # Modèle le plus économique: $0.42/MTok input
)
result = await manager.send_message(context, "Explique-moi la programmation asynchrone en Python")
print(f"Réponse: {result['content']}")
print(f"Coût: ${result['cost_usd']}")
print(f"Latence: {result['latency_ms']}ms")
print(f"Tokens: {result['usage']}")
if __name__ == "__main__":
import asyncio
asyncio.run(main())
Contrôle de Concurrence et Verrouillage Optimiste
La gestion simultanée de plusieurs conversations par utilisateur nécessite un contrôle de concurrence robuste. J'ai implémenté un système de verrouillage optimiste avec retry automatique.
#!/usr/bin/env python3
"""
Module de contrôle de concurrence pour conversations multi-utilisateurs.
Implémente le pattern Optimistic Locking avec PostgreSQL.
"""
import asyncio
import hashlib
import json
from datetime import datetime, timezone
from typing import Optional, Dict, Any, List
from dataclasses import dataclass
from uuid import UUID
import asyncpg
from contextlib import asynccontextmanager
@dataclass
class OptimisticLockError(Exception):
"""Exception levée lors d'un conflit de verrouillage optimiste."""
entity_type: str
entity_id: UUID
expected_version: int
actual_version: int
retry_count: int
@dataclass
class ConversationSnapshot:
"""Snapshot de conversation pour verrouillage optimiste."""
conversation_id: UUID
version: int
user_id: UUID
updated_at: datetime
messages_hash: str
pending_changes: List[Dict]
class OptimisticConcurrencyManager:
"""
Gestionnaire de concurrence optimiste pour conversations.
Implémente le pattern OCC (Optimistic Concurrency Control).
"""
def __init__(self, database_url: str):
self.database_url = database_url
self.pool: Optional[asyncpg.Pool] = None
self.max_retries = 3
self.base_delay = 0.1 # 100ms
async def initialize(self):
"""Initialise le pool de connexions."""
self.pool = await asyncpg.create_pool(
self.database_url,
min_size=10,
max_size=50,
command_timeout=60
)
async def close(self):
"""Ferme le pool de connexions."""
if self.pool:
await self.pool.close()
def _compute_messages_hash(
self,
messages: List[Dict[str, Any]]
) -> str:
"""Calcule un hash déterministe des messages."""
messages_normalized = []
for msg in messages:
normalized = {
"role": msg["role"],
"content": msg["content"],
"created_at": msg.get("created_at", "").isoformat() if msg.get("created_at") else ""
}
messages_normalized.append(normalized)
content = json.dumps(messages_normalized, sort_keys=True)
return hashlib.sha256(content.encode()).hexdigest()[:32]
@asynccontextmanager
async def conversation_lock(
self,
conversation_id: UUID,
expected_version: int,
user_id: UUID
):
"""
Contexte de verrouillage optimiste pour une conversation.
Utilise PostgreSQL Advisory Locks pour éviter les deadlocks.
"""
async with self.pool.acquire() as conn:
# Acquiert un advisory lock PostgreSQL
lock_key = int(conversation_id.int % (2**31))
await conn.execute(f"SELECT pg_advisory_xact_lock({lock_key})")
# Vérifie la version avec FOR UPDATE NOWAIT
row = await conn.fetchrow(
"""
SELECT version, messages_hash, updated_at
FROM conversations
WHERE conversation_id = $1 AND user_id = $2
FOR UPDATE NOWAIT
""",
conversation_id,
user_id
)
if not row:
raise ValueError(f"Conversation {conversation_id} non trouvée")
if row['version'] != expected_version:
raise OptimisticLockError(
entity_type="conversation",
entity_id=conversation_id,
expected_version=expected_version,
actual_version=row['version'],
retry_count=0
)
try:
yield ConversationSnapshot(
conversation_id=conversation_id,
version=row['version'],
user_id=user_id,
updated_at=row['updated_at'],
messages_hash=row['messages_hash'],
pending_changes=[]
)
except Exception as e:
await conn.execute("ROLLBACK")
raise
async def update_conversation_with_retry(
self,
conversation_id: UUID,
user_id: UUID,
new_messages: List[Dict[str, Any]],
base_version: int
) -> Dict[str, Any]:
"""
Met à jour une conversation avec retry automatique en cas de conflit.
Implémente le pattern Exponential Backoff.
"""
messages_hash = self._compute_messages_hash(new_messages)
attempt = 0
while attempt < self.max_retries:
try:
async with self.conversation_lock(
conversation_id,
base_version + attempt,
user_id
) as snapshot:
async with self.pool.acquire() as conn:
new_version = base_version + attempt + 1
await conn.execute(
"""
UPDATE conversations
SET
messages_hash = $1,
updated_at = NOW(),
version = $2,
last_message_at = NOW()
WHERE conversation_id = $3
""",
messages_hash,
new_version,
conversation_id
)
# Insertion des nouveaux messages
for idx, msg in enumerate(new_messages):
await conn.execute(
"""
INSERT INTO messages (
message_id, conversation_id, role,
content, content_tokens, message_index,
created_at, parent_message_id
) VALUES (
gen_random_uuid(), $1, $2, $3, $4, $5, NOW(), $6
)
""",
conversation_id,
msg["role"],
msg["content"],
msg.get("content_tokens", 0),
idx,
msg.get("parent_message_id")
)
return {
"success": True,
"new_version": new_version,
"messages_hash": messages_hash,
"attempts": attempt + 1
}
except OptimisticLockError as e:
attempt += 1
if attempt >= self.max_retries:
raise
# Exponential backoff avec jitter
delay = self.base_delay * (2 ** attempt) + (0.1 * attempt)
await asyncio.sleep(delay)
except Exception as e:
raise
raise OptimisticLockError(
entity_type="conversation",
entity_id=conversation_id,
expected_version=base_version,
actual_version=-1,
retry_count=self.max_retries
)
async def get_conversation_safe(
self,
conversation_id: UUID,
user_id: UUID
) -> Optional[Dict[str, Any]]:
"""
Récupère une conversation de manière thread-safe.
Utilise une transaction READ COMMITTED.
"""
async with self.pool.acquire() as conn:
async with conn.transaction():
conversation = await conn.fetchrow(
"""
SELECT c.*,
COALESCE(
json_agg(
json_build_object(
'message_id', m.message_id,
'role', m.role,
'content', m.content,
'created_at', m.created_at,
'content_tokens', m.content_tokens
) ORDER BY m.message_index
) FILTER (WHERE m.message_id IS NOT NULL),
'[]'::json
) as messages
FROM conversations c
LEFT JOIN messages m ON c.conversation_id = m.conversation_id
WHERE c.conversation_id = $1 AND c.user_id = $2
GROUP BY c.conversation_id
""",
conversation_id,
user_id
)
if not conversation:
return None
return {
"conversation_id": str(conversation["conversation_id"]),
"version": conversation["version"],
"messages_hash": conversation["messages_hash"],
"messages": conversation["messages"],
"updated_at": conversation["updated_at"].isoformat(),
"is_archived": conversation["is_archived"]
}
Exemple d'utilisation concurrente
async def concurrent_user_scenario():
"""Simule deux utilisateurs modifiant des conversations simultanément."""
manager = OptimisticConcurrencyManager("postgresql://user:pass@localhost/ai_db")
await manager.initialize()
conv_id = UUID("123e4567-e89b-12d3-a456-426614174000")
user_id = UUID("987fcdeb-51a2-3bc4-d567-890123456789")
# Scénario concurrent
async def user_action_1():
messages = [
{"role": "user", "content": "Premier message"},
{"role": "assistant", "content": "Réponse initiale"}
]
return await manager.update_conversation_with_retry(
conv_id, user_id, messages, base_version=1
)
async def user_action_2():
await asyncio.sleep(0.05) # Petit délai pour créer un conflit potentiel
messages = [
{"role": "user", "content": "Message concurrent"}
]
return await manager.update_conversation_with_retry(
conv_id, user_id, messages, base_version=1
)
try:
results = await asyncio.gather(
user_action_1(),
user_action_2(),
return_exceptions=True
)
for i, result in enumerate(results):
if isinstance(result, Exception):
print(f"Utilisateur {i+1}: Échec - {result}")
else:
print(f"Utilisateur {i+1}: Succès - Version {result['new_version']}")
finally:
await manager.close()
if __name__ == "__main__":
asyncio.run(concurrent_user_scenario())
Optimisation des Coûts et Choix du Modèle
La réduction des coûts est cruciale en production. Voici mon framework d'optimisation basé sur 18 mois d'utilisation HolySheep. Avec des tarifs comme $0.42/MTok pour DeepSeek V3.2 contre $8/MTok pour GPT-4.1, le choix du modèle impacte directement la rentabilité.
#!/usr/bin/env python3
"""
Module d'optimisation des coûts pour appels IA.
Implémente le routing intelligent et la mise en cache.
"""
import hashlib
import json
import time
from typing import Dict, List, Optional, Any, Tuple
from dataclasses import dataclass, field
from enum import Enum
from collections import OrderedDict
from datetime import datetime, timedelta
import asyncio
class ModelTier(Enum):
"""Tiers de modèles par coût croissant."""
BUDGET = "deepseek-v3.2"
STANDARD = "gemini-2.5-flash"
PREMIUM = "gpt-4.1"
ENTERPRISE = "claude-sonnet-4.5"
class TaskComplexity(Enum):
"""Classification de complexité des tâches."""
TRIVIAL = 1 # Salutations, confirmations simples
LOW = 2 # FAQ, reformulation
MEDIUM = 3 # Analyse, résumé, traduction
HIGH = 4 # Code complexe, raisonnement profond
EXPERT = 5 # Tasks spécialisée, debugging
@dataclass
class CostStats:
"""Statistiques de coûts agrégées."""
total_requests: int = 0
total_input_tokens: int = 0
total_output_tokens: int = 0
total_cost_usd: float = 0.0
model_breakdown: Dict[str, Dict[str, int]] = field(default_factory=dict)
cache_hits: int = 0
cache_misses: int = 0
avg_latency_ms: float = 0.0
class IntelligentModelRouter:
"""
Routeur intelligent qui sélectionne le modèle optimal
selon la complexité de la tâche et le budget disponible.
"""
def __init__(self, budget_limit_usd: float = 100.0):
self.budget_limit = budget_limit_usd
self.spent_today = 0.0
self.reset_date = datetime.now().date()
self.stats = CostStats()
self.cache = OrderedCache(max_size=10000, ttl_seconds=3600)
def _classify_task(self, messages: List[Dict[str, str]]) -> TaskComplexity:
"""
Classification automatique de la complexité de la tâche.
Utilise des heuristiques basées sur le contenu et la longueur.
"""
if not messages:
return TaskComplexity.LOW
last_user_msg = ""
for msg in reversed(messages):
if msg["role"] == "user":
last_user_msg = msg["content"].lower()
break
# Mots-clés indicateurs de complexité
complexity_indicators = {
TaskComplexity.EXPERT: [
"debug", "architectur", "optimiz", "algorithm", "benchmark",
"performance", "concurrent", "distributed", "machine learning",
"neural network", "deployment", "production"
],
TaskComplexity.HIGH: [
"code", "implement", "explain", "compare", "analyze",
"refactor", "review", "design pattern", "api", "database"
],
TaskComplexity.MEDIUM: [
"summarize", "translate", "rewrite", "convert", "transform",
"extract", "classify", "categorize"
],
TaskComplexity.LOW: [
"hello", "hi", "thanks", "thank you", "bye", "yes", "no",
"help", "what is", "how do i"
]
}
# Vérification des indicateurs dans le message
for complexity, keywords in complexity_indicators.items():
for keyword in keywords:
if keyword in last_user_msg:
# Score plus élevé pour les mots trouvés plus tôt dans la liste
return complexity
# Heuristique basée sur la longueur
total_length = sum(len(m["content"]) for m in messages)
if total_length < 50:
return TaskComplexity.TRIVIAL
elif total_length < 200:
return TaskComplexity.LOW
elif total_length < 1000:
return TaskComplexity.MEDIUM
return TaskComplexity.HIGH
def _get_cache_key(
self,
messages: List[Dict[str, str]],
model: str
) -> str:
"""Génère une clé de cache déterministe."""
content = json.dumps(messages, sort_keys=True) + model
return hashlib.sha256(content.encode()).hexdigest()
def _estimate_cost(
self,
model: str,
input_tokens: int,
output_tokens: int
) -> Tuple[float, float]:
"""Estime le coût et la latence pour un modèle."""
costs = {
"deepseek-v3.2": (0.42, 2.80, 50), # input, output, latency_ms
"gemini-2.5-flash": (2.50, 10.00, 45),
"gpt-4.1": (8.00, 24.00, 80),
"claude-sonnet-4.5": (15.00, 75.00, 120)
}
if model not in costs:
model = "deepseek-v3.2"
input_cost_per_m, output_cost_per_m, avg_lat = costs[model]
input_cost = (input_tokens / 1_000_000) * input_cost_per_m
output_cost = (output_tokens / 1_000_000) * output_cost_per_m
return input_cost + output_cost, avg_lat
def select_model(
self,
messages: List[Dict[str, str]],
preferred_tier: Optional[ModelTier] = None,
force_premium: bool = False
) -> Tuple[str, float, float]:
"""
Sélectionne le modèle optimal selon la tâche et le budget.
Retourne (model_name, estimated_cost, estimated_latency).
"""
# Vérifie le budget quotidien
today = datetime.now().date()
if today > self.reset_date:
self.spent_today = 0.0
self.reset_date = today
remaining_budget = self.budget_limit - self.spent_today
# Classification de la tâche
complexity = self._classify_task(messages)
# Mapping complexité vers tier de modèle
complexity_to_tier = {
TaskComplexity.TRIVIAL: ModelTier.BUDGET,
TaskComplexity.LOW: ModelTier.BUDGET,
TaskComplexity.MEDIUM: ModelTier.STANDARD,
TaskComplexity.HIGH: ModelTier.PREMIUM,
TaskComplexity.EXPERT: ModelTier.ENTERPRISE
}
# Détermine le tier optimal
if force_premium or preferred_tier:
optimal_tier = preferred_tier or ModelTier.PREMIUM
else:
optimal_tier = complexity_to_tier.get(complexity, ModelTier.STANDARD)
# Sélectionne le modèle avec fallback selon budget
model = optimal_tier.value
estimated_cost, estimated_lat = self._estimate_cost(model, 1000, 500)
# Fallback vers modèle moins cher si budget insuffisant
if estimated_cost > remaining_budget * 0.1: # Plus de 10% du budget restant
if remaining_budget > 0.01:
model = ModelTier.BUDGET.value
estimated_cost, estimated_lat = self._estimate_cost(model, 1000, 500)
else:
raise ValueError(f"Budget épuisé. Reste: {remaining_budget:.4f} USD")
return model, estimated_cost, estimated_lat
def get_cached_response(
self,
messages: List[Dict[str, str]],
model: str
) -> Optional[Dict[str, Any]]:
"""Vérifie si une réponse existe en cache."""
cache_key = self._get_cache_key(messages, model)
cached = self.cache.get(cache_key)
if cached:
self.stats.cache_hits += 1
return cached["response"]
self.stats.cache_misses += 1
return None
def cache_response(
self,
messages: List[Dict[str, str]],
model: str,
response: Dict[str, Any],
cost_usd: float
):
"""Met en cache une réponse avec statistiques."""
cache_key = self._get_cache_key(messages, model)
self.cache.put(cache_key, {
"response": response,
"cost_usd": cost_usd,
"timestamp": datetime.now().isoformat()
})
# Mise à jour des statistiques
self.stats.total_cost_usd += cost_usd
self.spent_today += cost_usd
if model not in self.stats.model_breakdown:
self.stats.model_breakdown[model] = {
"requests": 0, "input_tokens": 0, "output_tokens": 0, "cost": 0.0
}
self.stats.model_breakdown[model]["requests"] += 1
self.stats.model_breakdown[model]["cost"] += cost_usd
class OrderedCache:
"""Cache LRU simple avec TTL."""
def __init__(self, max_size: int = 1000, ttl_seconds: int = 3600):
self.max_size = max_size
self.ttl = ttl_seconds
self._cache: OrderedDict = OrderedDict()
self._timestamps: Dict[str, datetime] = {}
def get(self, key: str) -> Optional[Any]:
if key not in self._cache:
return None
# Vérifie expiration
if (datetime.now() - self._timestamps[key]).total_seconds() > self.ttl:
self._evict(key)
return None
# Move to end (most recently used)
self._cache.move_to_end(key)
return self._cache[key]
def put(self, key: str, value: Any):
if key in self._cache:
self._cache.move_to_end(key)
else:
if len(self._cache) >= self.max_size:
# Evict least recently used
oldest_key = next(iter(self._cache))
self._evict(oldest_key)
self._cache[key] = value
self._timestamps[key] = datetime.now()
def _evict(self, key: str):
if key in self._cache:
del self._cache[key]
if key in self._timestamps:
del self._timestamps[key]
def get_stats(self) -> Dict[str, Any]:
return {
"size": len(self._cache),