Files
hsbg-ai/hsbg_ai/backend/ai/engine/decision_engine.py

197 lines
6.7 KiB
Python
Raw Normal View History

2026-03-31 13:10:46 +02:00
"""
Moteur de décision IA principal - Architecture hybride.
Flux: StateAnalyzer HeuristicEngine + LLMAdvisor Fusion FullAdvice
"""
import time
from dataclasses import dataclass, field
import structlog
log = structlog.get_logger()
@dataclass
class GameState:
"""État complet d'un tour HSBG."""
turn: int = 0
tavern_tier: int = 1
gold: int = 3
hero_id: str = ""
hero_hp: int = 40
tavern_minions: list = field(default_factory=list)
board_minions: list = field(default_factory=list)
hand_minions: list = field(default_factory=list)
freeze: bool = False
can_upgrade: bool = True
upgrade_cost: int = 5
available_spells: list = field(default_factory=list)
opponent_boards: list = field(default_factory=list)
current_placement: int = 5
player_count: int = 8
phase: str = "recruit"
@dataclass
class Decision:
"""Une décision IA avec justification."""
action: str # buy|sell|freeze|upgrade|reposition|hero_power|wait
target: dict = None # Carte ou minion ciblé
priority: int = 5 # 1-10
confidence: float = 0.5 # 0.0-1.0
reasoning: str = "" # Explication textuelle
alternatives: list = field(default_factory=list)
synergies_highlighted: list = field(default_factory=list)
warnings: list = field(default_factory=list)
@dataclass
class FullAdvice:
"""Conseil complet pour un tour entier."""
main_decision: Decision
secondary_decisions: list = field(default_factory=list)
board_analysis: str = ""
strategy_long_term: str = ""
threat_assessment: str = ""
processing_ms: int = 0
model_used: str = "heuristic"
confidence_overall: float = 0.5
class DecisionEngine:
"""
Moteur de décision hybride HSBG.
Architecture:
1. StateAnalyzer Parse l'état brut en GameState typé
2. HeuristicEngine Règles métier rapides et déterministes
3. LLMAdvisor Raisonnement LLM pour enrichir (si disponible)
4. Fusion Combine les deux avec pondération par confiance
"""
def __init__(self, settings):
self.settings = settings
self._initialized = False
# Import lazy pour éviter les imports circulaires
from backend.ai.engine.heuristics import HeuristicEngine
from backend.ai.engine.llm_advisor import LLMAdvisor
from backend.ai.engine.state_analyzer import StateAnalyzer
self.heuristic = HeuristicEngine()
self.llm = LLMAdvisor(settings)
self.analyzer = StateAnalyzer()
async def initialize(self):
"""Initialise les composants asynchrones (LLM)."""
await self.llm.initialize()
self._initialized = True
log.info("decision_engine.ready", model=self.settings.llm_model)
async def get_advice(self, raw_state: dict) -> FullAdvice:
"""
Point d'entrée principal.
Args:
raw_state: Dict brut (depuis API ou OCR)
Returns:
FullAdvice avec conseil principal + alternatives
"""
start = time.perf_counter()
# 1. Parser l'état brut
state = await self.analyzer.parse(raw_state)
# 2. Heuristiques (toujours disponibles, < 5ms)
heuristic_decisions = self.heuristic.evaluate(state)
# 3. LLM (si disponible, ~500-2000ms)
llm_advice = None
if self._initialized:
try:
llm_advice = await self.llm.advise(state, heuristic_decisions)
except Exception as e:
log.warning("llm.failed_gracefully", error=str(e))
# 4. Fusion avec pondération
final = self._fuse(state, heuristic_decisions, llm_advice)
final.processing_ms = int((time.perf_counter() - start) * 1000)
log.info(
"advice.generated",
action=final.main_decision.action,
confidence=round(final.main_decision.confidence, 2),
ms=final.processing_ms,
model=final.model_used,
)
return final
def _fuse(self, state: GameState, heuristics: list, llm: "FullAdvice | None") -> FullAdvice:
"""
Fusionne heuristiques + LLM.
Règle: LLM prioritaire si confiance > 0.7, sinon heuristiques.
"""
if not heuristics:
return FullAdvice(
main_decision=Decision(action="wait", reasoning="Aucune action identifiée"),
model_used="fallback",
)
# LLM haute confiance → priorité totale
if llm and llm.confidence_overall > 0.7:
llm.model_used = f"{self.settings.llm_model}+heuristic"
return llm
# Heuristiques comme base
main = max(heuristics, key=lambda d: d.priority * d.confidence)
secondary = sorted(
[d for d in heuristics if d != main],
key=lambda d: d.priority,
reverse=True,
)[:3]
result = FullAdvice(
main_decision=main,
secondary_decisions=secondary,
board_analysis=self._analyze_board(state),
model_used="heuristic",
confidence_overall=main.confidence,
)
# Enrichissement LLM partiel (stratégie + menaces même si confiance faible)
if llm:
result.strategy_long_term = llm.strategy_long_term
result.threat_assessment = llm.threat_assessment
result.model_used = f"heuristic+{self.settings.llm_model}"
return result
def _analyze_board(self, state: GameState) -> str:
"""Génère une analyse textuelle rapide du board."""
parts = []
if len(state.board_minions) == 7:
parts.append("⚠️ Board plein — vendre avant d'acheter")
if state.gold >= 10 and state.tavern_tier < 6:
parts.append(f"💰 Or abondant — envisager tier {state.tavern_tier + 1}")
if state.current_placement > 5 and state.turn > 8:
parts.append("🚨 Position critique — trouver synergie forte rapidement")
# Détecter synergie dominante
from collections import Counter
races = []
for m in state.board_minions:
r = m.get("race", [])
races.extend(r if isinstance(r, list) else [r])
if races:
top_race, top_count = Counter(races).most_common(1)[0]
if top_count >= 3:
parts.append(f"✨ Synergie {top_race} détectée ({top_count}/7)")
if state.freeze:
parts.append("❄️ Taverne gelée — les cartes sont réservées")
return " | ".join(parts) if parts else "Board standard — continuer normalement"
async def shutdown(self):
"""Arrête proprement les composants."""
await self.llm.shutdown()