- 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>
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-dbodoo— XML-RPC auth against Odoo succeeded at startupollama— GET/api/tagson the Ollama host returns 200master_agent— MasterAgent instance is loaded and readyprivacy_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 logsstatus:processing→complete/partial/failed/awaiting_approvalraw_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.