Skip to content

ai-agents

Audience

PLANA staff. The runtime behind every BOS agent chat. For the customer view, see BOS → Chat and agents.

ai-agents is the FastAPI service that runs the four BOS agents (Finance, Warehouse, Marketing, Sales). It accepts a chat message, the agent decides which tools to call against the customer's tenant Odoo, and the response streams back over Server-Sent Events.

Stack

PropertyValue
Repogit.planapulse.com/plana-pulse/ai-agents
LanguagePython 3.12
FrameworkFastAPI
LLM SDKAnthropic Python SDK
Embeddingssentence-transformers (768 dims) for the Neural Business Network
Image basepython:3.12-slim
Namespaceai-bos-agent
Port8000
Tenant-side RPCOdoo JSON-RPC (via in-cluster Service URL)

Agents and tools

Each agent has a registry-driven set of tools — small functions that read from (or, with caution, write to) the tenant Odoo:

Finance agent (finance)

ToolReads fromReturns
GetGLBalanceaccount_move_lineGL account balances
ListGLAccountsaccount_accountAccount hierarchy
GetCashFlowaccount_move_line (cash accounts)Cash position by date
GetBankBalanceaccount_journal + account_move_linePer-journal balance
GetBankTransactionsaccount_bank_statement_lineImported transactions
ReconcileAccount(read-only currently)Suggested reconciliations

Warehouse agent (warehouse, BOS ID operations)

ToolReads fromReturns
GetStockLevelsstock.quantOn-hand by product / location
GetReorderAlertsstock.warehouse.orderpoint + stock.quantProducts below reorder point
GetSlowMovingStockstock.moveSKUs with no movement in N days
GetPendingReceiptsstock.pickingReceipts not yet validated

Marketing agent (marketing)

ToolReads fromReturns
GetPipelineOverviewcrm.leadPipeline by stage
GetLeadScoringcrm.leadLead scores
GetCampaignMetricsmailing.mailingCampaign send / open rates
GetConversionRatescrm.lead + sale.orderStage conversion

Sales agent (sales)

ToolReads fromReturns
GetPipelineValuecrm.leadPipeline value by stage
GetDealVelocitycrm.leadTime-in-stage analytics
GetTopCustomerssale.orderTop N customers by revenue
GetUpsellOpportunitiessale.order + res.partnerSuggested upsells

Architecture

Customer chats in BOS


pulse-account-api  ─── proxies to ───→  ai-agents

                                            │ 1. Load agent registry by id
                                            │ 2. Build system prompt + tool schemas
                                            │ 3. Stream completion from Anthropic

                                            ├──→ Anthropic API (claude-sonnet-4-6)

                                            │ 4. On tool_call event, dispatch:

                                            ├──→ Tenant Odoo via JSON-RPC

                                            │ 5. Send tool_result back to model
                                            │ 6. Continue completion

                                            │ 7. Emit text_delta events

                                       SSE stream back to BOS

The control loop is in BaseAgent.run_stream():

python
async for event in claude.stream_complete(prompt, tools):
    if event.type == "tool_call":
        result = await self._execute_tool_call(event)
        yield {"type": "tool_result", "data": result}
    elif event.type == "text_delta":
        yield {"type": "text_delta", "data": event.text}
    elif event.type == "done":
        yield {"type": "done"}

API surface

POST /agents/{agent_id}/chat              non-streaming
POST /agents/{agent_id}/chat/stream       SSE streaming
GET  /agents/{agent_id}/executions        recent tool-call audit
GET  /healthz                             liveness
GET  /readyz                              readiness (Anthropic API reachable)

Authentication: X-API-Key header. The key is checked against per-caller records — pulse-account-api has one, pulse-portal another for internal use.

Tool execution

When the model emits a tool call, the agent dispatches it via the tool's implementation. Each tool:

  1. Authenticates against the tenant Odoo via JSON-RPC (ODOO_URL env var, but per-tenant URL via the workspace binding)
  2. Builds an ORM query (search_read typically)
  3. Returns a JSON-serialisable result
  4. Logs the execution to PLANA:executions:{workspace} Redis log with:
    • Tool name
    • Arguments (after sensitive-field redaction)
    • Result size
    • Latency in ms

The Execution log is what customers see in BOS → Execution log.

Shadow Mode

On by default. Every agent runs with shadow_mode=True, which means:

  • Read-only tools execute normally
  • Write tools (currently none in production) emit a would_execute event in the log instead of mutating data
  • The model sees the "what would have happened" result and continues the conversation

Shadow Mode protects against any unintentional write coming out of LLM reasoning. The workspace owner can disable it per-agent from Settings (when write tools ship, which is not yet the case).

Streaming

Streaming is end-to-end SSE:

LayerStreams
Anthropic SDK → BaseLLMClient.stream_complete()text_delta events from the model
BaseAgent.run_stream()wraps text_delta + emits tool_call / tool_result events
ai-agents FastAPI routeSSE response with text/event-stream
pulse-account-api proxyreply.hijack() to forward bytes verbatim
bos-portal (Vue 3 SPA)EventSource client; updates chat bubble per delta

Total round-trip from first byte to first text_delta is typically 800–1500ms for claude-sonnet-4-6.

Guardrails

  • Per-call timeout — 60 seconds for a full chat turn; tool calls individually timeout at 30 seconds
  • Per-tool rate limit — bursty workloads cap at 30 RPS per tool
  • Anomaly detection — sudden 10× growth in tool calls from one workspace flags an alert
  • Kill switchai-agents can be disabled globally via feature_flag.agents_enabled=false; existing chats return a "agents temporarily unavailable" message

Configuration

Env varPurpose
ANTHROPIC_API_KEYPer-org key; SOPS ai_agents.anthropic_api_key
ANTHROPIC_MODELDefault: claude-sonnet-4-6; overrides for batch jobs
OPENROUTER_API_KEYFallback router; SOPS
ODOO_URLCurrently a global URL; per-tenant binding via workspace lookup is in flight
REDIS_URLFor execution log

Multi-tenancy

Per tenant, the agent:

  1. Resolves the tenant's Odoo URL from the workspace binding (pulse-account-api provides this at request time)
  2. Uses the tenant's API user credentials (looked up in SOPS per-tenant today; a per-tenant credentials service is on the roadmap)
  3. Logs to PLANA:sessions:{tenant_id}:{session_id} for chat memory
  4. Logs to PLANA:executions:{workspace} for the audit feed

A bug in one tenant's tool call does not affect another tenant — each request is independent.

Operational tasks

See an agent's recent failures

bash
kubectl -n ai-bos-agent logs deploy/ai-agents --tail=200 | grep ERROR

Test a tool in isolation

bash
kubectl -n ai-bos-agent exec deploy/ai-agents -it -- python -c "
from agents.finance.tools import GetCashFlow
import asyncio
print(asyncio.run(GetCashFlow().run(
    odoo_url='https://e2etest.planapulse.app',
    workspace='e2etest', date_from='2026-05-01'
)))
"

Switch the default model

For a quality vs cost tradeoff:

bash
kubectl -n ai-bos-agent set env deploy/ai-agents \
  ANTHROPIC_MODEL=claude-haiku-4-5-20251001

Best for low-stakes workloads; Sonnet is the right default for routine chats; Opus for complex reasoning on accounting questions.

Where to read more

© PLANA Digital Ltd.