Commit Graph

65 Commits

Author SHA1 Message Date
Carlos Garcia
8a9d772b8e fix: increase timeout and parallelize receipt processing
- 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>
2026-05-16 01:50:12 -04:00
Carlos Garcia
ef6dad5a81 feat: OCR via tesseract, dedup, category selection for expense receipts
- 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>
2026-05-16 01:40:32 -04:00
Carlos Garcia
6ab9624ec6 fix: harden master agent synthesize/memory, fix expense create fields
- _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>
2026-05-16 01:37:36 -04:00
Carlos Garcia
261252abdd fix: resolve group XML IDs via ir.model.data in access check
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>
2026-05-16 01:28:01 -04:00
Carlos Garcia
f9ade69f55 fix: auto-activate registered agents with descriptive capabilities
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>
2026-05-16 01:24:26 -04:00
Carlos Garcia
62d5d3f550 fix: force JSON output for Ollama intent classification; fix attachment detection
- 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>
2026-05-16 01:17:58 -04:00
Carlos Garcia
4b7223a139 feat: file upload + expense report creation from Discuss attachments
- 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>
2026-05-16 01:02:24 -04:00
Carlos Garcia
bee8e20580 feat(elearning): add course-building capability to elearning agent
- 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>
2026-05-14 23:49:11 -04:00
Carlos Garcia
65920d6128 feat: auto-inject Odoo workflow context into every agent execution
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>
2026-05-14 23:35:03 -04:00
147a99cab2 chore: ignore odootrain/ (separate repo) 2026-05-15 03:22:14 +00:00
043d30bd9b chore: ignore odootrain/ (separate repo) 2026-05-15 03:21:54 +00:00
Carlos Garcia
e215a26c58 feat: register OdooDocAgent as PeerBus specialist agent
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>
2026-05-14 22:53:29 -04:00
384b42ab03 Switch default Ollama model to activeblue-chat (fine-tuned Llama 3.1 8B) 2026-05-14 13:31:22 +00:00
Carlos Garcia
5261396ef7 fix(agent): add missing ping() to OllamaBackend and OdooClient
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>
2026-05-12 22:51:03 -04:00
Carlos Garcia
103f575f92 fix(addon): bot presence — fix network isolation, computed field write, and timer gap
- 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>
2026-05-12 22:42:35 -04:00
f30751a696 fix: correct server IP 192.168.2.47 -> 192.168.2.9 in MCP config 2026-05-12 23:09:04 +00:00
9b2dc1ee6b fix: publish port 8001 to host, replace curl healthcheck with python3 urllib 2026-05-12 23:09:03 +00:00
b6d5e6ee57 fix: add AgentRegistry.get_all() method
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.
2026-05-12 23:08:45 +00:00
Carlos Garcia
c215b9bf0c fix(addon): require LLM backend reachability for bot online state
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.
2026-04-26 11:32:35 -04:00
Carlos Garcia
38fa508e0f fix(addon): correct existence check for bus.presence model
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.
2026-04-25 17:10:35 -04:00
Carlos Garcia
5e9b4b244c feat(addon): show AI bot online in Discuss when agent is healthy
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).
2026-04-25 17:05:55 -04:00
Carlos Garcia
d49a51a5e8 fix(agent): tolerant intent JSON parse + log raw output on failure
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.
2026-04-24 23:28:18 -04:00
Carlos Garcia
f774cca7ab feat(agent): direct-answer fallback for non-Odoo questions
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.
2026-04-24 23:27:06 -04:00
Carlos Garcia
27325bc140 fix(agent): render denied_agents list in access error
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.
2026-04-24 23:25:58 -04:00
Carlos Garcia
18f2c91715 fix(agent): persist user message on every turn, not just happy path
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.
2026-04-24 23:24:40 -04:00
Carlos Garcia
01adfbfb1a fix(agent): handle dict and pydantic shapes from ollama-python
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.
2026-04-24 23:16:36 -04:00
Carlos Garcia
f020d1406a deps(agent): add ollama python client
ollama_backend.py imports ollama lazily; without the package every
local-mode dispatch crashed with ModuleNotFoundError.
2026-04-24 23:13:34 -04:00
Carlos Garcia
67e6eff534 fix(agent): use plain substitution for master_system prompt
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.
2026-04-24 23:12:51 -04:00
Carlos Garcia
4cbc4cc0f1 chore(agent): log full traceback when MasterAgent fails
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).
2026-04-24 23:11:46 -04:00
Carlos Garcia
b4f1f5f015 fix(agent): coerce user_id to int in MasterAgent.handle_message
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.
2026-04-24 23:10:00 -04:00
Carlos Garcia
4cb94b18f1 fix(agent): align /dispatch with MasterAgent.handle_message signature
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)
2026-04-24 23:06:24 -04:00
Carlos Garcia
beb08b0b4b fix(addon): seed ab.ai.bot and agent registry on install
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.
2026-04-24 22:51:37 -04:00
Carlos Garcia
79537a5e21 fix(addon): create AI bot as Confirmed (skip invitation email)
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.
2026-04-24 22:49:42 -04:00
Carlos Garcia
368c50bde4 fix(registry): use correct Odoo field names (active/agent_name not is_active/agent_key)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-24 22:29:45 -04:00
Carlos Garcia
01f7037a38 fix(addon): create AI bot as internal user; use post_migrate_hook
- 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>
2026-04-24 22:20:09 -04:00
Carlos Garcia
d5381220fb fix(addon): create bot partner via post_init_hook instead of XML
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>
2026-04-24 22:17:43 -04:00
Carlos Garcia
65957339ef fix(addon): inherit discuss.channel not mail.channel (renamed in Odoo 17+)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-24 22:15:46 -04:00
Carlos Garcia
e3ef649ee6 fix(addon): remove discuss from depends (included in mail in Odoo 18)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-24 22:15:08 -04:00
Carlos Garcia
992d2c2775 feat(addon): add ActiveBlue AI bot to Odoo Discuss
- 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>
2026-04-24 22:14:00 -04:00
Carlos Garcia
6cb09282c2 fix(addon): replace useService('rpc') with direct rpc import for Odoo 18
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>
2026-04-24 21:44:59 -04:00
Carlos Garcia
86708f09eb fix(addon): replace deprecated attrs with Odoo 17+ inline expressions
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>
2026-04-24 21:23:15 -04:00
Carlos Garcia
15afe51d9c fix(addon): resolve all Odoo 18 install errors
- security/res_groups.xml: fully qualify ref('group_ai_user') with module prefix
- data/ir_cron.xml: remove numbercall field (removed in Odoo 17/18)
- views/menus.xml: remove web_icon referencing missing static/src/img/icon.png
- controllers/webhook.py: use get_json() instead of deprecated json.loads(data)
- security/ir.model.access.csv: use fully-qualified group external IDs (prior commit)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-24 21:20:40 -04:00
Carlos Garcia
a4759b583e fix(addon): use fully-qualified external IDs in access CSV
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>
2026-04-24 19:23:36 -04:00
Carlos Garcia
cd606a1db8 fix(addon): load res_groups.xml before ir.model.access.csv
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>
2026-04-24 19:21:40 -04:00
Carlos Garcia
590f1b7ee2 fix: make Odoo login configurable via ODOO_USER (default __system__)
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>
2026-04-24 19:15:06 -04:00
Carlos Garcia
65e471b6ce fix: MemoryManager kwarg 'llm' -> 'llm_router'; fix alembic script_location
- 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>
2026-04-24 17:43:03 -04:00
Carlos Garcia
6d15859779 fix(setup.sh): detect stale DB volume, run Alembic migrations on startup
- Before bringing up the stack, check if the agent-db volume exists but
  is missing the expected database (left from a previous broken run with
  wrong POSTGRES_DB). Offer to wipe and re-init automatically.
