Skip to content

feat(appkit): add Genie plugin for AI/BI space integration#108

Merged
calvarjorge merged 19 commits intomainfrom
feat/genie-plugin
Mar 2, 2026
Merged

feat(appkit): add Genie plugin for AI/BI space integration#108
calvarjorge merged 19 commits intomainfrom
feat/genie-plugin

Conversation

@calvarjorge
Copy link
Contributor

@calvarjorge calvarjorge commented Feb 17, 2026

Summary

  • Add new Genie plugin
  • Single SSE endpoint (POST /api/genie/:alias/messages) handles both new and follow-up conversations
  • Space alias abstraction: users configure URL aliases, which map to a Genie Space ID in the backend.
  • Always executes as user (OBO) via asUser(req)
  • SSE event flow: message_startstatus (×N) → message_resultquery_result (×N per query attachment)
  • Configurable timeout (default 2min, 0 for indefinite)
  • No cache/retry (chat is stateful and non-idempotent)
  • Programmatic sendMessage API exposed via exports()
  • Conversation history SSE endpoint (GET /api/genie/:alias/conversations/:conversationId) replays full conversation using the same event types, enabling page refresh without losing chat state
    • includeQueryResults query param (default true) controls whether query result data is fetched
    • Messages streamed first, then query results fetched in parallel
    • Server-side pagination with 200-message safety cap
    • Programmatic getConversation API exposed via exports()
  • Fix pre-existing ajv type resolution issue in shared package

Add a new Genie plugin that provides an opinionated chat API powered by
Databricks AI/BI Genie spaces. Users configure named space aliases in
plugin config, and the backend resolves aliases to actual space IDs.

Key design:
- Single SSE endpoint: POST /api/genie/:alias/messages
- Always executes as user (OBO) via asUser(req)
- SSE event flow: message_start → status (×N) → message_result → query_result (×N)
- Space alias abstraction keeps space IDs out of URLs and client code
- No cache/retry (chat is stateful and non-idempotent)
- Configurable timeout (default 2min, 0 for indefinite)

Also fixes pre-existing ajv type resolution issue in shared package
where pnpm hoisting caused TypeScript to resolve ajv@6 types instead
of the declared ajv@8 dependency.

Signed-off-by: Jorge Calvar <jorge.calvar@databricks.com>
Cast error entries to `any` when mapping validation errors so the code
works regardless of which ajv version TypeScript resolves (v6 has
`dataPath`, v8 has `instancePath`).

Signed-off-by: Jorge Calvar <jorge.calvar@databricks.com>
Signed-off-by: Jorge Calvar <jorge.calvar@databricks.com>
Add genie manifest.json to tsdown copy config so it's available at
runtime when loading from the built dist/ output.

Signed-off-by: Jorge Calvar <jorge.calvar@databricks.com>
@calvarjorge calvarjorge marked this pull request as draft February 18, 2026 08:50
SSE endpoint (GET /api/genie/:alias/conversations/:conversationId)
that replays full conversation history reusing existing event types,
enabling page refresh without losing chat state.

Signed-off-by: Jorge Calvar <jorge.calvar@databricks.com>
@calvarjorge calvarjorge marked this pull request as ready for review February 18, 2026 16:38
Use top-level @databricks/sdk-experimental export for Time/TimeUnits
and import createLogger from logging index instead of direct file path.

Signed-off-by: Jorge Calvar <jorge.calvar@databricks.com>
Signed-off-by: Jorge Calvar <jorge.calvar@databricks.com>

# Conflicts:
#	packages/appkit/src/index.ts
#	packages/shared/src/cli/commands/plugin/sync/sync.ts
Copy link
Member

@pkosiec pkosiec left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice work!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should add

/**
 * @internal
 */

comment to the genie const to avoid generating this

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to document the plugin in the plugins.md file in our docs, so that users know how to use it 👍

}
: undefined,
text: att.text ? { content: att.text.content } : undefined,
suggestedQuestions: att.suggested_questions?.questions,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do you have a screenshot of how the suggested questions look like? (I mean the user point of view, so it's probably a question to this PR: #116

return;
}

const includeQueryResults = req.query.includeQueryResults !== "false";
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what's the use case of not querying the attachments? I'm just wondering if we need that?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the query results are used for the Genie response (so can be found there indirectly as well), maybe the user don't want the actual table data that was used to provide the response

…ugins.md

Add @internal JSDoc to genie const to exclude it from generated API
docs. Add Genie plugin section to plugins.md covering configuration,
endpoints, SSE events, and programmatic usage.

