mirror of
https://github.com/apache/superset.git
synced 2026-08-05 05:22:37 +00:00
355 lines
16 KiB
Markdown
355 lines
16 KiB
Markdown
<!--
|
|
Licensed to the Apache Software Foundation (ASF) under one
|
|
or more contributor license agreements. See the NOTICE file
|
|
distributed with this work for additional information
|
|
regarding copyright ownership. The ASF licenses this file
|
|
to you under the Apache License, Version 2.0 (the
|
|
"License"); you may not use this file except in compliance
|
|
with the License. You may obtain a copy of the License at
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
Unless required by applicable law or agreed to in writing,
|
|
software distributed under the License is distributed on an
|
|
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
KIND, either express or implied. See the License for the
|
|
specific language governing permissions and limitations
|
|
under the License.
|
|
-->
|
|
|
|
# Superset AI Assistant (chat extension)
|
|
|
|
An AI assistant inside the Superset chat host. The extension registers a
|
|
trigger and panel through the public `chat` contribution API (SIP-214) and
|
|
talks to a server-side gateway (`backend/`) that invokes a
|
|
configured model provider and orchestrates the Superset MCP tools under the
|
|
current user's own permissions. Mutating operations require an explicit,
|
|
server-enforced user approval.
|
|
|
|
## Architecture
|
|
|
|
```mermaid
|
|
flowchart LR
|
|
subgraph Browser
|
|
T[Chat trigger] --- P[Chat panel]
|
|
P -->|"fetch + CSRF"| G
|
|
end
|
|
subgraph "Superset backend"
|
|
G["AI gateway REST API<br/>/extensions/enx-dev/ai-chat"] --> O[Orchestrator]
|
|
O --> PR["Provider abstraction<br/>mock / openai_compatible / anthropic"]
|
|
O --> A["Approval store<br/>(key-value, single-use)"]
|
|
O --> B["MCP bridge<br/>(allowlist + classification)"]
|
|
B --> M["MCP service tools<br/>(RBAC under current user)"]
|
|
end
|
|
PR -->|HTTPS| LLM["Model provider API<br/>(server-side key)"]
|
|
```
|
|
|
|
- **Frontend** (`frontend/`): conversation UI, tool-activity cards, approval
|
|
cards, page-context awareness (`navigation` API), file attachments, local
|
|
persistence, retry and cancellation. It holds no secrets and performs no
|
|
Superset operations itself.
|
|
|
|
- **Gateway** (`backend/`): authenticates the session user (CSRF
|
|
enforced), validates payloads, calls the provider, executes allowlisted
|
|
MCP tools in-process through the in-memory FastMCP client (full RBAC
|
|
middleware chain), enforces approvals, caps sizes, sanitizes errors.
|
|
|
|
- **Protocol**: one POST per turn returning an ordered list of typed events
|
|
(`message.completed`, `tool.running`, `tool.completed`, `tool.failed`,
|
|
`tool.approval_required`, `tool.rejected`, `request.completed`,
|
|
`request.failed`). The event vocabulary is transport-agnostic so a future
|
|
SSE transport can stream the same events; see Limitations.
|
|
|
|
## Registration
|
|
|
|
The entry point (`frontend/src/index.tsx`) registers as a module-level side
|
|
effect:
|
|
|
|
```tsx
|
|
import { chat } from "@apache-superset/core";
|
|
chat.registerChat({ id: "enx-dev.ai-chat", name: "…" }, ChatTrigger, ChatPanel);
|
|
```
|
|
|
|
No manifest contribution entry is required for chat.
|
|
|
|
The backend registers the same way, by import side effect. The host imports
|
|
`backend/src/enx_dev/ai_chat/entrypoint.py` — the conventional path
|
|
derived from the publisher and name in `extension.json` — and the `@api`
|
|
decorator on `AiChatRestApi` mounts the routes under
|
|
`/extensions/enx-dev/ai-chat/` as the class is created.
|
|
|
|
```
|
|
extension.json publisher, name, version, license
|
|
frontend/src/index.tsx frontend entry point (registers the chat)
|
|
backend/pyproject.toml Python package metadata and build includes
|
|
backend/src/enx_dev/ai_chat/entrypoint.py
|
|
backend entry point (registers the API)
|
|
backend/tests/ pytest suite, run against a Superset checkout
|
|
```
|
|
|
|
## Enabling the feature
|
|
|
|
Four independent switches, all server-side:
|
|
|
|
1. **Extension framework**: `FEATURE_FLAGS = {"ENABLE_EXTENSIONS": True}`.
|
|
2. **Extension loading**: add this directory to `LOCAL_EXTENSIONS` (after
|
|
building) or install the packaged `.supx` via `EXTENSIONS_PATH`.
|
|
3. **AI chat gateway**: `AI_CHAT_CONFIG = {"ENABLED": True, ...}`.
|
|
4. **MCP tools**: install the `fastmcp` extra
|
|
(`pip install apache_superset[fastmcp]`); without it the assistant is
|
|
chat-only.
|
|
|
|
When configuration is incomplete the panel shows an administrator-friendly
|
|
disabled state; no secrets are ever included in any response.
|
|
|
|
## Provider configuration
|
|
|
|
Everything lives in `AI_CHAT_CONFIG` in `superset_config.py`. Superset
|
|
itself carries no default for it; whatever you set is merged over the
|
|
defaults the extension ships in
|
|
`backend/src/enx_dev/ai_chat/settings.py`, so naming only the keys
|
|
you care about leaves the curated tool allowlist and the size limits intact.
|
|
The provider API key is read at request time from the environment variable
|
|
named by `API_KEY_ENV_VAR` — it is never stored in config, never logged, and
|
|
never sent to the browser.
|
|
|
|
### Mock (development and tests — no credentials)
|
|
|
|
```python
|
|
AI_CHAT_CONFIG = {
|
|
"ENABLED": True,
|
|
"PROVIDER": "mock",
|
|
}
|
|
```
|
|
|
|
The mock provider is deterministic: `list dashboards` runs a read-only tool,
|
|
`delete dashboard <id>` proposes a destructive tool (approval flow),
|
|
`run sql: <query> on database <id>` proposes SQL execution, and anything
|
|
else returns a help message.
|
|
|
|
### OpenAI-compatible endpoint
|
|
|
|
```python
|
|
AI_CHAT_CONFIG = {
|
|
"ENABLED": True,
|
|
"PROVIDER": "openai_compatible",
|
|
"MODEL": "gpt-4o-mini",
|
|
"API_KEY_ENV_VAR": "OPENAI_API_KEY", # export OPENAI_API_KEY=<your-key>
|
|
# Optional: any /chat/completions-compatible server (vLLM, llama.cpp,
|
|
# OpenRouter, an internal gateway). Operator-configured only.
|
|
# "BASE_URL": "https://internal-llm.example.com/v1",
|
|
}
|
|
```
|
|
|
|
### Anthropic
|
|
|
|
```python
|
|
AI_CHAT_CONFIG = {
|
|
"ENABLED": True,
|
|
"PROVIDER": "anthropic",
|
|
"MODEL": "claude-sonnet-4-5",
|
|
"API_KEY_ENV_VAR": "ANTHROPIC_API_KEY", # export ANTHROPIC_API_KEY=<your-key>
|
|
}
|
|
```
|
|
|
|
> **About subscriptions vs API access**: a ChatGPT Plus subscription does
|
|
> not include OpenAI API usage, and a Claude consumer subscription does not
|
|
> include Anthropic API usage. Production use requires a separately
|
|
> provisioned API credential (or a compatible internally hosted model
|
|
> endpoint). The deterministic mock provider requires no credential at all.
|
|
|
|
### Why keys must stay server-side
|
|
|
|
A key shipped to the browser is readable by every user of the page (and by
|
|
any injected content). The gateway therefore performs all provider calls
|
|
server-side; the browser only ever talks to Superset with its session
|
|
cookie and CSRF token. Do not proxy or embed provider keys client-side.
|
|
|
|
## MCP integration and tool classification
|
|
|
|
The gateway lists tools through the in-memory FastMCP client, so the MCP
|
|
service's own authentication, RBAC and visibility filtering all apply under
|
|
the requesting user. On top of that:
|
|
|
|
- `ALLOWED_MCP_TOOLS` is an explicit allowlist — the model never sees tools
|
|
outside it. An empty list disables tool use.
|
|
|
|
- Every tool is classified from its declared `ToolAnnotations`:
|
|
`readOnlyHint=True` → **read-only** (executes immediately);
|
|
`destructiveHint=True` → **destructive** (always requires approval);
|
|
`readOnlyHint=False` → **mutating** (requires approval unless the
|
|
operator disables `REQUIRE_APPROVAL_FOR_MUTATIONS`); missing/unknown
|
|
annotations → **unknown**, treated like destructive and never
|
|
auto-executed.
|
|
|
|
- SQL execution goes through the MCP `execute_sql` tool, which fail-closes
|
|
on unparseable SQL, blocks destructive DDL unconditionally and leaves DML
|
|
to the per-database `allow_dml` flag — classification by code, not by the
|
|
model.
|
|
|
|
**Adding a tool**: append its name to `ALLOWED_MCP_TOOLS`. Its class is
|
|
derived automatically from its annotations; if it has none it will require
|
|
approval every time until annotated. Optional presentation-only warnings can
|
|
be added in `backend/src/enx_dev/ai_chat/classification.py::TOOL_APPROVAL_WARNINGS`.
|
|
|
|
## Approval behavior
|
|
|
|
Before any mutating/destructive call, the gateway creates a single-use
|
|
approval record in the metadata database bound to the user id, conversation
|
|
id, tool name and a SHA-256 hash of the canonicalized arguments, with a TTL
|
|
(`APPROVAL_TTL_SECONDS`, default 5 minutes). The panel shows the exact
|
|
action, target arguments, classification, reversibility hint and warnings
|
|
with Approve/Reject buttons.
|
|
|
|
- Approving sends the approval id; the server re-validates every bound
|
|
property and consumes the record atomically before executing. Changing
|
|
the arguments, tool, user or conversation invalidates it; replay after
|
|
use fails.
|
|
|
|
- Rejecting burns the approval and returns a structured rejection to the
|
|
model, which responds without executing.
|
|
|
|
- Approval is enforced server-side; nothing the model, the page content or
|
|
the client sends can bypass it.
|
|
|
|
## Security model
|
|
|
|
- Session authentication + CSRF on every route; a dedicated `can read on
|
|
AiChat` permission lets operators grant the assistant per role.
|
|
|
|
- All Superset operations run through MCP tools under the requesting user —
|
|
the gateway adds no privileged path. If `MCP_DEV_USERNAME` is set to a
|
|
different user than the session user, tool execution fails closed
|
|
(identity alignment guard).
|
|
|
|
- Prompt-injection defense in depth: trusted system prompt kept separate
|
|
from all retrieved data; MCP wraps user-authored strings in
|
|
`<UNTRUSTED-CONTENT>` tags; tool output is size-capped and carried only
|
|
as tool-role data; the tool allowlist, classification and approvals are
|
|
code-enforced; no shell/filesystem/URL-fetch/code-execution tools exist.
|
|
|
|
- The frontend renders Markdown through a minimal React-element renderer:
|
|
no raw HTML, no `dangerouslySetInnerHTML`, unsafe link schemes refused.
|
|
|
|
- Attached files are read in the browser and travel inside the user turn as
|
|
delimited `<ATTACHED-FILE>` blocks, which the system prompt declares to be
|
|
reference data and never instructions — including any text written inside
|
|
an attached image. Block markers are stripped from the file text so a file
|
|
cannot close its own block, file names are stripped of quotes and angle
|
|
brackets, and each attachment is capped (see Limitations). Images are
|
|
accepted only as base64 with an allowlisted media type, are attached to
|
|
user turns only (the gateway drops any others), and are bounded per image
|
|
and per request. Nothing is uploaded or stored server-side.
|
|
|
|
- Errors returned to the browser are sanitized (no tracebacks, no provider
|
|
response bodies); secret-looking argument values are redacted in events
|
|
and logs.
|
|
|
|
## Development
|
|
|
|
```bash
|
|
# Frontend: install, test, typecheck, build
|
|
cd extensions/ai-chat/frontend
|
|
npm install
|
|
npm test # jest
|
|
npm run type # tsc --noEmit over src + tests
|
|
npm run build # webpack production build into dist/
|
|
|
|
# Backend tests (from the repo root, in the Superset venv)
|
|
pytest extensions/ai-chat/backend/tests/
|
|
```
|
|
|
|
To load the extension locally, run `superset-extensions build` in this
|
|
directory (produces `dist/manifest.json` + `dist/frontend/dist/*`) and add
|
|
the directory to `LOCAL_EXTENSIONS`. `superset-extensions bundle` packages
|
|
it as a `.supx` for `EXTENSIONS_PATH`-based deployments.
|
|
|
|
### Manual testing steps
|
|
|
|
1. `FEATURE_FLAGS["ENABLE_EXTENSIONS"] = True`, `AI_CHAT_CONFIG["ENABLED"] =
|
|
True` with the mock provider, and this extension in `LOCAL_EXTENSIONS`.
|
|
2. Log in; the robot trigger appears bottom-right. Click it — the panel
|
|
opens in floating mode; the header toggle docks it as a sidebar.
|
|
3. Send `list dashboards` — a read-only tool card runs and the mock
|
|
summarizes the result.
|
|
4. Send `delete dashboard <id>` — an approval card appears; Reject and
|
|
verify nothing was deleted; repeat and Approve to execute.
|
|
5. Navigate between pages — the header context tag updates and a note marks
|
|
the transition; the conversation is retained.
|
|
6. Set `"PROVIDER": "openai_compatible"` without a key — the panel shows the
|
|
misconfigured state and the input is disabled.
|
|
|
|
### Adding another provider
|
|
|
|
Implement `BaseChatProvider` (`backend/src/enx_dev/ai_chat/providers/base.py`) — one
|
|
async `complete(messages, tools) -> ProviderResult` translating the neutral
|
|
message format to your wire format — and register the class in
|
|
`PROVIDERS` (`backend/src/enx_dev/ai_chat/providers/__init__.py`). Raise
|
|
`AiChatProviderError` with browser-safe messages on failure. The UI is
|
|
provider-agnostic and needs no changes.
|
|
|
|
## Limitations
|
|
|
|
- **One host import**: the MCP bridge imports `superset.mcp_service.app` to
|
|
reach the server the host already runs. `apache-superset-core` exposes
|
|
decorators for contributing MCP tools but no client for calling them, and
|
|
a second server would mean a second copy of the middleware chain the
|
|
bridge exists to go through. The import is lazy and guarded, so a host
|
|
that moves it costs tool use rather than the whole assistant. Everything
|
|
else in `backend/` goes through `apache-superset-core`.
|
|
|
|
- **Unreleased frontend APIs**: `chat` and `navigation` are not in
|
|
`@apache-superset/core` 0.1.0 on npm, so the frontend builds against a
|
|
Superset checkout and needs a host recent enough to provide the chat
|
|
contribution point.
|
|
|
|
- **No token streaming**: the backend has no SSE convention; each turn
|
|
returns its events at once. The typed event protocol is
|
|
transport-agnostic so streaming can be added without changing the UI
|
|
event model.
|
|
|
|
- **Cancellation is client-side**: cancelling aborts the fetch and recovers
|
|
the UI; the in-flight server work completes and is discarded.
|
|
|
|
- **Page context is a hint**: the public navigation API exposes only the
|
|
page type; the dashboard id is parsed from the URL and verified via tools.
|
|
|
|
- **No multi-step atomicity**: dashboard/chart creation reports per-step
|
|
results honestly; there is no transaction across MCP tools.
|
|
|
|
- **History replay**: the server is stateless; the client replays trimmed
|
|
history, so very long conversations lose their oldest turns and tool
|
|
results are replayed as bounded excerpts.
|
|
|
|
- **Conversation storage** is browser-local (namespaced localStorage via
|
|
the extension storage API), capped in size, cleared by "New conversation".
|
|
|
|
- **Attachments**: up to 3 per message. Text files (`.csv`, `.json`, `.log`,
|
|
`.md`, `.py`, `.sql`, `.tsv`, `.txt`, `.yaml`, `.yml`) are capped at 1 MB
|
|
and 20 000 characters each, longer ones truncated with a note the model is
|
|
told to surface; there is no PDF or spreadsheet parsing. Images (`.png`,
|
|
`.jpg`, `.jpeg`, `.gif`, `.webp`) are capped at 8 MB and require a
|
|
vision-capable configured model — a text-only model rejects the request
|
|
and the error surfaces with a retry. An image whose encoded payload
|
|
exceeds \~300 KB is re-encoded in the browser with its longest edge bounded
|
|
to 1400 px; smaller ones are sent as they are, whatever their pixel
|
|
dimensions, and re-encoding is best-effort (the original is kept if it
|
|
fails or does not come out smaller).
|
|
|
|
- **Dropped objects**: dragging a chart title, dashboard card or dataset
|
|
link into the composer attaches it as context (up to 5). Identity comes
|
|
from the dropped URL, so no host-side drag source is needed, and only
|
|
same-origin URLs are read. They stay attached until removed with the chip's
|
|
X or until the conversation is cleared, and travel as page context on every
|
|
turn — so they are hints the assistant verifies with a tool, exactly like
|
|
the page's own resource. Each message records the objects it was sent with
|
|
above the question, as links back to them: a snapshot of the context that
|
|
turn carried, kept even after the object is detached.
|
|
|
|
- **Attachments live in the turn**: file text counts toward
|
|
`MAX_INPUT_CHARS`, and both files and images leave context once history
|
|
trimming reaches that message. Images are stripped before the conversation
|
|
is written to browser storage (a screenshot dwarfs the persistence
|
|
budget), so a reloaded conversation keeps the message and the attachment's
|
|
name but not the image itself.
|