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>
- 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>
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>
- 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>
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>
debian/DEBIAN/control: package metadata, depends on python3.11+, postgresql-client
debian/DEBIAN/postinst: creates activeblue-ai system user, installs venv, enables service
debian/DEBIAN/prerm: stops and disables service before removal
debian/DEBIAN/postrm: purge removes config, logs, venv, and system user
debian/lib/systemd/system/activeblue-ai.service:
- Runs as dedicated user with PrivateTmp + ProtectSystem hardening
- EnvironmentFile=/etc/activeblue-ai/.env
- Restart=on-failure with 5s backoff
debian/usr/bin/activeblue-ai: CLI with start/stop/restart/status/logs/migrate/health/sweep/privacy/version
build_deb.sh: builds activeblue-ai_X.Y.Z_all.deb in dist/
publish_repo.sh: scans packages, generates Release + checksums, optional GPG signing
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>