Signed-off-by: Jorge Calvar <jorge.calvar@databricks.com>
Signed-off-by: Jorge Calvar <jorge.calvar@databricks.com>
Comment on lines +145 to +195
const statusQueue: string[] = [];
let notifyGenerator: () => void = () => {};
let waiterDone = false;

const onProgress = async (message: GenieMessage): Promise<void> => {
if (message.status) {
statusQueue.push(message.status);
notifyGenerator();
}
};

let resultConversationId = "";
let resultMessageId = "";
let completedMessage: GenieMessage =
undefined as unknown as GenieMessage;
let waiterError: Error | null = null;

// Launch Genie API call
const waiterPromise = (async () => {
let messageWaiter: CreateMessageWaiter;

if (conversationId) {
messageWaiter = await workspaceClient.genie.createMessage({
space_id: spaceId,
conversation_id: conversationId,
content,
});
resultConversationId = conversationId;
} else {
const startWaiter: StartConversationWaiter =
await workspaceClient.genie.startConversation({
space_id: spaceId,
content,
});
resultConversationId = startWaiter.conversation_id;
resultMessageId = startWaiter.message_id;
messageWaiter = startWaiter as unknown as CreateMessageWaiter;
}

const result = await messageWaiter.wait({ onProgress });
completedMessage = result;
resultMessageId = result.message_id;
return result;
})()
.catch((err: Error) => {
waiterError = err;
})
.finally(() => {
waiterDone = true;
notifyGenerator();
});
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

as talked in private chat we will make a wrapper around this to make the stream callback much easier to read

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i implemented the wrapper as we discussed, it makes the code easier to follow. the length of the func is reduced, although i don't think as much as we expected 😅


