> For the complete documentation index, see [llms.txt](https://docs.opendatadiscovery.org/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.opendatadiscovery.org/features/active-platform-features/genai.md).

# GenAI assistant

The **GenAI assistant** is an opt-in feature that lets the ODD Platform proxy natural-language questions to an **external AI service** that the operator deploys and operates separately. The platform itself does not embed an LLM; it forwards the question, waits for a response, and returns it through its API.

The feature is **disabled by default** (`genai.enabled: false`) and is **API-only today** — the generated TypeScript client (`GenaiApi.ts`, `GenAIRequest.ts`, `GenAIResponse.ts`) ships with the platform UI, but no in-app affordance currently calls it. Operators who turn this on either drive it from their own UI / scripts against the platform's `/api/genai/ask` endpoint, or wait for a future UI surface.

## What gets proxied

Each call from the operator → platform → external AI service has this shape:

```
[client] → POST /api/genai/ask  (JSON: {"body": "<question text>"})
              → platform forwards to: POST {genai.url}/query_data
                                       (JSON: {"question": "<question text>"})
              ← external service returns raw JSON-encoded string
              ← platform un-quotes + Java-unescapes, returns JSON: {"body": "<answer>"}
[client] ← 200 OK (JSON: {"body": "<answer>"})
```

The platform does not modify the question text it forwards (just rewraps it from `body` → `question`). Any catalog context, retrieval-augmentation, or prompt construction must happen inside the external AI service — the platform is a thin proxy.

## Configuration

The three `genai.*` keys (`enabled`, `url`, `request_timeout`), their defaults, the silent-misconfiguration warning when `enabled=true` is set without `url` and `request_timeout`, the working YAML / environment-variable example, and the platform-restart requirement when any of the three change all live on the operator-facing configuration reference at [Configure ODD Platform → GenAI Configuration](/configuration-and-deployment/odd-platform.md#genai-configuration). That page is the canonical home for the platform's configuration keys; this page links rather than embedding so the two surfaces never drift.

## External AI service contract

When `genai.enabled=true`, the platform's `genAiWebClient` is configured at startup with `baseUrl = genai.url` and `responseTimeout = Duration.ofMinutes(genai.request_timeout)`. Each call from `/api/genai/ask` forwards to the external service as:

* **Method:** `POST`
* **Path:** `{genai.url}/query_data` (the `/query_data` suffix is fixed in `GenAIServiceImpl.java:22`)
* **Headers:** none added by the platform (default WebClient headers; no auth, no API key)
* **Request body** (`application/json`):

  ```json
  { "question": "<the question text from the GenAIRequest.body field>" }
  ```
* **Response body** (the platform expects `application/json`): a **raw JSON-encoded string** — that is, the response payload is a single JSON string value (with leading and trailing `"` quotes). The platform strips the outer quotes via `CharMatcher.is('"').trimFrom(item)` and then runs `StringEscapeUtils.unescapeJava(...)` to interpret embedded escape sequences (`\"`, `\n`, etc.) before placing the result in the `GenAIResponse.body` field.

A minimal-but-conforming AI service implementation in Python (Flask), for reference:

```python
from flask import Flask, request, jsonify
import json

app = Flask(__name__)

@app.post("/query_data")
def query_data():
    payload = request.get_json(force=True)
    question = payload["question"]
    # ... your AI / RAG / LLM call here ...
    answer = my_ai_pipeline(question)
    # Return a raw JSON-encoded string. The platform expects to read this with
    # bodyToMono(String.class) and trim the surrounding quotes; jsonify wraps
    # it correctly.
    return app.response_class(json.dumps(answer), mimetype="application/json")
```

{% hint style="info" %}
The platform sends **no authentication** to the external service. If the service must reject anonymous traffic, deploy it behind a network policy / mesh / ingress that authenticates / restricts callers — there is no `genai.token` or similar in `GenAIProperties` today. The external service is also fully responsible for prompt construction, catalog-aware retrieval, and rate limiting; the platform forwards questions verbatim and returns whatever the service returns.
{% endhint %}

## Platform endpoint

The platform's GenAI surface is a single endpoint:

| Method | Path             | Operation ID    | Purpose                                                                         |
| ------ | ---------------- | --------------- | ------------------------------------------------------------------------------- |
| `POST` | `/api/genai/ask` | `genAiQuestion` | Forward a question to the configured external AI service and return its answer. |

Request schema (`GenAIRequest`):

```json
{ "body": "<question text>" }
```

Response schema (`GenAIResponse`):

```json
{ "body": "<answer text>" }
```

When `genai.enabled=false`, the endpoint responds with a `BadUserRequestException` carrying the message "Gen AI is disabled" (HTTP 400) — operators get a clear error rather than a silent no-op.

When the external service times out, the endpoint responds with a `GenAIException` carrying "Gen AI request take longer that {minutes} min" (HTTP 500) — the timeout duration in the message is the configured `genai.request_timeout` value, so operators see exactly what they configured.

For non-timeout errors from the external service (HTTP 5xx, connection refused, malformed response), the platform wraps the underlying cause in a `GenAIException` (HTTP 500) and surfaces it on the `/api/genai/ask` response. There is no retry loop; a single attempt per request.

## UI status

**API-only today.** The platform UI ships generated clients (`odd-platform-ui/src/generated-sources/apis/GenaiApi.ts` plus the request/response models) but no view, button, or panel currently calls them — verified via grep across `odd-platform-ui/src/` excluding `generated-sources/`. To use the feature, drive `POST /api/genai/ask` from your own client (curl, an internal app, a custom in-platform fork). When a UI affordance ships, this section will be updated.

## Platform-to-user security posture

The "Known limitations" section below covers the platform's posture toward the **external** AI service (no auth headers, no retry, no prompt modification, the misconfiguration-fail-at-first-request behaviour). This section covers the platform's posture toward the **user calling the platform** — the surface operators must harden when enabling GenAI.

{% hint style="danger" %}
**The `POST /api/genai/ask` endpoint has no RBAC entry — every authenticated caller can drive arbitrary cost on the operator's external LLM account.** The platform's authorization rules do not enumerate `/api/genai/**`; the route falls through to the catch-all "any authenticated user" rule under `LOGIN_FORM` / `OAUTH2` / `LDAP`, and is reachable **anonymously** under `auth.type=DISABLED`. The endpoint accepts any well-formed `GenAIRequest`, forwards it to the external service, and pays the request cost from the operator's LLM account. Five sub-gaps compound the exposure:

* **No per-user quota or rate limit.** A single client can issue as many requests per second as the network and the external service tolerate.
* **No in-platform audit trail.** The Activity Feed is structurally data-entity-scoped (see [Activity Feed → Scope](/features/active-platform-features/activity-feed.md)) — GenAI events have no data entity to anchor against and cannot be added to the feed without a schema migration. There is no platform-side answer to "who asked what at what time."
* **No PII redaction.** The user's question text, including any free-form content typed in, flows verbatim to the external LLM. Cross-team entity names, owner identities, and downstream-lineage names reach the external service if a question references them.
* **No GenAI-specific request-body size cap.** Large free-form prompts pass through up to the platform's global WebFlux in-memory codec limit (`spring.codec.max-in-memory-size`, default `20MB`); there is no tighter per-user or per-endpoint cap on the GenAI route.
* **Under `auth.type=DISABLED`, no per-user attribution at all.** Every request appears as an anonymous call; both the cost and any external-service-side audit trail attribute the request to the platform process, not to a specific user.

**Mitigation today.** Place a rate-limiting reverse proxy or an API gateway in front of `/api/genai/**` before enabling GenAI in production. Do **not** enable GenAI under `auth.type=DISABLED`; tighten who has `.authenticated()` access if the platform is fronted by a permissive auth mode. Treat the external LLM bill as user-driven cost and monitor it for spikes. The upstream platform hardening (RBAC entry, rate limit, body-size cap, PII scrubber) is on the roadmap.
{% endhint %}

{% hint style="warning" %}
**`genai.url` accepts any URL with any scheme — a config-driven SSRF surface.** The `GenAIProperties` class declares `url` as a plain string with no `@URL` constraint and no scheme / host allow-list. An operator (or any party with config-write access — a Helm overlay, an env-var injection, a leaked configmap) can point GenAI outbound at any reachable URL, **including the platform's own internal network**: cloud-metadata endpoints (`http://169.254.169.254/...`), internal services accessible from the platform's egress identity, file-protocol URIs that the underlying HTTP client may follow.

**Mitigation today.** Treat `genai.url` as a privileged configuration value — restrict who can modify it the same way you restrict database credentials or signing keys. Audit the resolved value at deployment time (don't accept a `genai.url` from a CI variable without a deploy-time review). The upstream fix is a configurable scheme / host allow-list on the property; until it ships, the only guard is operator discipline at the config layer.
{% endhint %}

## Known limitations

* **`@ConfigurationProperties` defaults bite when only `genai.enabled` is set.** `url` defaults to `null` (no field initializer in `GenAIProperties.java`) and `request_timeout` defaults to `0` (= immediate timeout). The platform accepts the misconfiguration at startup and fails at first request. Set `genai.url` and `genai.request_timeout` explicitly when enabling — the operator-side guidance lives on [Configure ODD Platform → GenAI Configuration](/configuration-and-deployment/odd-platform.md#genai-configuration). The upstream-side improvement (defaults declared in `GenAIProperties` or a startup validation) is queued as a separate platform issue.
* **No authentication to the external service.** If the external AI must reject anonymous callers, do it at the network layer (mesh, ingress, NetworkPolicy) — there is no per-request auth header or token added by the platform.
* **No retries.** A single `POST /query_data` per `/api/genai/ask` invocation. The external service must be reliable enough that a single attempt is acceptable, or operators must add retry / circuit-breaker logic upstream of the platform's caller.
* **`request_timeout` is the reply-wait timeout, in minutes** (not seconds, not milliseconds). It sets how long the platform waits for the model's response — the outbound response timeout on the WebClient call to the external service — not a budget for sending the request. The minimum effective value is 1 minute; setting `0` means immediate timeout (see above). It is read once at startup (see "Bean is built once at startup" below), so a change requires a platform restart.
* **Bean is built once at startup.** `WebClientConfiguration` reads `genai.url` and `genai.request_timeout` to construct the `genAiWebClient` bean — changing those values requires a Platform restart for the new WebClient to pick them up.
* **No UI today.** Generated TypeScript clients exist but are not wired into any view; the feature is callable only via the API.

## Where to next

* [Configure ODD Platform → GenAI](/configuration-and-deployment/odd-platform.md#genai-configuration) — the same three keys in the platform's full configuration reference, with environment-variable equivalents.
* [Main Concepts → AI aspects](/introduction/main-concepts.md#ai-aspects) — where GenAI sits among the other AI capabilities (data profiling, ML lineage).
* [API Reference → GenAI](/developer-guides/api-reference/genai.md) — the single `POST /api/genai/ask` endpoint (operationId `genAiQuestion`), the request / response shapes, status codes, and the authorisation posture (cross-link to the security caveat above).