- After docker compose up, wait for pg_isready then run
  `alembic upgrade head` inside the agent container so tables are created
  before the agent attempts to use them.
- Restart agent-service after migrations so it connects to a fully
  initialized database on its first attempt.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-24 17:39:51 -04:00
Carlos Garcia
7765824c70 feat: add setup.sh — zero-config interactive installer
Replaces manual .env editing with a guided setup script.

Auto-discovers:
- Odoo container name and database (via `docker ps` + odoo.conf inspection)
- Ollama endpoint (scans 192.168.2.10/9, 192.168.2.1, localhost on port 11434)
- Ollama model list (lets user pick from available, auto-selects if only one)

Auto-generates (idempotent — preserves on re-run):
- POSTGRES_PASSWORD (openssl rand -hex 24)
- AGENT_API_KEY     (openssl rand -hex 32)
- WEBHOOK_SECRET    (openssl rand -hex 32)

Postgres constants always written correctly:
- POSTGRES_HOST=agent-db, POSTGRES_DB=activeblue_ai, POSTGRES_USER=activeblue
  (fixes previous issue where these were blank or wrong in .env)

Odoo API key:
- Attempts auto-creation via Odoo HTTP session + res.users.apikeys.description
  wizard (works on Odoo 16/17/18 with valid admin credentials)
