from abc import ABC, abstractmethod from typing import Any import httpx from fastapi import HTTPException from app.core.config import Settings, get_settings class AIAdapter(ABC): """Interface for model provider adapters.""" provider_name: str @abstractmethod def ask(self, prompt: str, context: dict[str, Any] | None = None) -> dict[str, Any]: raise NotImplementedError class NoopAdapter(AIAdapter): """Deterministic adapter used when no model provider is configured.""" provider_name = "noop" def ask(self, prompt: str, context: dict[str, Any] | None = None) -> dict[str, Any]: return { "answer": ( "AI provider is not configured yet. This is a deterministic placeholder. " "Set MODEL_PROVIDER to openclaw_hermes, openclaw, hermes, or direct_llm " "after credentials are ready." ), "raw": {"prompt": prompt, "context": context or {}}, } class OpenClawAdapter(AIAdapter): """Adapter for an OpenClaw-compatible agent endpoint.""" provider_name = "openclaw" def __init__(self, settings: Settings): self.settings = settings def ask(self, prompt: str, context: dict[str, Any] | None = None) -> dict[str, Any]: url = f"{self.settings.openclaw_base_url.rstrip('/')}/api/v1/agent/ask" headers = {} if self.settings.openclaw_api_key: headers["Authorization"] = f"Bearer {self.settings.openclaw_api_key}" payload = {"prompt": prompt, "context": context or {}} with httpx.Client(timeout=60) as client: response = client.post(url, json=payload, headers=headers) if response.status_code >= 400: raise HTTPException(status_code=502, detail={"openclaw_error": response.text}) data = response.json() return {"answer": data.get("answer") or data.get("content") or str(data), "raw": data} class HermesAdapter(AIAdapter): """Adapter for a Hermes-compatible memory or agent endpoint.""" provider_name = "hermes" def __init__(self, settings: Settings): self.settings = settings def ask(self, prompt: str, context: dict[str, Any] | None = None) -> dict[str, Any]: url = f"{self.settings.hermes_base_url.rstrip('/')}/api/v1/ask" headers = {} if self.settings.hermes_api_key: headers["Authorization"] = f"Bearer {self.settings.hermes_api_key}" payload = {"prompt": prompt, "context": context or {}} with httpx.Client(timeout=60) as client: response = client.post(url, json=payload, headers=headers) if response.status_code >= 400: raise HTTPException(status_code=502, detail={"hermes_error": response.text}) data = response.json() return {"answer": data.get("answer") or data.get("content") or str(data), "raw": data} class OpenClawHermesAdapter(AIAdapter): """Compose Hermes memory with OpenClaw execution.""" provider_name = "openclaw_hermes" def __init__(self, settings: Settings): self.openclaw = OpenClawAdapter(settings) self.hermes = HermesAdapter(settings) def ask(self, prompt: str, context: dict[str, Any] | None = None) -> dict[str, Any]: base_context = context or {} recall = self._recall_memory(prompt, base_context) openclaw_context = { **base_context, "agent_pipeline": self.provider_name, "hermes_memory": recall["answer"], } openclaw_result = self.openclaw.ask(prompt, openclaw_context) remember = self._remember_interaction(prompt, base_context, openclaw_result["answer"]) return { "answer": openclaw_result["answer"], "raw": { "pipeline": "hermes_recall -> openclaw_answer -> hermes_remember", "hermes_recall": recall, "openclaw": openclaw_result.get("raw", {}), "hermes_remember": remember, }, } def _recall_memory(self, prompt: str, context: dict[str, Any]) -> dict[str, Any]: recall_prompt = ( "Retrieve concise long-term memory, preferences, prior decisions, and relevant " "business context for this request. Return only information useful to answer it." ) try: result = self.hermes.ask( recall_prompt, { "mode": "memory_recall", "user_prompt": prompt, "request_context": context, }, ) except Exception as exc: # Hermes memory should not block OpenClaw execution. return {"answer": "", "raw": {}, "error": _error_detail(exc)} return {"answer": result["answer"], "raw": result.get("raw", {})} def _remember_interaction( self, prompt: str, context: dict[str, Any], answer: str, ) -> dict[str, Any]: remember_prompt = ( "Store durable lessons from this interaction for future company management " "assistance. Ignore transient details and do not store secrets." ) try: result = self.hermes.ask( remember_prompt, { "mode": "memory_write", "user_prompt": prompt, "request_context": context, "assistant_answer": answer, }, ) except Exception as exc: return {"ok": False, "raw": {}, "error": _error_detail(exc)} return {"ok": True, "raw": result.get("raw", {}), "answer": result["answer"]} class DirectLLMAdapter(AIAdapter): """Adapter for OpenAI-compatible chat completions APIs.""" provider_name = "direct_llm" def __init__(self, settings: Settings): self.settings = settings def ask(self, prompt: str, context: dict[str, Any] | None = None) -> dict[str, Any]: if not self.settings.direct_llm_api_key: raise HTTPException(status_code=503, detail="DIRECT_LLM_API_KEY is not configured") url = f"{self.settings.direct_llm_base_url.rstrip('/')}/chat/completions" headers = {"Authorization": f"Bearer {self.settings.direct_llm_api_key}"} messages = [ { "role": "system", "content": ( "You are a company management AI. Be concise, cite data from context, " "and never approve payments, performance changes, or trades automatically." ), }, {"role": "user", "content": f"Context:\n{context or {}}\n\nTask:\n{prompt}"}, ] payload = {"model": self.settings.direct_llm_model, "messages": messages} with httpx.Client(timeout=60) as client: response = client.post(url, json=payload, headers=headers) if response.status_code >= 400: raise HTTPException(status_code=502, detail={"llm_error": response.text}) data = response.json() answer = data["choices"][0]["message"]["content"] return {"answer": answer, "raw": data} def get_adapter() -> AIAdapter: """Return the configured AI provider adapter.""" settings = get_settings() provider = settings.model_provider.lower() if provider == "openclaw": return OpenClawAdapter(settings) if provider == "hermes": return HermesAdapter(settings) if provider in {"openclaw_hermes", "openclaw-hermes", "hybrid"}: return OpenClawHermesAdapter(settings) if provider == "direct_llm": return DirectLLMAdapter(settings) return NoopAdapter() def _error_detail(exc: Exception) -> Any: if isinstance(exc, HTTPException): return exc.detail return {"type": type(exc).__name__, "message": str(exc)}