feat(appkit): add Genie plugin for AI/BI space integration#108
feat(appkit): add Genie plugin for AI/BI space integration#108calvarjorge wants to merge 13 commits intomainfrom
Conversation
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>
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>
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
There was a problem hiding this comment.
we should add
/**
* @internal
*/comment to the genie const to avoid generating this
There was a problem hiding this comment.
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, |
There was a problem hiding this comment.
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"; |
There was a problem hiding this comment.
what's the use case of not querying the attachments? I'm just wondering if we need that?
There was a problem hiding this comment.
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>
| }); | ||
| } | ||
|
|
||
| async _handleSendMessage( |
There was a problem hiding this comment.
we are not handling reconnections, or at least I don't see it? am I missing something?
| 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(); | ||
| }); |
There was a problem hiding this comment.
as talked in private chat we will make a wrapper around this to make the stream callback much easier to read
There was a problem hiding this comment.
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>; |
There was a problem hiding this comment.
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", |
There was a problem hiding this comment.
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>
Summary
POST /api/genie/:alias/messages) handles both new and follow-up conversationsasUser(req)message_start→status(×N) →message_result→query_result(×N per query attachment)0for indefinite)sendMessageAPI exposed viaexports()GET /api/genie/:alias/conversations/:conversationId) replays full conversation using the same event types, enabling page refresh without losing chat stateincludeQueryResultsquery param (defaulttrue) controls whether query result data is fetchedgetConversationAPI exposed viaexports()