"""Conseiller LLM local via Ollama — enrichit les décisions heuristiques.""" import json import httpx import structlog from backend.ai.engine.decision_engine import GameState, Decision, FullAdvice log = structlog.get_logger() SYSTEM_PROMPT = """Tu es un expert Hearthstone Battlegrounds rang Légende. Analyse l'état de jeu et donne un conseil tactique optimal. Réponds UNIQUEMENT avec un objet JSON valide, sans texte avant/après, sans markdown: {"main_action":"buy|sell|freeze|upgrade|reposition|hero_power|wait","target_card":null,"priority":7,"confidence":0.8,"reasoning":"explication courte","strategy":"stratégie long terme 1 phrase","threats":"principale menace adversaire","warnings":[]}""" class LLMAdvisor: """Interface avec Ollama pour les conseils LLM.""" def __init__(self, settings): self.settings = settings self.base_url = settings.llm_base_url self.model = settings.llm_model self._client: httpx.AsyncClient | None = None self._available = False async def initialize(self): """Teste la disponibilité d'Ollama et du modèle.""" self._client = httpx.AsyncClient(timeout=10) try: r = await self._client.get(f"{self.base_url}/api/tags") if r.status_code == 200: models = r.json().get("models", []) model_names = [m.get("name", "") for m in models] self._available = any(self.model in name for name in model_names) if self._available: log.info("llm.ready", model=self.model) else: log.warning("llm.model_not_found", model=self.model, available=model_names, hint=f"Exécutez: ollama pull {self.model}") except Exception as e: log.warning("llm.ollama_unreachable", url=self.base_url, error=str(e), hint="Installez Ollama: curl -fsSL https://ollama.ai/install.sh | sh") self._available = False async def advise(self, state: GameState, heuristics: list[Decision]) -> FullAdvice | None: """Demande un conseil au LLM local.""" if not self._available or not self._client: return None prompt = self._build_prompt(state, heuristics) try: r = await self._client.post( f"{self.base_url}/api/generate", json={ "model": self.model, "prompt": prompt, "system": SYSTEM_PROMPT, "stream": False, "options": { "temperature": self.settings.llm_temperature, "num_predict": self.settings.llm_max_tokens, }, }, timeout=self.settings.llm_timeout, ) if r.status_code == 200: raw = r.json().get("response", "") return self._parse(raw) except Exception as e: log.warning("llm.request_failed", error=str(e)) return None def _build_prompt(self, state: GameState, decisions: list[Decision]) -> str: """Construit le prompt depuis l'état de jeu.""" board = ", ".join( f"{m.get('name','?')}({m.get('attack',0)}/{m.get('health',0)})" + (" [DIVINE]" if m.get("has_divine") else "") + (" [TAUNT]" if m.get("has_taunt") else "") for m in state.board_minions ) or "vide" tavern = ", ".join( f"{m.get('name','?')}[{m.get('cost',3)}g T{m.get('tier',1)}]" for m in state.tavern_minions ) or "vide" top_h = decisions[0].reasoning if decisions else "aucune heuristique" return f"""=== ÉTAT DU JEU === Tour: {state.turn} | Tier taverne: {state.tavern_tier} | Or: {state.gold}g Héros: {state.hero_id} | HP: {state.hero_hp} | Position: {state.current_placement}/{state.player_count} Board ({len(state.board_minions)}/7): {board} Taverne: {tavern} Gel: {'OUI' if state.freeze else 'NON'} | Upgrade possible: {'OUI' if state.can_upgrade else 'NON'} ({state.upgrade_cost}g) Phase: {state.phase} === MEILLEURE HEURISTIQUE === {top_h} Analyse et donne ta recommandation JSON.""" def _parse(self, raw: str) -> FullAdvice | None: """Parse la réponse JSON du LLM.""" try: start = raw.find("{") end = raw.rfind("}") + 1 if start == -1 or end == 0: return None data = json.loads(raw[start:end]) main = Decision( action=data.get("main_action", "wait"), priority=min(10, max(1, int(data.get("priority", 5)))), confidence=min(1.0, max(0.0, float(data.get("confidence", 0.5)))), reasoning=data.get("reasoning", ""), warnings=data.get("warnings", []) or [], ) return FullAdvice( main_decision=main, strategy_long_term=data.get("strategy", ""), threat_assessment=data.get("threats", ""), confidence_overall=main.confidence, model_used=self.model, ) except (json.JSONDecodeError, ValueError, TypeError) as e: log.warning("llm.parse_failed", error=str(e), raw_preview=raw[:300]) return None async def shutdown(self): if self._client: await self._client.aclose()