Files
odoo-ai/ARCHITECTURE.md
Carlos Garcia a0fc1396a9 fix: Odoo 18 field errors, routing quality, bot presence, and add architecture docs
- expenses_tools: remove 'date' from hr.expense.sheet field lists (Odoo 18
  uses accounting_date; querying 'date' raised ValueError at runtime)
- master_system.txt: add few-shot routing examples so Llama 3.1 8B correctly
  outputs agents=[] for general questions instead of defaulting to expenses_agent
- ab_ai_bot.py: increase bot presence last_poll offset from 90s to 10min so
  the green dot stays on between cron runs (cron fires every ~5min in practice,
  not every 20s as configured)
- ARCHITECTURE.md: full system documentation covering component layout, request
  flow, LLM routing, agent registry, access control, health/presence mechanism,
  known issues fixed today, and future self-healing concept

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 15:47:48 -04:00

12 KiB

ActiveBlue AI — System Architecture

Overview

ActiveBlue AI is a multi-agent system that adds a conversational AI assistant to the Active Blue Odoo 18 instance. Users chat with the bot through Odoo Discuss (direct message). The bot dispatches work to specialist agents that read and write Odoo data via XML-RPC, then synthesises their reports into a single reply.

User (Odoo Discuss)
       │ chat message / file upload
       ▼
ab_ai_mail.py  ←── Odoo addon (monkey-patches discuss.channel.message_post)
       │ daemon thread (non-blocking)
       ▼
