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>
This commit is contained in:
298
ARCHITECTURE.md
Normal file
298
ARCHITECTURE.md
Normal file
@@ -0,0 +1,298 @@
|
||||
# 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`: `processing` → `complete` / `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.
|
||||
Reference in New Issue
Block a user