from __future__ import annotations import logging logger = logging.getLogger(__name__) # Default capability descriptions used when the Odoo DB registry has no entry # for an agent. These drive the master-agent's intent classification, so they # must be specific enough for the LLM to pick the right agent. _DEFAULT_CAPABILITIES = { 'expenses_agent': 'Employee expense reports and receipts — create, submit, and approve hr.expense records', 'finance_agent': 'Financial reporting — P&L, balance sheet, cash flow, budgets', 'accounting_agent': 'Accounting — journal entries, invoices, vendor bills, payments, reconciliation', 'crm_agent': 'CRM — leads, opportunities, pipeline stages, customer interactions', 'sales_agent': 'Sales — orders, quotations, customers, pricelists', 'project_agent': 'Projects — tasks, timesheets, milestones, deadlines', 'elearning_agent': 'eLearning — courses, slides, attendees, certifications', 'employees_agent': 'HR — employees, contracts, attendance, leave requests', 'odoo_doc_agent': 'Odoo 18 documentation and workflow guidance (internal use)', 'sysops_agent': 'System operations — Docker containers, git, logs, auto-healing', } class AgentRegistry: def __init__(self): self._agents: dict = {} # agent_key -> BaseAgent instance self._active: set = set() self._capabilities: dict = {} async def load_from_odoo(self, odoo_client): try: rows = await odoo_client.search_read( 'ab.ai.agent.registry', [['active', '=', True]], ['agent_name', 'domain', 'backend']) self._active = {r['agent_name'] for r in rows} # Use the DB domain field when present, fall back to defaults for r in rows: key = r['agent_name'] self._capabilities[key] = r.get('domain') or _DEFAULT_CAPABILITIES.get(key, key) logger.info('AgentRegistry loaded %d agents from DB: %s', len(self._active), list(self._active)) except Exception as exc: logger.error('AgentRegistry.load_from_odoo failed: %s', exc) def register(self, agent_key, agent_instance): self._agents[agent_key] = agent_instance # Auto-activate every registered agent so it appears in the master # prompt even when the Odoo DB registry table has no row for it. self._active.add(agent_key) if not self._capabilities.get(agent_key): self._capabilities[agent_key] = _DEFAULT_CAPABILITIES.get( agent_key, agent_key.replace('_agent', '').replace('_', ' ') + ' management', ) logger.debug('AgentRegistry: registered %s (active=%d)', agent_key, len(self._active)) async def get_active_agents(self): return [ {'agent_key': k, 'capabilities_summary': self._capabilities.get(k, '')} for k in self._active if k in self._agents # only expose agents that have a live instance ] async def get_all(self): return [ { 'name': k, 'agent_key': k, 'domain': self._capabilities.get(k, ''), 'active': k in self._active, 'has_instance': k in self._agents, } for k in self._agents ] async def is_active(self, agent_key): return agent_key in self._active async def sync(self, active_keys): self._active = set(active_keys) logger.info('AgentRegistry synced: active=%s', active_keys) def get_agent_instance(self, agent_key): return self._agents.get(agent_key)