Agent Service  (Docker: activeblue-agent, port 8001)
       │
       ├── MasterAgent  ←── intent router (LLM → JSON)
       │       │
       │       ├── expenses_agent
       │       ├── sales_agent
       │       ├── accounting_agent
       │       ├── finance_agent
       │       ├── crm_agent
       │       ├── project_agent
       │       ├── employees_agent
       │       └── odoo_doc_agent  ←── RAG (Qdrant @ 192.168.2.9:8000)
       │
       ├── LLM Router  ──► Ollama (192.168.2.9:11434)  activeblue-chat (Llama 3.1 8B)
       │                └► Claude (Anthropic API)       only when mode != local
       │
       ├── agent-db  (Postgres 15, Docker: activeblue-agent-db)
       │     ├── ab_directive_log    (every request lifecycle)
       │     ├── ab_conversation     (per-user message history)
       │     └── ab_llm_config       (runtime backend overrides)
       │
       └── Odoo XML-RPC  (http://odoo-web-1:8069)

Docker Layout

Container Image Role
odoo-web-1 odoo:18.0 Odoo application + ActiveBlue AI addon
odoo-db-1 postgres:16 Odoo database
activeblue-agent odoo-ai-agent-service FastAPI agent service
activeblue-agent-db postgres:15-alpine Agent conversation/directive DB

All containers share the activeblue-net Docker bridge network. DNS resolves by container name (e.g. odoo-web-1, activeblue-agent, agent-db).

Compose files:

  • /root/odoo/docker-compose.yml — Odoo + Postgres (network: external)
  • /root/odoo/odoo-ai/docker-compose.yml — Agent service + agent-db (network: owner)

Bot Online / Presence Flow

[Every ~5 min]  ir.cron "ActiveBlue AI: Ping Service"
                    │
                    ▼
             ab_ai_bot.cron_ping_all()
                    │
                    ├── GET http://activeblue-agent:8001/health/detailed
                    │       returns: {status, db, odoo, ollama, master_agent, privacy_mode}
                    │
                    ├── Evaluate: db==ok AND master_agent==ok AND ollama==ok (local mode)
                    │
                    ├── ab.ai.bot.status = 'online'  (or 'error'/'offline')
                    │
                    └── _sync_bot_user_presence(online=True)
                                │
                                ▼
                        bus.presence for user 'activeblue_ai_bot@local'
                        last_poll = now + 10min   ← keeps green dot alive
                        last_presence = now        between cron runs

Why 10 minutes? Odoo's cron scheduler fires the 20-second-configured job only every ~5 minutes in practice. Setting last_poll 10 minutes ahead ensures the green dot stays on between runs. If the agent service goes down, the next cron cycle sets last_poll an hour in the past, making the dot go grey within 30 seconds.

Health endpoint (/health/detailed) checks:

  • db — asyncpg pool can acquire a connection to agent-db
  • odoo — XML-RPC auth against Odoo succeeded at startup
  • ollama — GET /api/tags on the Ollama host returns 200
  • master_agent — MasterAgent instance is loaded and ready
  • privacy_mode — current LLM routing mode (local / hybrid / cloud)

Request Flow: Text Message

1. User sends message in Odoo Discuss (direct chat with ActiveBlue AI)

2. Odoo: discuss.channel.message_post() is called
   ab_ai_mail.DiscussChannel.message_post() intercepts it:
   - Ignores non-chat channels and the bot's own messages
   - Reads bot URL + secret from ab.ai.bot record
   - Launches _agent_thread() as a daemon thread (returns immediately)

3. _agent_thread() (background, outside Odoo HTTP request):
   POST http://activeblue-agent:8001/dispatch
   {user_id, message, context: {source: "discuss"}}
   Header: X-ActiveBlue-Signature: <webhook_secret>

4. Agent Service /dispatch router:
   - Validates HMAC signature
   - Creates directive_id (UUID)
   - Calls MasterAgent.handle_message()

5. MasterAgent.handle_message():
   a. Persists user message to ab_conversation (agent-db)
   b. Builds context: last 20 conversation turns + operational findings
   c. _classify_intent(): sends system prompt + history + message to LLM
      - LLM returns JSON: {agents: [...], intent_summary, params, needs_clarification}
      - caller='master' → format='json' forced on Ollama
   d. If agents=[]: _direct_answer() → LLM answers conversationally, done
   e. If needs_clarification: returns clarification question, done
   f. _check_access(): verifies user has required Odoo groups for each agent
   g. _dispatch_agents(): calls each agent's execute() concurrently
   h. _synthesize(): if >1 agent, merges reports via LLM
   i. Persists assistant reply to ab_conversation
   j. Returns MasterResponse {reply, status, actions_taken, escalations}

6. _agent_thread() receives response, calls _post_bot_reply()
   - Opens fresh DB cursor (new transaction)
   - env['discuss.channel'].message_post() with bot as author
   - User sees the reply

Request Flow: File Upload (Receipts)

1. User uploads file(s) in Odoo Discuss

2. ab_ai_mail intercepts:
   - Files only, no text → _post_file_clarification(): bot asks "what to do?"
   - Files + text (or text after pending upload) → _agent_thread() with att_data

3. _agent_thread() → POST /upload (multipart)
   Fields: user_id, message, session_id
   Files: one part per attachment

4. /upload router:
   - Calls receipt_parser.parse_upload() for each file
   - ZIPs are recursively unpacked
   - Images: vision OCR (Ollama llama3.2-vision) → fallback Tesseract
   - PDFs: pdfplumber text extraction
   - Returns parsed receipt list to MasterAgent as extra_context['receipts']

5. MasterAgent routes to expenses_agent (always, when receipts present)

6. ExpensesAgent._plan():
   - Checks for duplicates against existing Odoo expenses (fuzzy match)
   - If duplicates found: returns clarification asking user to confirm/skip
   - Otherwise: plans expense creation

7. ExpensesAgent._act():
   - create_expense_sheet() → hr.expense.sheet
   - create_expense() per receipt → hr.expense
   - attach_receipt() → ir.attachment on each hr.expense
   - Posts chatter note on the sheet

LLM Routing

Controlled by LLM_PRIVACY_MODE in .env:

Mode Master Specialist Agents Notes
local Ollama Ollama All traffic stays on-prem
hybrid Per AGENT_BACKEND_* env vars Per env vars or Ollama Mixed routing
cloud Claude Claude (except HIPAA-locked) Requires Anthropic credits

HIPAA-locked agents (always Ollama regardless of mode): finance_agent, accounting_agent, employees_agent, expenses_agent

Current production setting: LLM_PRIVACY_MODE=local

Models:

  • Chat/routing: activeblue-chat (Llama 3.1 8B Q8_0, no embedded system prompt)
  • Vision OCR: llama3.2-vision:11b
  • Claude fallback: claude-sonnet-4-6 (requires API credits)

Known limitation: Llama 3.1 8B sometimes misroutes general questions to expenses_agent. Mitigated with few-shot examples in master_system.txt. For reliable routing, switch to hybrid mode with AGENT_BACKEND_MASTER=claude once Anthropic credits are available.


Specialist Agents

Each agent follows the same lifecycle: execute()_plan()_act()AgentReport.

Agent Odoo Models Key Capabilities
expenses_agent hr.expense, hr.expense.sheet Receipt OCR → expense creation, approval, reporting
sales_agent sale.order, res.partner Pipeline, quotes, customer lookup
accounting_agent account.move, account.payment Invoices, journal entries, reconciliation
finance_agent account.* P&L, cash flow, budget analysis
crm_agent crm.lead Leads, opportunities, pipeline
project_agent project.project, project.task Tasks, deadlines, progress
employees_agent hr.employee, hr.leave Headcount, leave, HR queries
odoo_doc_agent RAG (Qdrant) Odoo documentation Q&A

elearning_agent — defined but not registered: exceeds the 8-tool limit per agent. Needs to be split into contextual tool groups before it can be used.


Access Control

Each agent maps to an Odoo security group. Before dispatching, MasterAgent checks that the requesting user belongs to the required group:

Agent Required Group
finance_agent / accounting_agent account.group_account_user
sales_agent sales_team.group_sale_salesman
project_agent project.group_project_user
expenses_agent hr_expense.group_hr_expense_user
employees_agent hr.group_hr_user

If denied, the bot replies with a "you don't have access" message and logs the denial.


Agent Memory

Per-user conversation history is stored in ab_conversation (agent-db Postgres). On each request, MasterAgent loads:

  • Last 20 message turns (for intent classification context)
  • Last 10 turns (for direct answers)
  • Stored operational findings from past agent runs (e.g. "user is employee #4")

This allows the bot to handle follow-up questions and maintain context across turns without the user repeating themselves.


Directive Logging

Every request is logged to ab_directive_log (agent-db):

  • directive_id (UUID) — correlates across logs
  • status: processingcomplete / partial / failed / awaiting_approval
  • raw_message, final_response, actions_taken, escalations, error

Cleanup cron removes entries older than 30 days.


Odoo Addon Components

File Role
ab_ai_mail.py Intercepts Discuss messages, launches agent thread
ab_ai_bot.py Bot config record, ping, presence sync, dispatch helpers
ab_ai_registry.py Mirrors agent registry from service into Odoo
ab_ai_directive.py Odoo-side directive/log records
health_proxy.py /ai/health endpoint proxies to agent service
webhook.py Receives async callbacks from agent service
approval.py Handles human-in-the-loop escalation approvals

Known Issues & Fixes (2026-05-19)

Issue Root Cause Fix
Agent service offline at startup Odoo not ready when agent starts (race condition) Restart agent after Odoo is up; both now share network
Vision OCR failed: 'dict' object has no attribute 'message' ollama-python newer versions return dict not object receipt_parser.py: hasattr(response, 'message') check
Invalid field 'date' on model 'hr.expense.sheet' Odoo 18 uses accounting_date, not date on sheets Removed 'date' from hr.expense.sheet field lists in expenses_tools.py
Bot routes all messages to expenses_agent Llama 3.1 8B needs few-shot examples to output agents:[] Added routing examples to master_system.txt
Bot green dot always offline last_poll offset (90s) shorter than cron cycle (~5min) ab_ai_bot.py: increased offset to 10 minutes

Future: Self-Healing via LLM OS Access

A planned capability is to give the local Llama instance shell access to the agent service container so it can diagnose and fix its own runtime issues based on health check failures — e.g. automatically restarting a stuck worker, adjusting Ollama parameters, or patching a known model misbehaviour. This would require a sandboxed tool interface (likely a restricted bash tool exposed via the agent's tool registry) with clear authorization boundaries.