Initial commit
This commit is contained in:
196
hsbg_ai/backend/ai/engine/decision_engine.py
Normal file
196
hsbg_ai/backend/ai/engine/decision_engine.py
Normal file
@@ -0,0 +1,196 @@
|
||||
"""
|
||||
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()
|
||||
Reference in New Issue
Block a user