From e215a26c585886799818f13ddeb18c5ff3dac56d Mon Sep 17 00:00:00 2001 From: Carlos Garcia Date: Thu, 14 May 2026 22:53:22 -0400 Subject: [PATCH] feat: register OdooDocAgent as PeerBus specialist agent Wraps odootrain RAG API (http://192.168.2.9:8000) as a BaseAgent so any specialist agent can query Odoo 18 docs mid-execution via PeerBus request_type=query_docs. Participates in sweep health checks. Co-Authored-By: Claude Sonnet 4.6 --- agent_service/agents/odoo_doc_agent.py | 128 +++++++++++++++++++++++++ agent_service/main.py | 7 ++ 2 files changed, 135 insertions(+) create mode 100644 agent_service/agents/odoo_doc_agent.py diff --git a/agent_service/agents/odoo_doc_agent.py b/agent_service/agents/odoo_doc_agent.py new file mode 100644 index 0000000..8d46139 --- /dev/null +++ b/agent_service/agents/odoo_doc_agent.py @@ -0,0 +1,128 @@ +from __future__ import annotations +import logging +import httpx +from .base_agent import BaseAgent, AgentReport, SweepReport + +logger = logging.getLogger(__name__) + +RAG_URL = "http://192.168.2.9:8000" +RAG_TIMEOUT = 60 + + +class OdooDocAgent(BaseAgent): + """ + Read-only knowledge agent backed by the odootrain RAG stack. + Answers questions about Odoo 18 workflows using the indexed documentation. + + Other agents query it via PeerBus: + response = await self._peer_bus.request( + from_agent=self.name, + to_agent='odoo_doc_agent', + request_type='query_docs', + params={'question': '...', 'module': 'accounting'}, # module optional + reason='Need Odoo workflow guidance', + ) + guidance = response.data.get('answer', '') + """ + + name = 'odoo_doc_agent' + domain = 'documentation' + required_odoo_module = 'base' + system_prompt_file = '' + tools = [] + + async def _plan(self) -> dict: + return { + 'question': self._directive.task, + 'module': self._directive.params.get('module'), + 'top_k': self._directive.params.get('top_k', 6), + } + + async def _gather(self, plan: dict) -> None: + payload = { + 'question': plan['question'], + 'top_k': plan['top_k'], + } + if plan.get('module'): + payload['module'] = plan['module'] + + try: + async with httpx.AsyncClient(timeout=RAG_TIMEOUT) as client: + resp = await client.post(f"{RAG_URL}/ask", json=payload) + resp.raise_for_status() + self._gathered = resp.json() + except Exception as exc: + logger.error('odoo_doc_agent RAG call failed: %s', exc) + self._gathered = {'answer': '', 'sources': [], 'error': str(exc)} + + async def _reason(self) -> dict: + return {} + + async def _act(self, reasoning: dict) -> None: + pass + + async def _report(self) -> AgentReport: + answer = self._gathered.get('answer', '') + sources = self._gathered.get('sources', []) + error = self._gathered.get('error') + + status = 'failed' if error and not answer else 'complete' + summary = answer or f'RAG lookup failed: {error}' + + return AgentReport( + directive_id=self._directive.directive_id, + agent=self.name, + status=status, + summary=summary, + data={ + 'answer': answer, + 'sources': sources, + 'model': self._gathered.get('model', ''), + }, + error=error, + ) + + async def handle_peer_request(self, request_type: str, params: dict, directive_id: str) -> dict: + if request_type != 'query_docs': + return {'success': False, 'error': f'Unknown request type: {request_type}'} + + question = params.get('question', '') + if not question: + return {'success': False, 'error': 'question is required'} + + payload = {'question': question, 'top_k': params.get('top_k', 6)} + if params.get('module'): + payload['module'] = params['module'] + + try: + async with httpx.AsyncClient(timeout=RAG_TIMEOUT) as client: + resp = await client.post(f"{RAG_URL}/ask", json=payload) + resp.raise_for_status() + data = resp.json() + return { + 'success': True, + 'answer': data.get('answer', ''), + 'sources': data.get('sources', []), + } + except Exception as exc: + logger.error('odoo_doc_agent peer request failed: %s', exc) + return {'success': False, 'error': str(exc)} + + async def sweep(self) -> SweepReport: + try: + async with httpx.AsyncClient(timeout=10) as client: + resp = await client.get(f"{RAG_URL}/health") + health = resp.json() + qdrant_ok = 'ok' in str(health.get('qdrant', '')) + ollama_ok = 'ok' in str(health.get('ollama', '')) + findings = [] + if not qdrant_ok: + findings.append({'issue': 'Qdrant unhealthy', 'severity': 'high', 'detail': health}) + if not ollama_ok: + findings.append({'issue': 'Ollama unhealthy', 'severity': 'high', 'detail': health}) + return SweepReport(agent=self.name, findings=findings) + except Exception as exc: + return SweepReport( + agent=self.name, + findings=[{'issue': 'RAG API unreachable', 'severity': 'high', 'detail': str(exc)}], + ) diff --git a/agent_service/main.py b/agent_service/main.py index 13a3612..ab04fa2 100644 --- a/agent_service/main.py +++ b/agent_service/main.py @@ -155,6 +155,13 @@ async def lifespan(app: FastAPI): def _register_specialist_agents(agent_registry, peer_bus, odoo, llm_router) -> None: + try: + from .agents.odoo_doc_agent import OdooDocAgent + agent_registry.register('odoo_doc_agent', OdooDocAgent(odoo=odoo, llm=llm_router, peer_bus=peer_bus)) + logger.info('odoo_doc_agent registered (RAG @ http://192.168.2.9:8000)') + except Exception as exc: + logger.warning('Could not register odoo_doc_agent: %s', exc) + try: from .agents.finance_agent import FinanceAgent agent_registry.register('finance_agent', FinanceAgent(odoo=odoo, llm=llm_router, peer_bus=peer_bus))