- 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>
After parsing all receipts, identify photos that are different shots of
the same physical receipt by comparing amount + date + vendor similarity
(difflib ratio >= 0.6). When a duplicate is found, keep whichever photo
produced the most OCR text (clearest shot) and report the skipped ones.
Zero-amount receipts (OCR failed entirely) are excluded from semantic
dedup to avoid false positives.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Dockerfile: add tesseract-ocr-osd for orientation detection data
- receipt_parser: resize large phone photos to 1800px, convert to
grayscale, sharpen before OCR; use psm 1 (auto + OSD) so rotated
receipts are correctly oriented before text extraction
- expenses_agent: tighten amount extraction prompt to pick the FINAL
total, not subtotal or tax line, reducing misreads like 42.90->409.00
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>
- Dockerfile: install tesseract-ocr so Pillow+pytesseract can OCR receipt images
- operational_store: JSON-serialize raw_data before passing to asyncpg JSONB
- receipt_parser: add SHA256 hash + date extracted from filename timestamps
- expenses_agent: deduplicate receipts by hash before creating expense records
- expenses_agent: fetch all expensable Odoo products, pass list to LLM for
category selection (Meals, Flights, etc.) per receipt
- expenses_agent: pass date_hint from filename (e.g. 20260509_180857.jpg -> 2026-05-09)
as fallback when OCR text is unavailable
- expenses_tools: add get_expense_products() to fetch all expensable products
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- _synthesize: short-circuit on any single-agent report (avoids extra
Ollama call that can timeout); wrap multi-agent LLM call in try/except
- _update_memory: catch exceptions so DB/memory failures don't kill reply
- _log_directive_start: use 0 instead of NULL for channel_id (NOT NULL col)
- create_expense: drop 'description' field (not valid on hr.expense in Odoo 18)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
AGENT_ACCESS_GROUPS uses XML IDs (e.g. hr_expense.group_hr_expense_user)
but the check compared them against res.groups.full_name strings which
never matched, denying every user access to all restricted agents.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The master agent was routing expense/receipt requests to finance_agent
instead of expenses_agent because only DB-registered agents appeared
in get_active_agents(). This adds auto-activation of all in-memory
registered agents with precise capability summaries so the LLM picks
the right specialist.
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>
- ElearningTools: add create_course, update_course, publish_course,
add_section, create_slide, enroll_user write methods using OdooClient
- ElearningAgent: fix all BaseAgent method signatures (_plan/_gather/
_reason/_act/_report no longer take wrong positional args)
- Replace dead _dispatch_tool pattern with _tool_<name> methods so
BaseAgent._run_tool() can drive them via LLM tool calls in _loop()
- Add LLM-driven course creation in _reason(): when intent is create,
_loop() is called with a course-building system prompt and all tools;
the LLM calls create_course → add_section → create_slide → publish
- Fix handle_peer_request signature to match BaseAgent interface
- Fix AgentReport missing directive_id; fix SweepReport invalid kwargs
- Extend ELEARNING_TOOLS list with all new write-side tools
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
BaseAgent._lookup_odoo_context() calls odoo_doc_agent via PeerBus before
_plan() runs on every directive. The RAG answer is stored in
self._gathered['odoo_context'] and injected into every _loop() LLM call
so agents reason with correct Odoo 18 workflow steps automatically.
No changes required to individual agents. odoo_doc_agent opts out via
auto_rag=False to prevent self-referential calls.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Wraps odootrain RAG API (http://192.168.2.9:8000) as a BaseAgent so any
specialist agent can query Odoo 18 docs mid-execution via PeerBus
request_type=query_docs. Participates in sweep health checks.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Health endpoint called .ping() on both but neither implemented it,
causing ollama/odoo to always show as error and the bot to stay offline.
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>
Routers calling /registry/agents raised AttributeError because
get_all() was not defined. Added method returning all registered
agents with active status, capabilities and instance flags.
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).
The classifier was silently falling back to a clarification prompt every
time the LLM wrapped its JSON in markdown fences, prefixed it with
'json', or added surrounding prose. The bot then asked 'Could you
clarify what you need?' to every message regardless of clarity.
Now: strip code fences, slice to the first {...} block, and on parse
failure log the raw content (truncated) and treat the message as 'no
specialist agent' so the direct-answer fallback responds instead of
looping on clarification.
Previously when the LLM classified a message as needing no specialist
agent, the dispatcher built zero directives and _synthesize returned
'No agent responses received.' Greetings, follow-up clarifications,
and general questions all fell into this dead end.
Now when intent.agents is empty and no clarification is needed, the
master makes a second LLM call with the recent conversation as context
and answers directly. Updated master_system.txt to steer the classifier
toward agents=[] for chitchat instead of forcing a clarification loop.
The f-string only spanned the first fragment ('You don') so the
{chr(44).join(...)} placeholder leaked into chat output as literal
text. Build the message with plain string concat.
User messages were only saved inside _update_memory at the end of a
successful directive. The clarification and access-denied branches
returned early without ever calling it, so when a clarification turn
asked 'what do you mean?' and the user replied, the original question
was missing from context — the bot looked at a transcript of nothing
but its own clarifying questions and asked yet another.
Save the user message at the top of handle_message so every branch
includes it. Drop the now-duplicate write from _update_memory.
ollama-python 0.3.x returns the response as a dict, while newer releases
return pydantic objects. The backend assumed objects (response.message)
and crashed with AttributeError on every dispatch. Use a helper that
accepts either shape so the code works across versions.
The prompt template contains a literal JSON example block ({"needs_clarification": ...})
which str.format() tried to interpret as format fields, raising KeyError on every
Discuss DM. Switch to .replace() so braces in the template are taken literally.
Without exc_info we only see the bare exception string, which has been
unhelpful for debugging Discuss DM failures (e.g. a KeyError whose
message is just a JSON key, with no clue where it was raised).
Odoo's bot model serialises user_id as a string (str(uid)) over the
HTTP boundary, but the asyncpg memory queries ($1) expect an integer.
This caused 'str object cannot be interpreted as an integer' on every
Discuss DM. Cast at the entry point so downstream stores get an int.
The router was calling handle_message(user_id, message, context, session_id)
but MasterAgent accepts (user_id, channel_id, message, directive_id) and
returns MasterResponse{response, status, ...} with no .reply or
.agent_reports fields. Discuss DMs to the bot crashed with TypeError.
Now the router:
- Derives directive_id from session_id (or generates one)
- Pulls channel_id out of req.context
- Maps MasterResponse.response -> DispatchResponse.reply
- Returns an empty agent_reports list (the field is reserved for future use;
per-agent reports aren't part of MasterResponse)
The bot DM flow silently failed because ab_ai_mail.message_post override
bails when no active ab.ai.bot row exists, and the agent service loaded
0 agents from an empty ab.ai.agent.registry. Both tables stayed empty
because nothing populated them.
The post_init/post_migrate hook now seeds:
- One active ab.ai.bot pointing at http://activeblue-agent:8001
- The 8 specialist agents (finance, accounting, crm, sales, project,
elearning, expenses, hr) plus master, all on the ollama backend
Idempotent: skips rows that already exist.
The post_init/post_migrate hook now:
- Uses no_reset_password context to skip Odoo's invitation email flow
- Writes a res_users_log row so the user shows as Confirmed instead of
Pending Invitations (Odoo 18 derives state from log presence)
- Idempotently ensures the partner_activeblue_ai external ID points at
the bot's partner so env.ref() resolves in ab_ai_mail.py
Discovered while fixing the bot DM flow on miaai: even after the user
was created, Discuss showed it as Pending until a res_users_log row was
inserted manually.
- Bot needs to be a res.users to appear in Discuss DM search
- post_migrate_hook runs on both install and -u update
- Idempotent: skips creation if user already exists
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
XML record creation bypasses ORM defaults causing NOT NULL violation
on autopost_bills (added by account module). Hook uses ORM create()
which applies all field defaults correctly.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- 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>
rpc service was removed in Odoo 17+. Import rpc function directly from
@web/core/network/rpc instead of using useService('rpc').
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
attrs={'invisible': [...]} syntax was removed in Odoo 17.
Converted all occurrences to inline invisible= expressions.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
group_ai_manager -> activeblue_ai.group_ai_manager to ensure Odoo
resolves the group reference correctly regardless of load order.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
CSV references group_ai_manager and group_ai_user which must be
created before the access rules that reference them.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Some Odoo instances require the user's actual login/email for API key
auth rather than the __system__ special login. ODOO_USER defaults to
__system__ for standard Odoo 16+ installs.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- main.py: MemoryManager(pool=pool, llm=...) -> llm_router=...
Class signature is __init__(self, pool, llm_router=None).
- alembic.ini: script_location = migrations -> agent_service/migrations
When alembic runs from WORKDIR /app inside the container, 'migrations'
resolves to /app/migrations (missing). Correct path is
/app/agent_service/migrations where versions/ actually lives.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>