export interface IGenieConfig extends BasePluginConfig {
/** Map of alias → Genie Space ID */
spaces: Record<string, string>;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also as talked on zoom, we should have a default spaces defined so we can just put the genie plugin like genie() without having to configure it

"permission": "CAN_RUN",
"fields": {
"id": {
"env": "DATABRICKS_GENIE_SPACE_ID",
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this env var and the env var we use as default in the default space should be the same

Signed-off-by: Jorge Calvar <jorge.calvar@databricks.com>
Pass deterministic streamId values to StreamManager so clients can
reconnect and replay missed events using Last-Event-ID. Also adds a
default bufferSize of 100 to the Genie stream settings.

Signed-off-by: Jorge Calvar <jorge.calvar@databricks.com>
… var

Make the `spaces` config optional. When omitted, fall back to
{ default: DATABRICKS_GENIE_SPACE_ID }. If the env var is also unset,
routes gracefully 404 instead of crashing at startup.

Signed-off-by: Jorge Calvar <jorge.calvar@databricks.com>
Replace manual concurrency code (statusQueue, notifyGenerator,
waiterDone, waiterError, IIFE promise chain) with a reusable
pollWaiter async generator that bridges callback-based
waiter.wait({ onProgress }) into a for-await-of loop.

Signed-off-by: Jorge Calvar <jorge.calvar@databricks.com>
Replace conversation-derived stream IDs with a client-provided
?requestId=<uuid> query param, enabling reliable SSE reconnection.
Falls back to crypto.randomUUID() when not provided.

Signed-off-by: Jorge Calvar <jorge.calvar@databricks.com>
calvarjorge and others added 3 commits March 2, 2026 10:16
* feat(appkit-ui): add Genie chat React components and dev-playground demo

Add plug-and-play React components for Genie AI/BI chat:

- useGenieChat hook: manages SSE streaming, conversation persistence
  via URL params, and history replay on page refresh
- GenieChat: all-in-one component wiring hook + UI
- GenieChatMessage: renders messages with markdown (via marked),
  avatars, and collapsible SQL query attachments
- GenieChatMessageList: scrollable message list with auto-scroll,
  loading skeletons, and streaming status indicator
- GenieChatInput: textarea with Enter-to-send and auto-resize

Also adds Genie demo page to dev-playground at /genie and fixes
conversation history ordering in the backend (reverse to chronological).

Signed-off-by: Jorge Calvar <jorge.calvar@databricks.com>

* fix(appkit-ui): address Genie chat UI review feedback

- Rename GENIE_SPACE_ID to DATABRICKS_GENIE_SPACE_ID in env and code
- Hide textarea scrollbar; only show overflow-y when content exceeds max height
- Skip rendering empty assistant bubbles during loading, show only the spinner
- Remove shadow from nested SQL query cards to fix corner shadow artifacts
- Move "New conversation" button to top-right of chat widget

Signed-off-by: Jorge Calvar <jorge.calvar@databricks.com>

* docs: add Genie component documentation with examples

Extend the doc generator to scan genie, multi-genie, and chat component
directories. Add JSDoc descriptions to all Genie components and props,
create usage examples for GenieChat and MultiGenieChat, and generate
8 new doc pages under a "Genie components" sidebar category.

Signed-off-by: Jorge Calvar <jorge.calvar@databricks.com>

* feat(genie): send requestId query param from frontend for SSE reconnection

Generate a UUID per request in useGenieChat and pass it as ?requestId
to the sendMessage and loadHistory SSE endpoints. This allows the server
to use a stable streamId for reconnection and missed-event replay.

Signed-off-by: Jorge Calvar <jorge.calvar@databricks.com>

---------

Signed-off-by: Jorge Calvar <jorge.calvar@databricks.com>
* feat(appkit-ui): add Genie chat React components and dev-playground demo

Add plug-and-play React components for Genie AI/BI chat:

- useGenieChat hook: manages SSE streaming, conversation persistence
  via URL params, and history replay on page refresh
- GenieChat: all-in-one component wiring hook + UI
- GenieChatMessage: renders messages with markdown (via marked),
  avatars, and collapsible SQL query attachments
- GenieChatMessageList: scrollable message list with auto-scroll,
  loading skeletons, and streaming status indicator
- GenieChatInput: textarea with Enter-to-send and auto-resize

Also adds Genie demo page to dev-playground at /genie and fixes
conversation history ordering in the backend (reverse to chronological).

Signed-off-by: Jorge Calvar <jorge.calvar@databricks.com>

* fix(appkit-ui): address Genie chat UI review feedback

- Rename GENIE_SPACE_ID to DATABRICKS_GENIE_SPACE_ID in env and code
- Hide textarea scrollbar; only show overflow-y when content exceeds max height
- Skip rendering empty assistant bubbles during loading, show only the spinner
- Remove shadow from nested SQL query cards to fix corner shadow artifacts
- Move "New conversation" button to top-right of chat widget

Signed-off-by: Jorge Calvar <jorge.calvar@databricks.com>

* docs: add Genie component documentation with examples

Extend the doc generator to scan genie, multi-genie, and chat component
directories. Add JSDoc descriptions to all Genie components and props,
create usage examples for GenieChat and MultiGenieChat, and generate
8 new doc pages under a "Genie components" sidebar category.

Signed-off-by: Jorge Calvar <jorge.calvar@databricks.com>

* feat(genie): send requestId query param from frontend for SSE reconnection

Generate a UUID per request in useGenieChat and pass it as ?requestId
to the sendMessage and loadHistory SSE endpoints. This allows the server
to use a stable streamId for reconnection and missed-event replay.

Signed-off-by: Jorge Calvar <jorge.calvar@databricks.com>

* chore: genie connector

* refactor(genie): replace sendMessage with streaming implementation

The old non-streaming sendMessage (returning Promise<GenieMessageResponse>)
is replaced by the streaming version (returning AsyncGenerator<GenieStreamEvent>).

Signed-off-by: Jorge Calvar <jorge.calvar@databricks.com>

---------

Signed-off-by: Jorge Calvar <jorge.calvar@databricks.com>
Co-authored-by: MarioCadenas <MarioCadenas@users.noreply.github.com>
Copy link
Collaborator

@MarioCadenas MarioCadenas left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's move the duplicated types from UI and backend into the shared folder and we are good to go!! 🚀

@@ -0,0 +1,129 @@
import { describe, expect, test, vi } from "vitest";
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe we should move this to the connector folder so it lives with the file its testing?

Move GenieStreamEvent, GenieMessageResponse, and GenieAttachmentResponse
to the shared package to eliminate duplication between appkit and
appkit-ui, ensuring both sides stay in sync.

Signed-off-by: Jorge Calvar <jorge.calvar@databricks.com>
Signed-off-by: Jorge Calvar <jorge.calvar@databricks.com>
Copy link
Collaborator

@MarioCadenas MarioCadenas left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Amazing job @calvarjorge ! Let's ship it! 🚀

@calvarjorge calvarjorge merged commit c3581d5 into main Mar 2, 2026
6 checks passed
@calvarjorge calvarjorge deleted the feat/genie-plugin branch March 2, 2026 17:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants