ab_ai_mail.py: when a user sends a file via Odoo 18 Discuss, the zip
was going through /dispatch (text-only) instead of /upload, causing the
bot to respond "I'm unable to locate the zip file" because attachment_ids
was empty in the message_post override.
Root cause: Odoo 18 Discuss links file attachments to mail.message
records via three different mechanisms depending on the upload path, and
we only checked one (the Many2many relation table).
Fixes:
1. Three-method attachment detection in message_post:
- Method 1: result.attachment_ids (Many2many relation table)
- Method 2: ir.attachment with res_model='mail.message' (Odoo 15+ style)
- Method 3: attachment IDs parsed from href URLs in the HTML body
2. Deferred retry in _agent_thread: if att_data is still empty but a
message_id is known, sleep 1s then re-read via a fresh DB cursor so
we see data committed after message_post returned (timing race fix)
3. Skip zero-byte attachments and warn instead of silently using them
4. Pass message_id to the background thread (new kwarg, backward compat)
5. Add debug logging so future issues can be diagnosed from Odoo logs
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1. OdooClient missing self._timeout — every _xmlrpc_call raised
AttributeError, making the odoo health check permanently fail.
Fix: set self._timeout = XMLRPC_TIMEOUT in __init__.
2. action_ping only accepted ollama=='ok' but health.py now returns
'warming' when the model is not yet hot in VRAM. Fix: treat
warming as passing so the bot goes online and the model loads
on the first real request.
3. /ai/approval/pending declared methods=['GET'] on a type='json'
route — Odoo JSON-RPC always POSTs, so every browser call got
405 METHOD NOT ALLOWED. Fix: change to methods=['POST'].
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- All specialist agents: handle_peer_request(request_type, params, directive_id)
replaces handle_peer_request(request: dict) so callers pass structured args
- ab_ai_bot: force-write bus_presence.status via SQL so Odoo 18 WebSocket presence
shows the correct colour immediately (ORM compute does not trigger on last_poll writes)
- odoo_client: wrap XML-RPC executor calls in asyncio.wait_for to enforce timeout
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Previously ab_ai_mail.py intercepted file uploads before reaching the
LLM and responded with a hardcoded clarification template. The LLM had
no involvement in the file upload response.
Changes:
- ab_ai_mail.py: remove _post_file_clarification, _find_pending_attachments,
_describe_zip, and the two-step pending-attachment lookup. All messages
(text, files, or both) are dispatched to the agent service immediately.
Files with no text pass an empty message — the LLM decides what to do.
- upload.py: default message changed from hardcoded receipt instruction
to '' so the LLM determines intent from file content.
- master_agent._synthesize: always runs through the LLM for both single
and multi-agent cases — no raw templates reach the user.
- master_system.txt: add FILE UPLOADS routing rule so the LLM knows to
route receipts to expenses_agent without asking for clarification.
New flow: upload → parse → LLM classifies → agent acts → LLM synthesizes
natural response → user sees it. Zero scripted intercepts.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Bot green dot stays on for 10 minutes after each successful health
check (2× the ~5-min cron cycle). A failed check sets last_poll to
1 hour in the past, going offline immediately. If the cron stops
entirely, the dot goes offline on its own after 10 minutes.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Odoo 18's _compute_status treats future last_poll as MORE disconnected
(absolute delta). Override forces status='online' when last_poll > now,
which is set 24h ahead by _sync_bot_user_presence when the health check
passes.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
action_ping now checks db, odoo, ollama, and master_agent individually.
All four must report 'ok' for the bot to go online. Presence is updated
immediately inside action_ping (not as a separate cron step), so every
ping — whether from the cron or a manual button press — atomically checks
all systems and sets the correct online/offline/error state.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Set last_poll and last_presence 24h ahead when the service is confirmed
online, so status stays 'online' until the cron explicitly marks it down.
The previous 10min offset still expired between cron runs.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- 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>
Before writing any expense records the bot now posts a numbered table
of parsed vendor/amount/date for every receipt, with duplicate entries
flagged inline. User replies 'confirm' (skips dups) or 'confirm, keep
all'. This catches OCR amount misreads before they land in Odoo.
Also removes the separate awaiting_dup_approval step; duplicate review
is now part of the single confirmation table.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
DispatchResponse declared actions_taken as list[dict] but agents return
list[str], causing a 422 on every successful upload.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
message_post now returns immediately after collecting attachment data.
The agent HTTP call and reply posting happen in a daemon thread, so
Odoo commits the user's message and the browser confirms receipt right
away -- instead of waiting 10+ seconds for Ollama to respond.
File clarification (no LLM) still posts inline since it's instant.
The background thread opens its own DB cursor to post the bot reply.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
All bot messages now built as plain text and converted via _text_to_html()
which escapes content and converts newlines to <br>. This avoids raw HTML
tags appearing literally in Odoo 18 Discuss.
- _describe_zip: returns plain str (no Markup/HTML)
- _post_file_clarification: builds plain text, posts via _text_to_html()
- _find_pending_attachments: strip HTML before phrase matching
- _text_to_html: new helper shared by clarification and agent replies
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- master_agent: thread raw user message into extra_context and peer_data so
expenses_agent can check it directly without relying on LLM intent_summary
- master_agent: when receipts are in extra_context always route to expenses_agent,
so replies like 'skip duplicates' still trigger expense processing
- expenses_agent: _plan() checks peer_data raw_message alongside task so
skip/keep keywords are detected even when master rewrites the intent
- ab_ai_mail: wrap clarification message HTML in Markup() so Odoo does not
re-escape the tags; use <br> instead of <br/>
- ab_ai_mail: convert agent plain-text replies newlines to <br> for proper
line-break rendering in Discuss
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- expenses_agent: extract transaction time (HH:MM) from OCR receipt text
- expenses_agent: _find_semantic_duplicate uses time to rule out false positives (>30 min apart = different receipts)
- expenses_agent: pause when duplicates found, set mode=awaiting_dup_approval, ask user before creating sheet
- expenses_agent: _report formats approval message listing each dup pair with vendor/amount/date/times/filenames
- ab_ai_mail: _find_pending_attachments recognises dup-approval bot message so ZIP re-attaches on user reply
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- ab_ai_bot: raise requests.post timeout 120s -> 600s so long OCR+LLM
runs don't silently drop the reply in Discuss
- upload: run parse_upload in ThreadPoolExecutor so tesseract OCR
doesn't block the FastAPI event loop
- expenses_agent: parse all receipts concurrently with asyncio.gather
(Ollama semaphore caps parallelism at 2); reduces 13-receipt LLM
time from ~39s sequential to ~20s parallel
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- ollama_backend: add format='json' for 'master' and receipt_parser
callers so llama3.1:8b returns valid JSON instead of plain English
- ab_ai_mail: add debug logging to trace attachment_ids from Discuss;
handle file-only messages and clarification look-back flow
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Discuss bot now reads ir.attachment from incoming messages; file-only
messages no longer silently dropped
- ZIP files are described (contents listed) and bot asks clarifying
question before acting; user's follow-up reply looks back for pending
attachments so files don't need to be re-uploaded
- receipt_parser: extracts text from ZIP (recursive), JPG/PNG/etc (OCR),
PDF (pdfplumber), HTML, TXT
- expenses_agent: full rewrite fixing broken method signatures; adds
create_expense_sheet / create_expense / attach_receipt flow driven by
LLM receipt parsing (Ollama, HIPAA-locked)
- master_agent: extra_context threads receipts + user_id into directives
- FastAPI /upload multipart endpoint; registered in main.py
- Odoo /ai/upload controller proxies files to agent service
- ab_ai_bot: dispatch_message_with_files() for multipart uploads
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- docker-compose.odoo.yml: add activeblue-net so Odoo can reach
activeblue-agent by hostname; fix addons volume mount (was odoo_module)
- ab_ai_bot.py: bus.presence.status is computed — write only last_poll
and last_presence; set last_poll 90s ahead when online so the bot
stays green across the 60s cron cycle (DISCONNECTION_TIMER=30s)
- ir_cron.xml: reduce ping interval to 20s (uses Odoo 18 seconds type)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
action_ping was hitting /health, which returns 200 as long as the
FastAPI app responds — even when Ollama is down, dispatch fails. So the
bot showed online while every DM errored.
Switch to /health/detailed and gate 'online' on db, master_agent and
the active LLM backend (ollama for local privacy mode, claude
otherwise) all reporting 'ok'. Anything else flips the bot to error
or offline, which propagates through _sync_bot_user_presence to a grey
dot in Discuss.
env.get() isn't an Odoo Environment method, so the existence guard
silently returned without ever writing the bot's presence row. Use the
'in self.env' check instead.
Extend cron_ping_all to upsert a bus.presence row for the
activeblue_ai_bot user, mirroring the agent service's reachability:
green dot when /health responds 200, offline otherwise. Drop the cron
interval from 5m to 1m so the presence stays fresh (Odoo's presence
client expects refreshes well under a minute).
- Create res.partner for the AI bot (appears in DM contacts)
- Override mail.channel.message_post to intercept direct messages
to the bot partner and forward them to the agent service
- Post the agent reply back into the Discuss channel as the bot
- Add discuss to depends; load res_partner_bot.xml data
Users can now open Discuss -> New Message -> search 'ActiveBlue AI'
to start a conversation with the agent.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>