Files
hsbg-ai/hsbg_ai/backend/ai/learning/feedback_processor.py
2026-03-31 13:10:46 +02:00

118 lines
4.0 KiB
Python

"""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,
}