Initial commit

This commit is contained in:
2026-03-31 13:10:46 +02:00
commit f60d9628e0
52 changed files with 3383 additions and 0 deletions

View File

View File

@@ -0,0 +1,117 @@
"""Système d'apprentissage par feedback utilisateur."""
import json
import os
from datetime import datetime
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
from backend.database.models import AIDecision, LearningFeedback
import structlog
log = structlog.get_logger()
class FeedbackProcessor:
"""
Traite les retours utilisateur pour améliorer l'IA.
Workflow:
1. Utilisateur évalue une décision (bon/mauvais/neutre)
2. Le feedback est persisté en DB
3. Un buffer accumule les feedbacks
4. Quand le buffer est plein → export JSON pour entraînement futur
"""
def __init__(self, settings):
self.settings = settings
self._buffer: list[dict] = []
self._trained_count = 0
async def record_feedback(
self,
db: AsyncSession,
decision_id: int,
rating: str,
better_action: dict | None = None,
comment: str | None = None,
) -> LearningFeedback:
"""Enregistre un feedback et met à jour la décision associée."""
decision = await db.get(AIDecision, decision_id)
if not decision:
raise ValueError(f"Décision {decision_id} introuvable")
# Créer le feedback
fb = LearningFeedback(
decision_id=decision_id,
rating=rating,
better_action=better_action,
comment=comment,
)
db.add(fb)
# Mettre à jour la décision avec le résultat
decision.outcome_rating = {"good": 1, "neutral": 0, "bad": -1}.get(rating, 0)
decision.user_feedback = comment
if better_action:
decision.better_decision = better_action
await db.flush()
# Buffer pour entraînement
self._buffer.append({
"decision_id": decision_id,
"game_state": decision.game_state,
"recommendation": decision.recommendation,
"rating": rating,
"better_action": better_action,
"ts": datetime.utcnow().isoformat(),
})
log.info("feedback.recorded", id=decision_id, rating=rating,
buffer=len(self._buffer))
# Auto-flush si buffer plein
if (self.settings.learning_auto_save
and len(self._buffer) >= self.settings.learning_batch_size):
await self._flush_buffer()
return fb
async def _flush_buffer(self):
"""Exporte le buffer en JSON pour entraînement."""
if not self._buffer:
return
os.makedirs("data/learning/feedback", exist_ok=True)
fname = f"data/learning/feedback/batch_{datetime.utcnow().strftime('%Y%m%d_%H%M%S')}.json"
try:
import aiofiles
async with aiofiles.open(fname, "w") as f:
await f.write(json.dumps(self._buffer, indent=2, ensure_ascii=False))
self._trained_count += len(self._buffer)
log.info("feedback.batch_saved", count=len(self._buffer), file=fname)
except Exception as e:
log.error("feedback.flush_failed", error=str(e))
finally:
self._buffer.clear()
async def force_flush(self):
"""Flush manuel du buffer."""
await self._flush_buffer()
async def get_stats(self, db: AsyncSession) -> dict:
"""Statistiques globales du système d'apprentissage."""
result = await db.execute(select(LearningFeedback))
feedbacks = result.scalars().all()
total = len(feedbacks)
good = sum(1 for f in feedbacks if f.rating == "good")
bad = sum(1 for f in feedbacks if f.rating == "bad")
neutral = total - good - bad
return {
"total": total,
"good": good,
"bad": bad,
"neutral": neutral,
"good_rate": round(good / total * 100, 1) if total > 0 else 0.0,
"trained": self._trained_count,
"buffer_pending": len(self._buffer),
"learning_enabled": self.settings.learning_enabled,
}