- Falls back to clear manual instructions + paste prompt on failure

Writes .env with chmod 600. Offers to `docker compose up -d` when done.

Usage:
  cd /root/odoo/odoo-ai
  bash setup.sh

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-24 17:35:23 -04:00
Carlos Garcia
a3fbc9746f fix: remove stale port binding and deprecated version key from compose
- Remove ports: mapping (192.168.2.47:8001:8001 — stale IP from prior host,
  caused 'cannot assign requested address' bind failure). Agent is only
  reachable via activeblue-net; no host exposure needed.
- Remove top-level version: key (obsolete, triggers deprecation warning).

These fixes were applied manually on the miaai host but never committed,
causing git pull to conflict and the Python fixes to silently not apply.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-24 16:53:54 -04:00
Carlos Garcia
c769fca79f fix: resolve all 5 startup constructor errors + add DB retry
Fixes all errors reported in docker compose logs agent-service:

1. config.py: add ollama_max_concurrent, claude_timeout, claude_max_concurrent
   fields so LLMRouter(config=settings) can read them without AttributeError.

2. main.py - LLM router: drop manual OllamaBackend/ClaudeBackend construction;
   call LLMRouter(config=settings, pg_pool=pool) to match class signature.
   Fixes: OllamaBackend.__init__() unexpected kwarg 'base_url'.

3. main.py - DB: add 5-attempt retry with 2s backoff and redacted DSN logging.
   Fixes: connection refused race on startup before Postgres accepts connections.

4. main.py - AgentRegistry: call AgentRegistry() with no args (class takes none),
   then await agent_registry.load_from_odoo(odoo) to populate active agents.
   Fixes: AgentRegistry.__init__() unexpected kwarg 'odoo'.

5. main.py - PeerBus: pass registry=agent_registry at construction; register
   specialist agents on agent_registry (not peer_bus, which has no register()).
   peer_bus.py: make directive_id optional (default None) — bus is a singleton
   at startup; directive_id is only needed per-request.
   Fixes: PeerBus.__init__() missing positional args 'registry' and 'directive_id'.

6. main.py - MasterAgent: drop unexpected peer_bus= kwarg from constructor call.
   Fixes: MasterAgent.__init__() unexpected kwarg 'peer_bus'.

7. mcp_router.py: pass NotificationOptions() instance instead of None.
   Fixes: AttributeError 'NoneType' has no attribute 'tools_changed' (was applied
   in running container but not committed; now committed).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-24 16:48:23 -04:00