---
module: technical.ai
title: Adoons (hiveops-ai) — Spring AI fleet assistant & incident/rule intelligence
tab: Overview
order: 10
audience: dev
---

> **DRAFT · internal.** Written 2026-07-01 from hiveops-ai source. Verify against code before relying on specifics.

## Responsibility

`hiveops-ai` (externally branded **Adoons**) is the Spring AI microservice on port **8085**. It owns the natural-language chat assistant (`ChatClient` over Anthropic Claude with `@Tool` function-calling into upstream services) plus a set of internal AI helpers for the incident domain: RAG incident recommendations, journal-line rule/closing-rule suggestions, ATM customer-journey analysis, and periodic analytics insight generation. It is read-only against fleet data — it queries other services on the caller's behalf and never mutates their domain. Cross-link [platform.service-ownership].

## Data

Two separate Hikari datasources (Spring Boot `DataSourceAutoConfiguration` is **excluded** in `application.yml`; both are wired manually).

| Datasource | Default DB | Config key | Tables |
|-----------|-----------|-----------|--------|
| `aiDataSource` (`AiDatabaseConfig`) | `hiveiq_ai` | `AI_DB_URL` / `hiveops.ai.db.url` | `ai_conversation_sessions`, `ai_chat_memory` |
| `vectorDataSource` (`VectorStoreConfig`) | `hiveops_incident` | `VECTOR_DB_URL` / `hiveops.ai.vectordb.url` | `incident_embeddings` (pgvector) |

- **`ai_conversation_sessions`** and **`ai_chat_memory`** are auto-created via `@PostConstruct CREATE TABLE IF NOT EXISTS` in `ConversationSessionRepository` / `JdbcChatMemoryRepository` — no Flyway in this service. Chat history is persisted (not in-memory, despite older CLAUDE.md notes).
- **`incident_embeddings`** is a `PgVectorStore` (COSINE distance, **1024 dims** for `voyage-3`), `initializeSchema(false)` — its schema is owned by Flyway in **hiveops-incident**, not here.

Cross-link [platform.data-stores]. (Journal note: several sibling services use env var `DB_URL`; hiveops-ai instead uses `AI_DB_URL` + `VECTOR_DB_URL`.)

## Kafka

Constants in `AiKafkaConfig`. Bootstrap `KAFKA_BOOTSTRAP_SERVERS` (default `kafka:29092`).

**Consumed**
| Topic | Listener class | Group |
|-------|---------------|-------|
| `analytics.snapshot-ready` | `AnalyticsInsightJob.onSnapshotReady` | `ai-insight-group` |
| `adoons.rule-suggestion.requested` | `RuleSuggestionConsumer.onRequest` | `ai-adoons-group` |

**Produced**
| Topic | Producer class | Notes |
|-------|---------------|-------|
| `adoons.rule-suggestion.ready` | `RuleSuggestionConsumer` (`ruleSuggestionKafkaTemplate`) | Async result of a rule-suggestion request; status `READY`/`FAILED` |

Note: `AnalyticsInsightJob` reacts to `analytics.snapshot-ready` but publishes its insights back over **REST** to hiveops-analytics (`AnalyticsApiClient`), not Kafka. Consumer `auto-offset-reset: latest` — historical snapshots are skipped on redeploy. Cross-link [platform.kafka].

## API surface

Two controllers. `/api/ai/**` requires a customer JWT; `/api/adoons/**` is internal (`permitAll` in Spring Security, gated inside the controller by the `X-Internal-Secret` header).

**`AiChatController` — base `/api/ai`** (JWT auth, `@AuthenticationPrincipal String userEmail`)
| Method | Path | Purpose |
|--------|------|---------|
| POST | `/api/ai/chat` | Synchronous chat; returns `{message, conversationId}` |
| POST | `/api/ai/chat/stream` | SSE streaming chat (`text/event-stream`) with 20s heartbeat |
| GET | `/api/ai/conversations` | List current user's conversations |
| GET | `/api/ai/conversations/{id}/messages` | Full message history for a conversation |
| DELETE | `/api/ai/conversations/{id}` | Delete a conversation + its messages |

**`IncidentAiController` — base `/api/adoons`** (X-Internal-Secret, called by hiveops-incident)
| Method | Path | Purpose |
|--------|------|---------|
| POST | `/api/adoons/incidents/index` | Upsert an incident into the pgvector store (on create/update) |
| POST | `/api/adoons/incident-recommend` | RAG recommendation from similar resolved incidents |
| POST | `/api/adoons/suggest-rule` | Suggest a processing rule from a raw log line (sync) |
| POST | `/api/adoons/suggest-closing-rule` | Suggest a closing rule (sync) |
| POST | `/api/adoons/analyze-journey` | Analyze a raw ATM journal block → JSON customer-journey narrative |
| POST | `/api/adoons/journal/index-now` | Trigger journal embedding run immediately (bypass 02:00 cron) |

The `suggest-rule`/`suggest-closing-rule` endpoints have an **async Kafka twin**: `adoons.rule-suggestion.requested` → `RuleSuggestionConsumer` → `adoons.rule-suggestion.ready`. Both the sync REST path and the async Kafka path share `RuleSuggestionService` / `ClosingRuleSuggestionService`.

## Gotchas

- **No role checks.** `/api/ai/**` only requires an authenticated caller (`anyRequest().authenticated()`) — any valid customer JWT (issued by **hiveops-auth**, HS512) works; there is no `hasAnyRole('MSP_ADMIN','BCOS_ADMIN')`. Upstream tool calls forward the caller's raw token (`RequestTokenHolder`, `@RequestScope`) so downstream services enforce their own permissions.
- **`JWT_SECRET` must match hiveops-auth**, not the mgmt internal secret.
- **`X-Internal-Secret`** (`INTERNAL_SERVICE_SECRET` / `hiveops.internal.service-secret`) guards all `/api/adoons/**` endpoints in the controller — if it is blank/unset, every internal call is rejected with 403 (`isValidSecret` requires a non-blank configured secret).
- **Streaming token capture:** `streamChat` reads the JWT from `RequestTokenHolder` on the servlet thread *before* entering the Reactor chain, because `@RequestScope` is not visible on scheduler threads where tool calls run. `shouldFilterAllDispatcherTypes(false)` skips re-auth on ASYNC SSE dispatches.
- **Analytics insight throttle:** even when `analytics.snapshot-ready` events arrive frequently, `AnalyticsInsightJob` only regenerates once per `INSIGHT_INTERVAL_MINUTES` (default 30).
- **Journal indexing cron:** `JournalIndexingJob` runs `0 0 2 * * *` (02:00 UTC daily); `/api/adoons/journal/index-now` forces an immediate run.
- **Model/provider swap:** default is Anthropic `claude-sonnet-4-6` + `voyage-3` embeddings; activating the `ollama` profile (`SPRING_PROFILES_ACTIVE=prod,ollama`) disables Anthropic and uses local Ollama chat + `mxbai-embed-large` embeddings instead. `spring.ai.retry.max-attempts: 1` (no retries).
- **DB-name inconsistency:** the vector datasource default URL points at `hiveops_incident` while most platform DBs are named `hiveiq_*` — confirm the actual DB name per environment before relying on it.
- **Stale CLAUDE.md:** the repo's `CLAUDE.md` still describes a stateless service with `InMemoryChatMemory` and only Fleet/Incident/Mgmt tools; the code now persists chat to Postgres and has more tool classes (`ClaimsTools`, `FleetTools`, `IncidentTools`, `JournalTools`, `MgmtTools`, `PatternTools`, `RagTools`).
