""" HERMES — il cervello (ciclo agentico con tool-calling). E qui che Hermes "ragiona": riceve la conversazione, decide DA SOLO se usare uno strumento (ricerca web, leggere/aggiornare la dashboard, salvare in memoria), ne osserva il risultato, e continua finche non ha una risposta per Ayman. Usa le API LLM in cascata: DeepSeek (principale) -> Groq -> OpenAI (riserve), cosi se un provider e giu o a corto di credito, l'agente continua a funzionare. """ from __future__ import annotations import json import requests from config import LLM_PROVIDERS, LLM_TIMEOUT from skills import TOOLS, dispatch MAX_STEPS = 6 # quanti giri di ragionamento+strumenti al massimo per messaggio def _chat(provider: dict, messages: list, use_tools: bool) -> dict | None: """Una chiamata a un provider. Restituisce il messaggio dell'assistente o None.""" if not provider.get("api_key"): return None payload = { "model": provider["model"], "messages": messages, "temperature": 0.4, } if use_tools: payload["tools"] = TOOLS payload["tool_choice"] = "auto" try: r = requests.post( f"{provider['base_url']}/chat/completions", headers={"Authorization": f"Bearer {provider['api_key']}", "Content-Type": "application/json"}, json=payload, timeout=LLM_TIMEOUT, ) if r.status_code != 200: print(f"[brain] {provider['name']} HTTP {r.status_code}: {r.text[:160]}", flush=True) return None return r.json()["choices"][0]["message"] except Exception as e: print(f"[brain] {provider['name']} errore: {e}", flush=True) return None def _chiama_llm(messages: list, use_tools: bool = True) -> dict | None: """Prova i provider in cascata finche uno risponde.""" for provider in LLM_PROVIDERS: msg = _chat(provider, messages, use_tools) if msg is not None: return msg return None def pensa(messages: list) -> str: """Ciclo agentico completo. messages = [system, ...storia..., user].""" msgs = list(messages) for _ in range(MAX_STEPS): msg = _chiama_llm(msgs, use_tools=True) if msg is None: return "Scusa, ora non riesco a ragionare (cervello AI non raggiungibile). Riprova tra poco." tool_calls = msg.get("tool_calls") if not tool_calls: return (msg.get("content") or "").strip() or "(nessuna risposta)" # l'agente ha deciso di usare uno o piu strumenti msgs.append(msg) for tc in tool_calls: fn = tc.get("function", {}) nome = fn.get("name", "") try: argomenti = json.loads(fn.get("arguments") or "{}") except Exception: argomenti = {} print(f"[brain] strumento -> {nome}({argomenti})", flush=True) risultato = dispatch(nome, argomenti) msgs.append({ "role": "tool", "tool_call_id": tc.get("id", ""), "content": risultato, }) # raggiunto il limite di giri: forza una risposta senza altri strumenti finale = _chiama_llm(msgs, use_tools=False) return (finale.get("content") if finale else "") or "Ho fatto un po' di passaggi ma non sono arrivato a una risposta netta. Riformula?"