""" ActiveBlue AI Agent — Odoo 18 RAG Specialist ============================================= Drop-in specialist agent for the ActiveBlue AI system. Implements the PeerBus interface defined in ACTIVEBLUE_AI_SPEC.md. Usage: from api.odoo_rag_agent import OdooRagAgent agent = OdooRagAgent(rag_url="http://localhost:8000") result = await agent.ask("How do I run a payroll batch?") print(result["answer"]) """ import json import httpx import logging from typing import AsyncIterator log = logging.getLogger(__name__) class OdooRagAgent: name = "odoo18_rag" description = "Answers Odoo 18 questions using RAG over official documentation" capabilities = [ "odoo_how_to", "odoo_configuration", "odoo_troubleshooting", "odoo_workflow", ] privacy_mode = "local" # uses local Ollama — HIPAA safe def __init__( self, rag_url: str = "http://localhost:8000", timeout: int = 120, default_model: str | None = None, ): self.rag_url = rag_url.rstrip("/") self.timeout = timeout self.default_model = default_model async def ask( self, question: str, module: str | None = None, top_k: int = 6, temperature: float = 0.3, ) -> dict: payload = {"question": question, "top_k": top_k, "temperature": temperature} if module: payload["module"] = module if self.default_model: payload["model"] = self.default_model async with httpx.AsyncClient(timeout=self.timeout) as client: resp = await client.post(f"{self.rag_url}/ask", json=payload) resp.raise_for_status() return resp.json() async def ask_stream( self, question: str, module: str | None = None, top_k: int = 6, temperature: float = 0.3, ) -> AsyncIterator[str]: payload = {"question": question, "top_k": top_k, "temperature": temperature} if module: payload["module"] = module async with httpx.AsyncClient(timeout=self.timeout) as client: async with client.stream("POST", f"{self.rag_url}/ask/stream", json=payload) as resp: async for line in resp.aiter_lines(): if line.startswith("data: "): data_str = line[6:] if data_str == "[DONE]": break try: data = json.loads(data_str) if data.get("type") == "token": yield data["content"] elif data.get("type") == "sources": yield json.dumps(data) except json.JSONDecodeError: continue async def handle_peer_message(self, message: dict) -> dict: """PeerBus message handler for the ActiveBlue Master AI.""" action = message.get("action") payload = message.get("payload", {}) req_id = message.get("request_id") if action == "ask": result = await self.ask( question = payload.get("question", ""), module = payload.get("module"), top_k = payload.get("top_k", 6), temperature = payload.get("temperature", 0.3), ) return {"request_id": req_id, "agent": self.name, "status": "ok", "result": result} elif action == "capabilities": return { "request_id": req_id, "agent": self.name, "capabilities": self.capabilities, "description": self.description, "privacy_mode": self.privacy_mode, } elif action == "health": return await self.health() return {"request_id": req_id, "agent": self.name, "status": "error", "error": f"Unknown action: {action}"} async def health(self) -> dict: try: async with httpx.AsyncClient(timeout=5) as client: resp = await client.get(f"{self.rag_url}/health") return {"agent": self.name, "status": "ok", "rag": resp.json()} except Exception as e: return {"agent": self.name, "status": "error", "error": str(e)} # ── Module convenience wrappers ─────────────────────────────────────────── async def ask_accounting(self, question: str) -> dict: return await self.ask(question, module="accounting") async def ask_payroll(self, question: str) -> dict: return await self.ask(question, module="payroll") async def ask_inventory(self, question: str) -> dict: return await self.ask(question, module="inventory") async def ask_crm(self, question: str) -> dict: return await self.ask(question, module="crm") async def ask_hr(self, question: str) -> dict: return await self.ask(question, module="employees") async def ask_manufacturing(self, question: str) -> dict: return await self.ask(question, module="manufacturing") async def ask_helpdesk(self, question: str) -> dict: return await self.ask(question, module="helpdesk")