118 lines
4.0 KiB
Python
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,
|
||
|
|
}
|