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

135 lines
5.4 KiB
Python
Raw Normal View History

2026-03-31 13:10:46 +02:00
"""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()