@dbx-tools/genie 0.1.112 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +115 -155
- package/index.ts +8 -0
- package/package.json +48 -16
- package/src/chat.ts +324 -0
- package/src/space.ts +111 -0
- package/tsconfig.json +41 -0
- package/dist/index.d.ts +0 -157
- package/dist/index.js +0 -307
package/README.md
CHANGED
|
@@ -1,202 +1,162 @@
|
|
|
1
1
|
# @dbx-tools/genie
|
|
2
2
|
|
|
3
|
-
Server-side Genie chat drivers.
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
`
|
|
3
|
+
Server-side Databricks Genie chat drivers.
|
|
4
|
+
|
|
5
|
+
Import this package when Node or AppKit backend code needs to run one turn
|
|
6
|
+
against a Genie space and consume either raw Genie message snapshots or a typed
|
|
7
|
+
event stream. It preserves AppKit OBO auth when called inside an AppKit request,
|
|
8
|
+
falls back to the Databricks SDK default auth outside AppKit, and supports
|
|
9
|
+
caller-provided cancellation.
|
|
10
|
+
|
|
11
|
+
Pure Genie schemas and event detector helpers live in
|
|
12
|
+
[`@dbx-tools/shared-genie`](../../shared/genie).
|
|
13
|
+
|
|
14
|
+
Key features:
|
|
15
|
+
|
|
16
|
+
- Starts new Genie conversations or continues an existing `conversationId`.
|
|
17
|
+
- Polls Databricks Genie until terminal status while filtering unchanged
|
|
18
|
+
snapshots.
|
|
19
|
+
- Converts raw Genie messages into semantic events for thinking text, generated
|
|
20
|
+
SQL, row counts, final results, and errors.
|
|
21
|
+
- Preserves AppKit OBO auth when called during an AppKit request, but also works
|
|
22
|
+
from standalone scripts with normal Databricks SDK auth.
|
|
23
|
+
- Accepts SDK `Context` or web `AbortSignal` cancellation for route handlers and
|
|
24
|
+
CLI tools.
|
|
25
|
+
- Fetches Genie space metadata and starter questions for UI suggestions.
|
|
26
|
+
|
|
27
|
+
## Why Not Just AppKit Genie?
|
|
28
|
+
|
|
29
|
+
Native AppKit's Genie plugin is the right choice for a standalone Genie chat
|
|
30
|
+
experience: it provides named space aliases, SSE status updates, conversation
|
|
31
|
+
history replay, query result fetching, OBO execution, and the AppKit UI
|
|
32
|
+
`GenieChat` component.
|
|
33
|
+
|
|
34
|
+
Use this package when Genie is one capability inside a larger agent or custom
|
|
35
|
+
backend:
|
|
36
|
+
|
|
37
|
+
- You want a low-level async iterator rather than an AppKit HTTP route.
|
|
38
|
+
- You want raw message snapshots or a normalized event stream that can be fed
|
|
39
|
+
into Mastra writer events, logs, tests, or custom SSE endpoints.
|
|
40
|
+
- You need to diff snapshots and emit only newly observed thinking, SQL, rows,
|
|
41
|
+
result, and error events.
|
|
42
|
+
- You want to combine Genie answers with agent-side chart planning, statement
|
|
43
|
+
row fetches, or durable thread storage owned elsewhere.
|
|
44
|
+
- You need the same driver to work inside AppKit with OBO auth and outside
|
|
45
|
+
AppKit from scripts using normal Databricks SDK auth.
|
|
46
|
+
|
|
47
|
+
## Stream Semantic Events
|
|
13
48
|
|
|
14
49
|
```ts
|
|
15
|
-
import {
|
|
50
|
+
import { chat } from "@dbx-tools/genie";
|
|
16
51
|
|
|
17
|
-
for await (const event of genieEventChat(spaceId, "Top
|
|
52
|
+
for await (const event of chat.genieEventChat(spaceId, "Top stores by revenue?")) {
|
|
18
53
|
switch (event.type) {
|
|
19
54
|
case "thinking":
|
|
20
|
-
console.log(
|
|
55
|
+
console.log(event.thought_type, event.text);
|
|
21
56
|
break;
|
|
22
57
|
case "query":
|
|
23
|
-
console.log(
|
|
58
|
+
console.log(event.sql);
|
|
59
|
+
break;
|
|
60
|
+
case "rows":
|
|
61
|
+
console.log(event.row_count);
|
|
24
62
|
break;
|
|
25
63
|
case "result":
|
|
26
|
-
console.log(
|
|
64
|
+
console.log(event.status);
|
|
27
65
|
break;
|
|
28
66
|
}
|
|
29
67
|
}
|
|
30
68
|
```
|
|
31
69
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
detectors, and `eventsFromMessage` all live there and are also
|
|
37
|
-
re-exported from `@dbx-tools/genie` for server-side convenience.
|
|
38
|
-
|
|
39
|
-
## `genieChat` - low-level snapshot stream
|
|
70
|
+
`chat.genieEventChat()` wraps the lower-level snapshot stream and yields a
|
|
71
|
+
`GenieChatEvent` union. Use it for SSE streams, log pipelines, and tool writer
|
|
72
|
+
events where consumers care about progress and SQL, not just the terminal
|
|
73
|
+
message.
|
|
40
74
|
|
|
41
|
-
|
|
42
|
-
when you want the raw stream (e.g. to drive a custom UI off the wire
|
|
43
|
-
shape, or to derive events yourself with the detectors in
|
|
44
|
-
`@dbx-tools/genie-shared`).
|
|
75
|
+
## Stream Raw Message Snapshots
|
|
45
76
|
|
|
46
77
|
```ts
|
|
47
|
-
import {
|
|
78
|
+
import { chat } from "@dbx-tools/genie";
|
|
48
79
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
render(m);
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
// Multi-turn: caller threads the conversation id.
|
|
55
|
-
let conversationId: string | undefined;
|
|
56
|
-
for (const question of questions) {
|
|
57
|
-
for await (const m of genieChat(spaceId, question, { conversationId })) {
|
|
58
|
-
conversationId = m.conversation_id ?? conversationId;
|
|
59
|
-
render(m);
|
|
60
|
-
}
|
|
80
|
+
for await (const message of chat.genieChat(spaceId, "Top stores by revenue?")) {
|
|
81
|
+
renderSnapshot(message);
|
|
61
82
|
}
|
|
62
83
|
```
|
|
63
84
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
via `client.genie.createMessage`.
|
|
71
|
-
- In both cases, after the create/start the driver polls
|
|
72
|
-
`client.genie.getMessage` every `options.pollIntervalMs` (default
|
|
73
|
-
500ms) until the message hits a terminal status
|
|
74
|
-
(`COMPLETED` / `FAILED` / `CANCELLED`), then yields the terminal
|
|
75
|
-
snapshot and returns.
|
|
76
|
-
|
|
77
|
-
Identical consecutive snapshots are filtered out (deep equal)
|
|
78
|
-
because Genie often returns the exact same payload twice during
|
|
79
|
-
quiet periods.
|
|
80
|
-
|
|
81
|
-
## `genieEventChat` - high-level typed events
|
|
82
|
-
|
|
83
|
-
Wraps `genieChat` and yields a `GenieChatEvent` discriminated union.
|
|
84
|
-
Stream order per turn:
|
|
85
|
-
|
|
86
|
-
1. `{ type: "message", message }` - the raw `GenieMessage`, once
|
|
87
|
-
per poll yield.
|
|
88
|
-
2. `{ type: "question", content, message_id, ... }` - fires
|
|
89
|
-
exactly once, on the first `message` yield. Carries the prompt
|
|
90
|
-
text Genie echoed back and the assigned `message_id` so
|
|
91
|
-
subscribers can group everything for one Genie call under that
|
|
92
|
-
one key.
|
|
93
|
-
3. Any of `status` / `attachment` / `thinking` / `text` / `query` /
|
|
94
|
-
`statement` / `rows` / `suggested_questions` the diff against
|
|
95
|
-
the prior snapshot produced.
|
|
96
|
-
4. On the terminal snapshot, `{ type: "result", status, message }`
|
|
97
|
-
as the final yield.
|
|
98
|
-
|
|
99
|
-
See [`@dbx-tools/genie-shared`](../genie-shared) for the full event
|
|
100
|
-
catalogue, field shapes, and the pure detectors that derive each
|
|
101
|
-
event from a snapshot diff.
|
|
102
|
-
|
|
103
|
-
Errors propagate by the generator throwing - there is no `error`
|
|
104
|
-
variant. Wrap the `for await` in `try / catch` if you need to handle
|
|
105
|
-
failures.
|
|
106
|
-
|
|
107
|
-
## Space metadata
|
|
108
|
-
|
|
109
|
-
Beyond the chat drivers, the package exposes two helpers for reading a
|
|
110
|
-
space's definition (both re-exported from the package root):
|
|
111
|
-
|
|
112
|
-
- `getGenieSpace(spaceId, options?)` - fetch a `GenieSpace` via
|
|
113
|
-
`GET /api/2.0/genie/spaces/<id>`. Includes the `serialized_space`
|
|
114
|
-
blob (catalogs, tables, sample questions, prompts) by default; pass
|
|
115
|
-
`{ serialized: false }` for the lighter title/description-only
|
|
116
|
-
payload. Takes the same `workspaceClient` / `context` options as the
|
|
117
|
-
drivers.
|
|
118
|
-
- `genieSampleQuestions(space)` - pull the author-curated starter
|
|
119
|
-
questions out of a space's serialized blob
|
|
120
|
-
(`config.sample_questions[*].question`), order preserved and
|
|
121
|
-
duplicates dropped. Returns `[]` when the space carries no serialized
|
|
122
|
-
blob or no configured questions, so a missing/misconfigured space
|
|
123
|
-
degrades to "no suggestions" rather than throwing.
|
|
85
|
+
`chat.genieChat()` starts a conversation or appends to an existing one, polls
|
|
86
|
+
`client.genie.getMessage`, filters identical consecutive payloads, and stops
|
|
87
|
+
after a terminal status. Use it when you want to run your own diffing or persist
|
|
88
|
+
the raw Genie wire shape.
|
|
89
|
+
|
|
90
|
+
## Continue A Conversation
|
|
124
91
|
|
|
125
92
|
```ts
|
|
126
|
-
|
|
93
|
+
let conversationId: string | undefined;
|
|
127
94
|
|
|
128
|
-
const
|
|
129
|
-
const
|
|
95
|
+
for (const prompt of prompts) {
|
|
96
|
+
for await (const event of chat.genieEventChat(spaceId, prompt, { conversationId })) {
|
|
97
|
+
if ("conversation_id" in event && event.conversation_id) {
|
|
98
|
+
conversationId = event.conversation_id;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
130
102
|
```
|
|
131
103
|
|
|
132
|
-
|
|
104
|
+
The driver does not own multi-turn state. Callers read the conversation id from
|
|
105
|
+
a yielded message/event and pass it into the next turn. That makes the package
|
|
106
|
+
usable in stateless route handlers, durable thread stores, and one-off scripts.
|
|
133
107
|
|
|
134
|
-
|
|
108
|
+
This split is deliberate: the package is a transport/driver layer, not a thread
|
|
109
|
+
store. AppKit-Mastra persists thread state separately and passes the Genie
|
|
110
|
+
conversation id back into this driver when a turn continues.
|
|
135
111
|
|
|
136
|
-
|
|
137
|
-
| ----------------- | --------------------------------------- | ----------------------------------------------------------------------------------------------------------- |
|
|
138
|
-
| `conversationId` | `undefined` | Seed conversation id. When set, this turn appends to the existing conversation via `createMessage`. |
|
|
139
|
-
| `workspaceClient` | resolved (see below) | Explicit `WorkspaceClient`. Defaults to AppKit's per-request client when available; otherwise env-var auth. |
|
|
140
|
-
| `pollIntervalMs` | `500` | Cadence between successive `getMessage` calls. |
|
|
141
|
-
| `context` | new internal `AbortController` per call | External cancellation. Accepts an `AbortSignal` or a fully-built SDK `Context` (`apiUtils.ContextLike`). |
|
|
112
|
+
## Resolve A Workspace Client
|
|
142
113
|
|
|
143
|
-
|
|
114
|
+
```ts
|
|
115
|
+
import { WorkspaceClient } from "@databricks/sdk-experimental";
|
|
116
|
+
import { chat } from "@dbx-tools/genie";
|
|
144
117
|
|
|
145
|
-
|
|
118
|
+
await chat.genieEventChat(spaceId, content, {
|
|
119
|
+
workspaceClient: new WorkspaceClient({ profile: "dev" }),
|
|
120
|
+
});
|
|
121
|
+
```
|
|
146
122
|
|
|
147
|
-
|
|
148
|
-
2. AppKit's per-request execution-context client, when
|
|
149
|
-
`@databricks/appkit` is installed AND we're inside an active
|
|
150
|
-
request scope. OBO auth is preserved automatically.
|
|
151
|
-
3. Fresh `new WorkspaceClient({})` (env-var auth via
|
|
152
|
-
`DATABRICKS_CONFIG_PROFILE` / `DATABRICKS_HOST` /
|
|
153
|
-
`DATABRICKS_TOKEN`).
|
|
123
|
+
Client resolution order:
|
|
154
124
|
|
|
155
|
-
|
|
156
|
-
|
|
125
|
+
1. `options.workspaceClient`;
|
|
126
|
+
2. AppKit execution-context client, when present;
|
|
127
|
+
3. `new WorkspaceClient({})` using normal Databricks SDK auth.
|
|
157
128
|
|
|
158
|
-
|
|
129
|
+
Pass `options.context` as an `AbortSignal` or SDK context to cancel SDK calls and
|
|
130
|
+
the polling sleep.
|
|
159
131
|
|
|
160
|
-
|
|
161
|
-
ties into that controller, so an external abort tears down every
|
|
162
|
-
in-flight SDK call AND the inter-poll sleep. Breaking out of the
|
|
163
|
-
`for await` does the same via the `try / finally`.
|
|
132
|
+
## Read Space Metadata And Starter Questions
|
|
164
133
|
|
|
165
134
|
```ts
|
|
166
|
-
|
|
167
|
-
setTimeout(() => ac.abort(), 30_000); // hard 30s ceiling
|
|
135
|
+
import { space } from "@dbx-tools/genie";
|
|
168
136
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
handle(event);
|
|
172
|
-
}
|
|
173
|
-
} catch (err) {
|
|
174
|
-
if (ac.signal.aborted) console.log("timed out");
|
|
175
|
-
else throw err;
|
|
176
|
-
}
|
|
137
|
+
const genieSpace = await space.getGenieSpace(spaceId);
|
|
138
|
+
const questions = space.genieSampleQuestions(genieSpace);
|
|
177
139
|
```
|
|
178
140
|
|
|
179
|
-
|
|
141
|
+
`space.getGenieSpace()` fetches the space definition, including serialized space
|
|
142
|
+
metadata by default. `space.genieSampleQuestions()` extracts curated starter
|
|
143
|
+
questions and returns `[]` when none are configured.
|
|
180
144
|
|
|
181
|
-
|
|
182
|
-
argv / stdin / a REPL and writes every emitted event to a per-run tmp
|
|
183
|
-
directory so you can inspect deltas after the fact. One subdirectory
|
|
184
|
-
per `GenieChatEvent` variant, plus a `rows-data/` directory for paired
|
|
185
|
-
SQL fetches on terminals that carry a `query_result.statement_id`.
|
|
145
|
+
## Options
|
|
186
146
|
|
|
187
|
-
|
|
188
|
-
# Required env (already in repo .env):
|
|
189
|
-
# DATABRICKS_GENIE_SPACE_ID
|
|
190
|
-
# DATABRICKS_CONFIG_PROFILE (or any SDK auth env)
|
|
147
|
+
`chat.GenieChatOptions` is shared by both drivers:
|
|
191
148
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
149
|
+
- `conversationId` - append to an existing Genie conversation.
|
|
150
|
+
- `workspaceClient` - explicit Databricks SDK client.
|
|
151
|
+
- `pollIntervalMs` - polling cadence, default `500`.
|
|
152
|
+
- `context` - SDK `Context` or `AbortSignal` for cancellation.
|
|
196
153
|
|
|
197
|
-
|
|
198
|
-
the per-run directory is printed at the top.
|
|
154
|
+
## Modules
|
|
199
155
|
|
|
200
|
-
|
|
156
|
+
- `chat` - `genieChat()` raw snapshot stream and `genieEventChat()` typed event
|
|
157
|
+
stream.
|
|
158
|
+
- `space` - `getGenieSpace()` and `genieSampleQuestions()`.
|
|
201
159
|
|
|
202
|
-
|
|
160
|
+
The AppKit-Mastra package builds its Genie tools on top of this driver; see
|
|
161
|
+
[`@dbx-tools/appkit-mastra`](../appkit-mastra) for the agent-level
|
|
162
|
+
workflow.
|
package/index.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// GENERATED by projen watch - DO NOT EDIT.
|
|
2
|
+
// Regenerated from the exporting modules in ./src.
|
|
3
|
+
// Hand edits are overwritten on the next watch; this file is read-only.
|
|
4
|
+
|
|
5
|
+
export * as chat from "./src/chat";
|
|
6
|
+
export * as space from "./src/space";
|
|
7
|
+
export type { GenieChatOptions } from "./src/chat";
|
|
8
|
+
export type { GetGenieSpaceOptions } from "./src/space";
|
package/package.json
CHANGED
|
@@ -1,25 +1,57 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dbx-tools/genie",
|
|
3
|
-
"
|
|
3
|
+
"repository": {
|
|
4
|
+
"type": "git",
|
|
5
|
+
"url": "git+https://github.com/reggie-db/dbx-tools.git",
|
|
6
|
+
"directory": "workspaces/node/genie"
|
|
7
|
+
},
|
|
8
|
+
"devDependencies": {
|
|
9
|
+
"@databricks/appkit": "^0.43.0",
|
|
10
|
+
"@types/node": "^24.6.0",
|
|
11
|
+
"tsx": "^4.23.0",
|
|
12
|
+
"typescript": "^5.9.3"
|
|
13
|
+
},
|
|
14
|
+
"peerDependencies": {
|
|
15
|
+
"@databricks/appkit": "^0.43.0"
|
|
16
|
+
},
|
|
4
17
|
"dependencies": {
|
|
5
18
|
"@databricks/sdk-experimental": "^0.17.0",
|
|
6
|
-
"@dbx-tools/
|
|
7
|
-
"@dbx-tools/shared": "0.1
|
|
19
|
+
"@dbx-tools/appkit": "0.3.1",
|
|
20
|
+
"@dbx-tools/shared-genie": "0.3.1",
|
|
21
|
+
"@dbx-tools/shared-core": "0.3.1"
|
|
22
|
+
},
|
|
23
|
+
"main": "index.ts",
|
|
24
|
+
"license": "UNLICENSED",
|
|
25
|
+
"publishConfig": {
|
|
26
|
+
"access": "public"
|
|
8
27
|
},
|
|
28
|
+
"version": "0.3.1",
|
|
29
|
+
"types": "index.ts",
|
|
30
|
+
"type": "module",
|
|
9
31
|
"exports": {
|
|
10
|
-
".":
|
|
11
|
-
|
|
12
|
-
|
|
32
|
+
".": "./index.ts",
|
|
33
|
+
"./package.json": "./package.json"
|
|
34
|
+
},
|
|
35
|
+
"peerDependenciesMeta": {
|
|
36
|
+
"@databricks/appkit": {
|
|
37
|
+
"optional": true
|
|
13
38
|
}
|
|
14
39
|
},
|
|
15
|
-
"
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
"
|
|
22
|
-
|
|
23
|
-
"
|
|
40
|
+
"dbxToolsConfig": {
|
|
41
|
+
"tags": [
|
|
42
|
+
"node"
|
|
43
|
+
]
|
|
44
|
+
},
|
|
45
|
+
"//": "~~ Generated by projen. To modify, edit .projenrc.js and run \"pnpm exec projen\".",
|
|
46
|
+
"scripts": {
|
|
47
|
+
"build": "projen build",
|
|
48
|
+
"compile": "projen compile",
|
|
49
|
+
"default": "projen default",
|
|
50
|
+
"package": "projen package",
|
|
51
|
+
"post-compile": "projen post-compile",
|
|
52
|
+
"pre-compile": "projen pre-compile",
|
|
53
|
+
"test": "projen test",
|
|
54
|
+
"watch": "projen watch",
|
|
55
|
+
"projen": "projen"
|
|
24
56
|
}
|
|
25
|
-
}
|
|
57
|
+
}
|
package/src/chat.ts
ADDED
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Genie chat driver.
|
|
3
|
+
*
|
|
4
|
+
* Drives a single turn against a Genie space from one `content` string;
|
|
5
|
+
* multi-turn conversations are the caller's job (thread the `conversation_id`
|
|
6
|
+
* returned on each `GenieMessage` back into the next turn's
|
|
7
|
+
* `options.conversationId`).
|
|
8
|
+
*
|
|
9
|
+
* Two layers serve two kinds of consumer. The low-level layer yields every
|
|
10
|
+
* poll-observed `GenieMessage` (validated against `GenieMessageSchema`,
|
|
11
|
+
* falling back to the raw snapshot on a schema miss) and owns the messy parts
|
|
12
|
+
* - cancellation, conversation seeding, distinct-filtering, and SDK quirks
|
|
13
|
+
* (Waiter stripping); reach for it when you want the raw stream. The
|
|
14
|
+
* high-level layer wraps it and emits semantic, deduplicated `{ type, payload }`
|
|
15
|
+
* events (see {@link GenieChatEvent}), always closing a successful turn with a
|
|
16
|
+
* terminal `result` event carrying the final `GenieMessage`; errors propagate
|
|
17
|
+
* by throwing, with no `error` variant. Iterating UI / agent code that wants
|
|
18
|
+
* every message verbatim takes the low-level stream; subscribers reacting to
|
|
19
|
+
* "Genie is thinking about X" or "Genie produced text Y" take the event layer.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { async, log, type PollContext } from "@dbx-tools/shared-core";
|
|
23
|
+
import { databricks } from "@dbx-tools/appkit";
|
|
24
|
+
import { event, genieModel, type GenieChatEvent, type GenieMessage } from "@dbx-tools/shared-genie";
|
|
25
|
+
import { WorkspaceClient } from "@databricks/sdk-experimental";
|
|
26
|
+
|
|
27
|
+
const logger = log.logger("genie/chat");
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Validate a polled wire snapshot against {@link GenieMessageSchema}
|
|
31
|
+
* and return the schema-normalized message. Genie's wire occasionally ships a
|
|
32
|
+
* shape the (SDK-derived) schema doesn't model exactly - e.g. an early poll
|
|
33
|
+
* that omits the SDK-required `message_id` - so a miss degrades to the raw
|
|
34
|
+
* snapshot rather than throwing, keeping a single odd poll from aborting the
|
|
35
|
+
* whole turn.
|
|
36
|
+
*/
|
|
37
|
+
function validateMessage(raw: GenieMessage): GenieMessage {
|
|
38
|
+
const result = genieModel.GenieMessageSchema.safeParse(raw);
|
|
39
|
+
if (result.success) return result.data;
|
|
40
|
+
logger.debug("wire-message:schema-miss", {
|
|
41
|
+
message_id: raw.message_id ?? raw.id,
|
|
42
|
+
issues: result.error.issues.length,
|
|
43
|
+
});
|
|
44
|
+
return raw;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/* -------------------------- shared options -------------------------- */
|
|
48
|
+
|
|
49
|
+
/** Options accepted by both {@link genieChat} and {@link genieEventChat}. */
|
|
50
|
+
export interface GenieChatOptions {
|
|
51
|
+
/**
|
|
52
|
+
* Seed conversation id. When set, this turn appends to the existing
|
|
53
|
+
* conversation (via `createMessage`) instead of opening a new one. Use it to
|
|
54
|
+
* thread a multi-turn conversation: read `conversation_id` off the prior
|
|
55
|
+
* turn's terminal `GenieMessage` (or the `result` event's
|
|
56
|
+
* `payload.conversation_id`) and pass it into the next call.
|
|
57
|
+
*/
|
|
58
|
+
conversationId?: string;
|
|
59
|
+
/**
|
|
60
|
+
* Explicit `WorkspaceClient`. Defaults to AppKit's per-request
|
|
61
|
+
* execution-context client when AppKit is installed and we're inside a
|
|
62
|
+
* request; falls back to a fresh `new WorkspaceClient({})` (env-var auth)
|
|
63
|
+
* otherwise.
|
|
64
|
+
*/
|
|
65
|
+
workspaceClient?: WorkspaceClient;
|
|
66
|
+
/** Poll cadence in milliseconds between successive `getMessage` calls (default 500). */
|
|
67
|
+
pollIntervalMs?: number;
|
|
68
|
+
/**
|
|
69
|
+
* External cancellation. Accepts a WHATWG `AbortSignal` or a fully-built SDK
|
|
70
|
+
* `Context` (see `databricks.ContextLike`). Aborting it cancels every in-flight
|
|
71
|
+
* SDK call and the next inter-poll sleep.
|
|
72
|
+
*/
|
|
73
|
+
context?: databricks.ContextLike;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/* ----------------------- low-level: genieChat ----------------------- */
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* One turn against a Genie space, yielded as a stream of `GenieMessage`
|
|
80
|
+
* snapshots.
|
|
81
|
+
*
|
|
82
|
+
* Turn lifecycle:
|
|
83
|
+
*
|
|
84
|
+
* - No `options.conversationId`: open a new conversation via
|
|
85
|
+
* `client.genie.startConversation`. The opened conversation id surfaces on
|
|
86
|
+
* every yielded `GenieMessage` (`.conversation_id`) so the caller can
|
|
87
|
+
* thread it into a follow-up call.
|
|
88
|
+
* - With `options.conversationId`: append to that conversation via
|
|
89
|
+
* `client.genie.createMessage`.
|
|
90
|
+
* - In both cases, after the create/start the driver polls
|
|
91
|
+
* `client.genie.getMessage` every `options.pollIntervalMs` (default 500ms)
|
|
92
|
+
* until the message reaches a terminal status, then yields the terminal
|
|
93
|
+
* snapshot and returns.
|
|
94
|
+
*
|
|
95
|
+
* Cancellation: a single internal `AbortController` covers the whole turn.
|
|
96
|
+
* `options.context` is tied into that controller so an external abort tears
|
|
97
|
+
* down every in-flight SDK call AND the inter-poll sleep. Breaking out of the
|
|
98
|
+
* `for await` does the same via the `try / finally`.
|
|
99
|
+
*
|
|
100
|
+
* @example
|
|
101
|
+
* // Single turn.
|
|
102
|
+
* for await (const m of genieChat(spaceId, "Top 5 stores?")) {
|
|
103
|
+
* render(m);
|
|
104
|
+
* }
|
|
105
|
+
*
|
|
106
|
+
* @example
|
|
107
|
+
* // Multi-turn: caller threads the conversation id.
|
|
108
|
+
* let conversationId: string | undefined;
|
|
109
|
+
* for (const question of questions) {
|
|
110
|
+
* for await (const m of genieChat(spaceId, question, { conversationId })) {
|
|
111
|
+
* conversationId = m.conversation_id ?? conversationId;
|
|
112
|
+
* render(m);
|
|
113
|
+
* }
|
|
114
|
+
* }
|
|
115
|
+
*/
|
|
116
|
+
export async function* genieChat(
|
|
117
|
+
space_id: string,
|
|
118
|
+
content: string,
|
|
119
|
+
options?: GenieChatOptions,
|
|
120
|
+
): AsyncGenerator<GenieMessage, void, void> {
|
|
121
|
+
const controller = new AbortController();
|
|
122
|
+
try {
|
|
123
|
+
const client = await getWorkspaceClient(options);
|
|
124
|
+
// Build the SDK Context ONCE. Building it inside the poll producer would
|
|
125
|
+
// re-attach an abort listener to `options.context` on every poll iteration
|
|
126
|
+
// (via `databricks.toContext` -> `async.tieAbortSignal`), eventually tripping
|
|
127
|
+
// Node's `MaxListenersExceededWarning`.
|
|
128
|
+
const ctx = databricks.toContext(controller, options?.context);
|
|
129
|
+
let conversationId = options?.conversationId;
|
|
130
|
+
let messageId: string | undefined;
|
|
131
|
+
|
|
132
|
+
const pollProducer = async (pollCtx: PollContext<GenieMessage>): Promise<GenieMessage> => {
|
|
133
|
+
if (!conversationId) {
|
|
134
|
+
// First poll: open the conversation. Refuse to retry: if
|
|
135
|
+
// `startConversation` returned a response without a `conversation_id`,
|
|
136
|
+
// retrying would just open conversation after conversation.
|
|
137
|
+
if (pollCtx.attempt > 0) {
|
|
138
|
+
throw new Error("Genie did not return a conversation id; refusing to retry");
|
|
139
|
+
}
|
|
140
|
+
const startResponse = await client.genie.startConversation({ space_id, content }, ctx);
|
|
141
|
+
conversationId = startResponse.conversation_id;
|
|
142
|
+
messageId = startResponse.message_id;
|
|
143
|
+
return startResponse.message!;
|
|
144
|
+
}
|
|
145
|
+
if (!messageId) {
|
|
146
|
+
// First poll of a follow-up turn: append to the seeded conversation.
|
|
147
|
+
// `client.genie.createMessage` returns a `Waiter<GenieMessage>`
|
|
148
|
+
// (`{ ...message, wait: async () => ... }`). Strip `wait` here so
|
|
149
|
+
// downstream serializers (e.g. yaml.stringify) don't choke on the
|
|
150
|
+
// AsyncFunction value.
|
|
151
|
+
const { wait: _wait, ...createResponse } = await client.genie.createMessage(
|
|
152
|
+
{ space_id, conversation_id: conversationId, content },
|
|
153
|
+
ctx,
|
|
154
|
+
);
|
|
155
|
+
messageId = createResponse.message_id;
|
|
156
|
+
return createResponse;
|
|
157
|
+
}
|
|
158
|
+
// Subsequent polls: re-fetch the current message until its status becomes
|
|
159
|
+
// terminal.
|
|
160
|
+
return await client.genie.getMessage(
|
|
161
|
+
{
|
|
162
|
+
space_id,
|
|
163
|
+
conversation_id: conversationId,
|
|
164
|
+
message_id: messageId,
|
|
165
|
+
},
|
|
166
|
+
ctx,
|
|
167
|
+
);
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
yield* async.poll(
|
|
171
|
+
async (pollCtx: PollContext<GenieMessage>) => validateMessage(await pollProducer(pollCtx)),
|
|
172
|
+
{
|
|
173
|
+
intervalMs: options?.pollIntervalMs ?? 500,
|
|
174
|
+
// Skip yielding identical consecutive snapshots; Genie often returns
|
|
175
|
+
// the exact same payload twice during quiet periods. `poll` does a deep
|
|
176
|
+
// equal on the previous yield.
|
|
177
|
+
filter: "distinct",
|
|
178
|
+
// Stop after the terminal message is yielded. `poll` checks the
|
|
179
|
+
// predicate AFTER yielding, so the terminal message still reaches the
|
|
180
|
+
// consumer.
|
|
181
|
+
predicate: (m) => !genieModel.isTerminalStatus(m.status),
|
|
182
|
+
// Wake the inter-poll sleep on abort so a `for await` break (or
|
|
183
|
+
// external abort) tears down promptly instead of waiting out the
|
|
184
|
+
// interval.
|
|
185
|
+
signal: controller.signal,
|
|
186
|
+
},
|
|
187
|
+
);
|
|
188
|
+
} finally {
|
|
189
|
+
// Cancels any still-pending SDK call and the inter-poll sleep whether we're
|
|
190
|
+
// unwinding from a normal return, a consumer break, or a thrown error.
|
|
191
|
+
// Idempotent.
|
|
192
|
+
controller.abort();
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/* ---------------------- high-level: genieEventChat ------------------ */
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* One turn against a Genie space, yielded as a typed {@link GenieChatEvent}
|
|
200
|
+
* stream. Drives {@link genieChat} underneath and decorates each snapshot with
|
|
201
|
+
* the derived events the field-level diff produced. Stream order:
|
|
202
|
+
*
|
|
203
|
+
* 1. `{ type: "message", message }` - the raw `GenieMessage`, once per poll
|
|
204
|
+
* yield.
|
|
205
|
+
* 2. `{ type: "question", content, message_id, ... }` fires exactly once, on
|
|
206
|
+
* the FIRST `message` yield. We read `content` and `message_id` straight
|
|
207
|
+
* off the snapshot so every downstream event for this turn shares the same
|
|
208
|
+
* `message_id` (the question included) - subscribers can group everything
|
|
209
|
+
* for one Genie call under that one key.
|
|
210
|
+
* 3. Any of `status` / `attachment` / `thinking` / `text` / `query` /
|
|
211
|
+
* `statement` / `rows` / `suggested_questions` the diff against the prior
|
|
212
|
+
* snapshot produced.
|
|
213
|
+
* 4. On the terminal snapshot, `{ type: "result", ... }` as the final yield.
|
|
214
|
+
*
|
|
215
|
+
* Errors propagate by the generator throwing - there's no `"error"` variant.
|
|
216
|
+
* Wrap the `for await` in `try/catch` if you need to handle failures.
|
|
217
|
+
*
|
|
218
|
+
* @example
|
|
219
|
+
* for await (const evt of genieEventChat(spaceId, "Top stores?")) {
|
|
220
|
+
* switch (evt.type) {
|
|
221
|
+
* case "thinking":
|
|
222
|
+
* console.log("[thinking]", evt.thought_type, evt.text);
|
|
223
|
+
* break;
|
|
224
|
+
* case "text":
|
|
225
|
+
* console.log("[text]", evt.text);
|
|
226
|
+
* break;
|
|
227
|
+
* case "result":
|
|
228
|
+
* console.log("[done]", evt.status);
|
|
229
|
+
* break;
|
|
230
|
+
* }
|
|
231
|
+
* }
|
|
232
|
+
*/
|
|
233
|
+
export async function* genieEventChat(
|
|
234
|
+
space_id: string,
|
|
235
|
+
content: string,
|
|
236
|
+
options?: GenieChatOptions,
|
|
237
|
+
): AsyncGenerator<GenieChatEvent, void, void> {
|
|
238
|
+
// Diff source for the current turn. Always `undefined` on the first snapshot
|
|
239
|
+
// so the initial status / attachments emit fresh; updated to the most recent
|
|
240
|
+
// snapshot after each yield.
|
|
241
|
+
let previous: GenieMessage | undefined;
|
|
242
|
+
// The `question` event is deferred to the first `message` yield so it can
|
|
243
|
+
// carry the assigned `message_id` (subscribers use it as the grouping key for
|
|
244
|
+
// every event in this turn). The first snapshot is the earliest point that id
|
|
245
|
+
// exists.
|
|
246
|
+
let questionEmitted = false;
|
|
247
|
+
for await (const rawMessage of genieChat(space_id, content, options)) {
|
|
248
|
+
// Normalize `message_id` from the legacy `id` field when Genie's wire
|
|
249
|
+
// response only populates one of them. The SDK schema marks both as
|
|
250
|
+
// required, but in practice the `startConversation` / `createMessage` inner
|
|
251
|
+
// `message` payload sometimes ships only `id` while the new `message_id`
|
|
252
|
+
// field lands undefined. Every downstream event detector keys grouping off
|
|
253
|
+
// `message_id`; the fallback keeps one Genie turn's events from splitting
|
|
254
|
+
// across an anon group + the real-id group when subscribers bucket by
|
|
255
|
+
// `message_id`.
|
|
256
|
+
const message: GenieMessage = rawMessage.message_id
|
|
257
|
+
? rawMessage
|
|
258
|
+
: { ...rawMessage, message_id: rawMessage.id };
|
|
259
|
+
yield {
|
|
260
|
+
type: "message",
|
|
261
|
+
space_id: message.space_id,
|
|
262
|
+
message_id: message.message_id,
|
|
263
|
+
message,
|
|
264
|
+
};
|
|
265
|
+
if (!questionEmitted) {
|
|
266
|
+
yield {
|
|
267
|
+
type: "question",
|
|
268
|
+
space_id: message.space_id,
|
|
269
|
+
...(message.conversation_id ? { conversation_id: message.conversation_id } : {}),
|
|
270
|
+
...(message.message_id ? { message_id: message.message_id } : {}),
|
|
271
|
+
content: message.content,
|
|
272
|
+
};
|
|
273
|
+
questionEmitted = true;
|
|
274
|
+
}
|
|
275
|
+
yield* event.eventsFromMessage(message, previous, message.space_id);
|
|
276
|
+
const status = message.status;
|
|
277
|
+
if (genieModel.isTerminalStatus(status)) {
|
|
278
|
+
yield {
|
|
279
|
+
type: "result",
|
|
280
|
+
space_id: message.space_id,
|
|
281
|
+
conversation_id: message.conversation_id,
|
|
282
|
+
message_id: message.message_id,
|
|
283
|
+
status,
|
|
284
|
+
message,
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
previous = message;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/* ---------------------- workspace client helper --------------------- */
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Resolve a `WorkspaceClient` in this preference order:
|
|
295
|
+
*
|
|
296
|
+
* 1. Caller-supplied `options.workspaceClient`.
|
|
297
|
+
* 2. AppKit's per-request execution-context client, when AppKit is installed
|
|
298
|
+
* AND we're inside a request scope.
|
|
299
|
+
* 3. Fresh `new WorkspaceClient({})` (env-var auth via
|
|
300
|
+
* `DATABRICKS_CONFIG_PROFILE` / `DATABRICKS_HOST` / `DATABRICKS_TOKEN`).
|
|
301
|
+
*
|
|
302
|
+
* AppKit is loaded lazily so this package stays usable in non-AppKit
|
|
303
|
+
* environments.
|
|
304
|
+
*/
|
|
305
|
+
async function getWorkspaceClient(options?: GenieChatOptions): Promise<WorkspaceClient> {
|
|
306
|
+
if (options?.workspaceClient) return options.workspaceClient;
|
|
307
|
+
const appkit = await getAppKit();
|
|
308
|
+
if (appkit) {
|
|
309
|
+
try {
|
|
310
|
+
return appkit.getExecutionContext().client;
|
|
311
|
+
} catch {
|
|
312
|
+
// Not inside an AppKit request context; fall through to env.
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
return new WorkspaceClient({});
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
async function getAppKit() {
|
|
319
|
+
try {
|
|
320
|
+
return await import("@databricks/appkit");
|
|
321
|
+
} catch {
|
|
322
|
+
return undefined;
|
|
323
|
+
}
|
|
324
|
+
}
|
package/src/space.ts
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Genie space metadata helpers.
|
|
3
|
+
*
|
|
4
|
+
* Fetches a Genie space's definition (including the opt-in `serialized_space`
|
|
5
|
+
* blob) and extracts the curated starter questions an author configured on the
|
|
6
|
+
* space. The typed SDK `client.genie.getSpace` only returns the
|
|
7
|
+
* directory-listing surface (`title` / `description` / `warehouse_id`); the
|
|
8
|
+
* sample questions live inside `serialized_space`, which the REST API returns
|
|
9
|
+
* only when `include_serialized_space=true`. We hit that endpoint through the
|
|
10
|
+
* workspace client's raw `apiClient` since the typed request shape has no flag
|
|
11
|
+
* for it.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { error, log, object, string } from "@dbx-tools/shared-core";
|
|
15
|
+
import { databricks } from "@dbx-tools/appkit";
|
|
16
|
+
import { genieModel, type GenieSpace } from "@dbx-tools/shared-genie";
|
|
17
|
+
import { WorkspaceClient } from "@databricks/sdk-experimental";
|
|
18
|
+
|
|
19
|
+
const logger = log.logger("genie/space");
|
|
20
|
+
|
|
21
|
+
/** Options for {@link getGenieSpace}. */
|
|
22
|
+
export interface GetGenieSpaceOptions {
|
|
23
|
+
/**
|
|
24
|
+
* Explicit `WorkspaceClient`. Defaults to a fresh `new WorkspaceClient({})`
|
|
25
|
+
* (env-var auth). Server callers should pass their OBO-scoped client so the
|
|
26
|
+
* lookup runs as the user.
|
|
27
|
+
*/
|
|
28
|
+
workspaceClient?: WorkspaceClient;
|
|
29
|
+
/**
|
|
30
|
+
* Request the `serialized_space` blob (catalogs, tables, sample questions,
|
|
31
|
+
* prompts). Defaults to `true` - the only reason to skip it is when the
|
|
32
|
+
* caller just needs title / description and wants the smaller payload.
|
|
33
|
+
*/
|
|
34
|
+
serialized?: boolean;
|
|
35
|
+
/**
|
|
36
|
+
* External cancellation. Accepts a WHATWG `AbortSignal` or a fully-built SDK
|
|
37
|
+
* `Context` (see `databricks.ContextLike`).
|
|
38
|
+
*/
|
|
39
|
+
context?: databricks.ContextLike;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Fetch a Genie space by id, optionally including its serialized definition.
|
|
44
|
+
* Hits `GET /api/2.0/genie/spaces/<id>` with `include_serialized_space=true`
|
|
45
|
+
* through the raw `apiClient`, then validates the response against
|
|
46
|
+
* {@link GenieSpaceSchema} (unknown fields like `etag` /
|
|
47
|
+
* `parent_path` are stripped).
|
|
48
|
+
*/
|
|
49
|
+
export async function getGenieSpace(
|
|
50
|
+
spaceId: string,
|
|
51
|
+
options?: GetGenieSpaceOptions,
|
|
52
|
+
): Promise<GenieSpace> {
|
|
53
|
+
const client = options?.workspaceClient ?? new WorkspaceClient({});
|
|
54
|
+
const serialized = options?.serialized !== false;
|
|
55
|
+
const ctx = options?.context ? databricks.toContext(options.context) : undefined;
|
|
56
|
+
const raw = await client.apiClient.request(
|
|
57
|
+
{
|
|
58
|
+
path: `/api/2.0/genie/spaces/${encodeURIComponent(spaceId)}`,
|
|
59
|
+
method: "GET",
|
|
60
|
+
query: serialized ? { include_serialized_space: true } : {},
|
|
61
|
+
headers: new Headers(),
|
|
62
|
+
raw: false,
|
|
63
|
+
},
|
|
64
|
+
ctx,
|
|
65
|
+
);
|
|
66
|
+
return genieModel.GenieSpaceSchema.parse(raw);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* One entry in a serialized space's `config.sample_questions`. The
|
|
71
|
+
* author-facing field is `question`, which the wire format models as a string
|
|
72
|
+
* array (a single multi-line question is split across entries); we treat the
|
|
73
|
+
* first non-empty entry as the displayable question text.
|
|
74
|
+
*/
|
|
75
|
+
interface SerializedSampleQuestion {
|
|
76
|
+
question?: unknown;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Extract the curated starter questions an author configured on a Genie space.
|
|
81
|
+
* Reads `serialized_space -> config.sample_questions[*].question`. Returns `[]`
|
|
82
|
+
* when the space carries no serialized blob, the blob is unparseable, or no
|
|
83
|
+
* sample questions are configured - so a missing or misconfigured space
|
|
84
|
+
* degrades to "no suggestions" rather than throwing. Order is preserved (the
|
|
85
|
+
* author's ordering) and duplicates are dropped.
|
|
86
|
+
*/
|
|
87
|
+
export function genieSampleQuestions(space: GenieSpace): string[] {
|
|
88
|
+
const serialized = space.serialized_space;
|
|
89
|
+
if (!serialized) return [];
|
|
90
|
+
let parsed: unknown;
|
|
91
|
+
try {
|
|
92
|
+
parsed = JSON.parse(serialized);
|
|
93
|
+
} catch (err) {
|
|
94
|
+
logger.warn("serialized-space:parse-error", {
|
|
95
|
+
spaceId: space.space_id,
|
|
96
|
+
error: error.errorMessage(err),
|
|
97
|
+
});
|
|
98
|
+
return [];
|
|
99
|
+
}
|
|
100
|
+
const sampleQuestions = (parsed as { config?: { sample_questions?: unknown } } | null)?.config
|
|
101
|
+
?.sample_questions;
|
|
102
|
+
if (!Array.isArray(sampleQuestions)) return [];
|
|
103
|
+
|
|
104
|
+
return [
|
|
105
|
+
...object
|
|
106
|
+
.sequence(sampleQuestions as SerializedSampleQuestion[])
|
|
107
|
+
.map((entry) => string.firstNonEmpty(entry?.question))
|
|
108
|
+
.nonNull()
|
|
109
|
+
.distinct(),
|
|
110
|
+
];
|
|
111
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// ~~ Generated by projen. To modify, edit .projenrc.js and run "pnpm exec projen".
|
|
2
|
+
{
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"rootDir": "src",
|
|
5
|
+
"outDir": "lib",
|
|
6
|
+
"alwaysStrict": true,
|
|
7
|
+
"declaration": true,
|
|
8
|
+
"esModuleInterop": true,
|
|
9
|
+
"experimentalDecorators": true,
|
|
10
|
+
"inlineSourceMap": true,
|
|
11
|
+
"inlineSources": true,
|
|
12
|
+
"lib": [
|
|
13
|
+
"ES2022"
|
|
14
|
+
],
|
|
15
|
+
"module": "ESNext",
|
|
16
|
+
"noEmitOnError": false,
|
|
17
|
+
"noFallthroughCasesInSwitch": true,
|
|
18
|
+
"noImplicitAny": true,
|
|
19
|
+
"noImplicitReturns": true,
|
|
20
|
+
"noImplicitThis": true,
|
|
21
|
+
"noUnusedLocals": true,
|
|
22
|
+
"noUnusedParameters": true,
|
|
23
|
+
"resolveJsonModule": true,
|
|
24
|
+
"strict": true,
|
|
25
|
+
"strictNullChecks": true,
|
|
26
|
+
"strictPropertyInitialization": true,
|
|
27
|
+
"stripInternal": true,
|
|
28
|
+
"target": "ES2022",
|
|
29
|
+
"types": [
|
|
30
|
+
"node"
|
|
31
|
+
],
|
|
32
|
+
"moduleResolution": "bundler",
|
|
33
|
+
"skipLibCheck": true
|
|
34
|
+
},
|
|
35
|
+
"include": [
|
|
36
|
+
"src/**/*.ts"
|
|
37
|
+
],
|
|
38
|
+
"exclude": [
|
|
39
|
+
"node_modules"
|
|
40
|
+
]
|
|
41
|
+
}
|
package/dist/index.d.ts
DELETED
|
@@ -1,157 +0,0 @@
|
|
|
1
|
-
import { GenieChatEvent, GenieMessage, GenieSpace } from "@dbx-tools/genie-shared";
|
|
2
|
-
import { WorkspaceClient } from "@databricks/sdk-experimental";
|
|
3
|
-
import { apiUtils } from "@dbx-tools/shared";
|
|
4
|
-
export * from "@dbx-tools/genie-shared";
|
|
5
|
-
|
|
6
|
-
//#region packages/genie/src/chat.d.ts
|
|
7
|
-
/** Options accepted by both {@link genieChat} and {@link genieEventChat}. */
|
|
8
|
-
interface GenieChatOptions {
|
|
9
|
-
/**
|
|
10
|
-
* Seed conversation id. When set, this turn appends to the
|
|
11
|
-
* existing conversation (via `createMessage`) instead of opening
|
|
12
|
-
* a new one. Use it to thread a multi-turn conversation: read
|
|
13
|
-
* `conversation_id` off the prior turn's terminal `GenieMessage`
|
|
14
|
-
* (or the `result` event's `payload.conversation_id`) and pass
|
|
15
|
-
* it into the next call.
|
|
16
|
-
*/
|
|
17
|
-
conversationId?: string;
|
|
18
|
-
/**
|
|
19
|
-
* Explicit `WorkspaceClient`. Defaults to AppKit's per-request
|
|
20
|
-
* execution-context client when AppKit is installed and we're
|
|
21
|
-
* inside a request; falls back to a fresh `new WorkspaceClient({})`
|
|
22
|
-
* (env-var auth) otherwise.
|
|
23
|
-
*/
|
|
24
|
-
workspaceClient?: WorkspaceClient;
|
|
25
|
-
/** Poll cadence in milliseconds between successive `getMessage` calls (default 500). */
|
|
26
|
-
pollIntervalMs?: number;
|
|
27
|
-
/**
|
|
28
|
-
* External cancellation. Accepts a WHATWG `AbortSignal` or a
|
|
29
|
-
* fully-built SDK `Context` (see `apiUtils.ContextLike`).
|
|
30
|
-
* Aborting it cancels every in-flight SDK call and the next
|
|
31
|
-
* inter-poll sleep.
|
|
32
|
-
*/
|
|
33
|
-
context?: apiUtils.ContextLike;
|
|
34
|
-
}
|
|
35
|
-
/**
|
|
36
|
-
* One turn against a Genie space, yielded as a stream of
|
|
37
|
-
* `GenieMessage` snapshots.
|
|
38
|
-
*
|
|
39
|
-
* Turn lifecycle:
|
|
40
|
-
*
|
|
41
|
-
* - No `options.conversationId`: open a new conversation via
|
|
42
|
-
* `client.genie.startConversation`. The opened conversation id
|
|
43
|
-
* surfaces on every yielded `GenieMessage` (`.conversation_id`)
|
|
44
|
-
* so the caller can thread it into a follow-up call.
|
|
45
|
-
* - With `options.conversationId`: append to that conversation
|
|
46
|
-
* via `client.genie.createMessage`.
|
|
47
|
-
* - In both cases, after the create/start the driver polls
|
|
48
|
-
* `client.genie.getMessage` every `options.pollIntervalMs`
|
|
49
|
-
* (default 500ms) until the message reaches a terminal
|
|
50
|
-
* status, then yields the terminal snapshot and returns.
|
|
51
|
-
*
|
|
52
|
-
* Cancellation: a single internal `AbortController` covers the
|
|
53
|
-
* whole turn. `options.context` is tied into that controller so an
|
|
54
|
-
* external abort tears down every in-flight SDK call AND the
|
|
55
|
-
* inter-poll sleep. Breaking out of the `for await` does the same
|
|
56
|
-
* via the `try / finally`.
|
|
57
|
-
*
|
|
58
|
-
* @example
|
|
59
|
-
* // Single turn.
|
|
60
|
-
* for await (const m of genieChat(spaceId, "Top 5 stores?")) {
|
|
61
|
-
* render(m);
|
|
62
|
-
* }
|
|
63
|
-
*
|
|
64
|
-
* @example
|
|
65
|
-
* // Multi-turn: caller threads the conversation id.
|
|
66
|
-
* let conversationId: string | undefined;
|
|
67
|
-
* for (const question of questions) {
|
|
68
|
-
* for await (const m of genieChat(spaceId, question, { conversationId })) {
|
|
69
|
-
* conversationId = m.conversation_id ?? conversationId;
|
|
70
|
-
* render(m);
|
|
71
|
-
* }
|
|
72
|
-
* }
|
|
73
|
-
*/
|
|
74
|
-
declare function genieChat(space_id: string, content: string, options?: GenieChatOptions): AsyncGenerator<GenieMessage, void, void>;
|
|
75
|
-
/**
|
|
76
|
-
* One turn against a Genie space, yielded as a typed
|
|
77
|
-
* {@link GenieChatEvent} stream. Drives {@link genieChat}
|
|
78
|
-
* underneath and decorates each snapshot with the derived events
|
|
79
|
-
* the field-level diff produced. Stream order:
|
|
80
|
-
*
|
|
81
|
-
* 1. `{ type: "message", message }` - the raw `GenieMessage`,
|
|
82
|
-
* once per poll yield.
|
|
83
|
-
* 2. `{ type: "question", content, message_id, ... }` fires
|
|
84
|
-
* exactly once, on the FIRST `message` yield. We read
|
|
85
|
-
* `content` and `message_id` straight off the snapshot so
|
|
86
|
-
* every downstream event for this turn shares the same
|
|
87
|
-
* `message_id` (the question included) - subscribers can
|
|
88
|
-
* group everything for one Genie call under that one key.
|
|
89
|
-
* 3. Any of `status` / `attachment` / `thinking` / `text` /
|
|
90
|
-
* `query` / `statement` / `rows` / `suggested_questions` the
|
|
91
|
-
* diff against the prior snapshot produced.
|
|
92
|
-
* 4. On the terminal snapshot, `{ type: "result", ... }` as
|
|
93
|
-
* the final yield.
|
|
94
|
-
*
|
|
95
|
-
* Errors propagate by the generator throwing - there's no
|
|
96
|
-
* `"error"` variant. Wrap the `for await` in `try/catch` if you
|
|
97
|
-
* need to handle failures.
|
|
98
|
-
*
|
|
99
|
-
* @example
|
|
100
|
-
* for await (const event of genieEventChat(spaceId, "Top stores?")) {
|
|
101
|
-
* switch (event.type) {
|
|
102
|
-
* case "thinking":
|
|
103
|
-
* console.log("[thinking]", event.thought_type, event.text);
|
|
104
|
-
* break;
|
|
105
|
-
* case "text":
|
|
106
|
-
* console.log("[text]", event.text);
|
|
107
|
-
* break;
|
|
108
|
-
* case "result":
|
|
109
|
-
* console.log("[done]", event.status);
|
|
110
|
-
* break;
|
|
111
|
-
* }
|
|
112
|
-
* }
|
|
113
|
-
*/
|
|
114
|
-
declare function genieEventChat(space_id: string, content: string, options?: GenieChatOptions): AsyncGenerator<GenieChatEvent, void, void>;
|
|
115
|
-
//#endregion
|
|
116
|
-
//#region packages/genie/src/space.d.ts
|
|
117
|
-
/** Options for {@link getGenieSpace}. */
|
|
118
|
-
interface GetGenieSpaceOptions {
|
|
119
|
-
/**
|
|
120
|
-
* Explicit `WorkspaceClient`. Defaults to a fresh
|
|
121
|
-
* `new WorkspaceClient({})` (env-var auth). Server callers should
|
|
122
|
-
* pass their OBO-scoped client so the lookup runs as the user.
|
|
123
|
-
*/
|
|
124
|
-
workspaceClient?: WorkspaceClient;
|
|
125
|
-
/**
|
|
126
|
-
* Request the `serialized_space` blob (catalogs, tables, sample
|
|
127
|
-
* questions, prompts). Defaults to `true` - the only reason to
|
|
128
|
-
* skip it is when the caller just needs title / description and
|
|
129
|
-
* wants the smaller payload.
|
|
130
|
-
*/
|
|
131
|
-
serialized?: boolean;
|
|
132
|
-
/**
|
|
133
|
-
* External cancellation. Accepts a WHATWG `AbortSignal` or a
|
|
134
|
-
* fully-built SDK `Context` (see `apiUtils.ContextLike`).
|
|
135
|
-
*/
|
|
136
|
-
context?: apiUtils.ContextLike;
|
|
137
|
-
}
|
|
138
|
-
/**
|
|
139
|
-
* Fetch a Genie space by id, optionally including its serialized
|
|
140
|
-
* definition. Hits `GET /api/2.0/genie/spaces/<id>` with
|
|
141
|
-
* `include_serialized_space=true` through the raw `apiClient`, then
|
|
142
|
-
* validates the response against {@link GenieSpaceSchema} (unknown
|
|
143
|
-
* fields like `etag` / `parent_path` are stripped).
|
|
144
|
-
*/
|
|
145
|
-
declare function getGenieSpace(spaceId: string, options?: GetGenieSpaceOptions): Promise<GenieSpace>;
|
|
146
|
-
/**
|
|
147
|
-
* Extract the curated starter questions an author configured on a
|
|
148
|
-
* Genie space. Reads `serialized_space -> config.sample_questions[*]
|
|
149
|
-
* .question`. Returns `[]` when the space carries no serialized blob,
|
|
150
|
-
* the blob is unparseable, or no sample questions are configured -
|
|
151
|
-
* so a missing or misconfigured space degrades to "no suggestions"
|
|
152
|
-
* rather than throwing. Order is preserved (the author's ordering)
|
|
153
|
-
* and duplicates are dropped.
|
|
154
|
-
*/
|
|
155
|
-
declare function genieSampleQuestions(space: GenieSpace): string[];
|
|
156
|
-
//#endregion
|
|
157
|
-
export { GenieChatOptions, GetGenieSpaceOptions, genieChat, genieEventChat, genieSampleQuestions, getGenieSpace };
|
package/dist/index.js
DELETED
|
@@ -1,307 +0,0 @@
|
|
|
1
|
-
import { GenieMessageSchema, GenieSpaceSchema, eventsFromMessage, isTerminalStatus } from "@dbx-tools/genie-shared";
|
|
2
|
-
import { WorkspaceClient } from "@databricks/sdk-experimental";
|
|
3
|
-
import { apiUtils, commonUtils, logUtils, stringUtils } from "@dbx-tools/shared";
|
|
4
|
-
|
|
5
|
-
export * from "@dbx-tools/genie-shared"
|
|
6
|
-
|
|
7
|
-
//#region packages/genie/src/chat.ts
|
|
8
|
-
/**
|
|
9
|
-
* `@dbx-tools/genie` chat driver.
|
|
10
|
-
*
|
|
11
|
-
* Drives a single turn against a Genie space from one `content`
|
|
12
|
-
* string; multi-turn conversations are the caller's job (thread
|
|
13
|
-
* the `conversation_id` returned on each `GenieMessage` back into
|
|
14
|
-
* the next turn's `options.conversationId`).
|
|
15
|
-
*
|
|
16
|
-
* Two layers serve two kinds of consumer. The low-level layer
|
|
17
|
-
* yields every poll-observed `GenieMessage` (validated against
|
|
18
|
-
* `GenieMessageSchema`, falling back to the raw snapshot on a schema
|
|
19
|
-
* miss) and owns the messy parts - cancellation, conversation
|
|
20
|
-
* seeding, distinct-filtering, and SDK quirks (Waiter stripping);
|
|
21
|
-
* reach for it when you want the raw stream. The high-level layer wraps it
|
|
22
|
-
* and emits semantic, deduplicated `{ type, payload }` events
|
|
23
|
-
* (see {@link GenieChatEvent}), always closing a successful turn
|
|
24
|
-
* with a terminal `result` event carrying the final
|
|
25
|
-
* `GenieMessage`; errors propagate by throwing, with no `error`
|
|
26
|
-
* variant. Iterating UI / agent code that wants every message
|
|
27
|
-
* verbatim takes the low-level stream; subscribers reacting to
|
|
28
|
-
* "Genie is thinking about X" or "Genie produced text Y" take the
|
|
29
|
-
* event layer.
|
|
30
|
-
*/
|
|
31
|
-
const log$1 = logUtils.logger("genie/chat");
|
|
32
|
-
/**
|
|
33
|
-
* Validate a polled wire snapshot against {@link GenieMessageSchema}
|
|
34
|
-
* and return the schema-normalized message. Genie's wire occasionally
|
|
35
|
-
* ships a shape the (SDK-derived) schema doesn't model exactly - e.g.
|
|
36
|
-
* an early poll that omits the SDK-required `message_id` - so a miss
|
|
37
|
-
* degrades to the raw snapshot rather than throwing, keeping a single
|
|
38
|
-
* odd poll from aborting the whole turn.
|
|
39
|
-
*/
|
|
40
|
-
function validateMessage(raw) {
|
|
41
|
-
const result = GenieMessageSchema.safeParse(raw);
|
|
42
|
-
if (result.success) return result.data;
|
|
43
|
-
log$1.debug("wire-message:schema-miss", {
|
|
44
|
-
message_id: raw.message_id ?? raw.id,
|
|
45
|
-
issues: result.error.issues.length
|
|
46
|
-
});
|
|
47
|
-
return raw;
|
|
48
|
-
}
|
|
49
|
-
/**
|
|
50
|
-
* One turn against a Genie space, yielded as a stream of
|
|
51
|
-
* `GenieMessage` snapshots.
|
|
52
|
-
*
|
|
53
|
-
* Turn lifecycle:
|
|
54
|
-
*
|
|
55
|
-
* - No `options.conversationId`: open a new conversation via
|
|
56
|
-
* `client.genie.startConversation`. The opened conversation id
|
|
57
|
-
* surfaces on every yielded `GenieMessage` (`.conversation_id`)
|
|
58
|
-
* so the caller can thread it into a follow-up call.
|
|
59
|
-
* - With `options.conversationId`: append to that conversation
|
|
60
|
-
* via `client.genie.createMessage`.
|
|
61
|
-
* - In both cases, after the create/start the driver polls
|
|
62
|
-
* `client.genie.getMessage` every `options.pollIntervalMs`
|
|
63
|
-
* (default 500ms) until the message reaches a terminal
|
|
64
|
-
* status, then yields the terminal snapshot and returns.
|
|
65
|
-
*
|
|
66
|
-
* Cancellation: a single internal `AbortController` covers the
|
|
67
|
-
* whole turn. `options.context` is tied into that controller so an
|
|
68
|
-
* external abort tears down every in-flight SDK call AND the
|
|
69
|
-
* inter-poll sleep. Breaking out of the `for await` does the same
|
|
70
|
-
* via the `try / finally`.
|
|
71
|
-
*
|
|
72
|
-
* @example
|
|
73
|
-
* // Single turn.
|
|
74
|
-
* for await (const m of genieChat(spaceId, "Top 5 stores?")) {
|
|
75
|
-
* render(m);
|
|
76
|
-
* }
|
|
77
|
-
*
|
|
78
|
-
* @example
|
|
79
|
-
* // Multi-turn: caller threads the conversation id.
|
|
80
|
-
* let conversationId: string | undefined;
|
|
81
|
-
* for (const question of questions) {
|
|
82
|
-
* for await (const m of genieChat(spaceId, question, { conversationId })) {
|
|
83
|
-
* conversationId = m.conversation_id ?? conversationId;
|
|
84
|
-
* render(m);
|
|
85
|
-
* }
|
|
86
|
-
* }
|
|
87
|
-
*/
|
|
88
|
-
async function* genieChat(space_id, content, options) {
|
|
89
|
-
const controller = new AbortController();
|
|
90
|
-
try {
|
|
91
|
-
const client = await getWorkspaceClient(options);
|
|
92
|
-
const context = apiUtils.toContext(controller, options?.context);
|
|
93
|
-
let conversationId = options?.conversationId;
|
|
94
|
-
let messageId;
|
|
95
|
-
const pollProducer = async (ctx) => {
|
|
96
|
-
if (!conversationId) {
|
|
97
|
-
if (ctx.attempt > 0) throw new Error("Genie did not return a conversation id; refusing to retry");
|
|
98
|
-
const startResponse = await client.genie.startConversation({
|
|
99
|
-
space_id,
|
|
100
|
-
content
|
|
101
|
-
}, context);
|
|
102
|
-
conversationId = startResponse.conversation_id;
|
|
103
|
-
messageId = startResponse.message_id;
|
|
104
|
-
return startResponse.message;
|
|
105
|
-
}
|
|
106
|
-
if (!messageId) {
|
|
107
|
-
const { wait: _wait, ...createResponse } = await client.genie.createMessage({
|
|
108
|
-
space_id,
|
|
109
|
-
conversation_id: conversationId,
|
|
110
|
-
content
|
|
111
|
-
}, context);
|
|
112
|
-
messageId = createResponse.message_id;
|
|
113
|
-
return createResponse;
|
|
114
|
-
}
|
|
115
|
-
return await client.genie.getMessage({
|
|
116
|
-
space_id,
|
|
117
|
-
conversation_id: conversationId,
|
|
118
|
-
message_id: messageId
|
|
119
|
-
}, context);
|
|
120
|
-
};
|
|
121
|
-
yield* commonUtils.poll(async (ctx) => validateMessage(await pollProducer(ctx)), {
|
|
122
|
-
intervalMs: options?.pollIntervalMs ?? 500,
|
|
123
|
-
filter: "distinct",
|
|
124
|
-
predicate: (m) => !isTerminalStatus(m.status),
|
|
125
|
-
signal: controller.signal
|
|
126
|
-
});
|
|
127
|
-
} finally {
|
|
128
|
-
controller.abort();
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
/**
|
|
132
|
-
* One turn against a Genie space, yielded as a typed
|
|
133
|
-
* {@link GenieChatEvent} stream. Drives {@link genieChat}
|
|
134
|
-
* underneath and decorates each snapshot with the derived events
|
|
135
|
-
* the field-level diff produced. Stream order:
|
|
136
|
-
*
|
|
137
|
-
* 1. `{ type: "message", message }` - the raw `GenieMessage`,
|
|
138
|
-
* once per poll yield.
|
|
139
|
-
* 2. `{ type: "question", content, message_id, ... }` fires
|
|
140
|
-
* exactly once, on the FIRST `message` yield. We read
|
|
141
|
-
* `content` and `message_id` straight off the snapshot so
|
|
142
|
-
* every downstream event for this turn shares the same
|
|
143
|
-
* `message_id` (the question included) - subscribers can
|
|
144
|
-
* group everything for one Genie call under that one key.
|
|
145
|
-
* 3. Any of `status` / `attachment` / `thinking` / `text` /
|
|
146
|
-
* `query` / `statement` / `rows` / `suggested_questions` the
|
|
147
|
-
* diff against the prior snapshot produced.
|
|
148
|
-
* 4. On the terminal snapshot, `{ type: "result", ... }` as
|
|
149
|
-
* the final yield.
|
|
150
|
-
*
|
|
151
|
-
* Errors propagate by the generator throwing - there's no
|
|
152
|
-
* `"error"` variant. Wrap the `for await` in `try/catch` if you
|
|
153
|
-
* need to handle failures.
|
|
154
|
-
*
|
|
155
|
-
* @example
|
|
156
|
-
* for await (const event of genieEventChat(spaceId, "Top stores?")) {
|
|
157
|
-
* switch (event.type) {
|
|
158
|
-
* case "thinking":
|
|
159
|
-
* console.log("[thinking]", event.thought_type, event.text);
|
|
160
|
-
* break;
|
|
161
|
-
* case "text":
|
|
162
|
-
* console.log("[text]", event.text);
|
|
163
|
-
* break;
|
|
164
|
-
* case "result":
|
|
165
|
-
* console.log("[done]", event.status);
|
|
166
|
-
* break;
|
|
167
|
-
* }
|
|
168
|
-
* }
|
|
169
|
-
*/
|
|
170
|
-
async function* genieEventChat(space_id, content, options) {
|
|
171
|
-
let previous;
|
|
172
|
-
let questionEmitted = false;
|
|
173
|
-
for await (const rawMessage of genieChat(space_id, content, options)) {
|
|
174
|
-
const message = rawMessage.message_id ? rawMessage : {
|
|
175
|
-
...rawMessage,
|
|
176
|
-
message_id: rawMessage.id
|
|
177
|
-
};
|
|
178
|
-
yield {
|
|
179
|
-
type: "message",
|
|
180
|
-
space_id: message.space_id,
|
|
181
|
-
message_id: message.message_id,
|
|
182
|
-
message
|
|
183
|
-
};
|
|
184
|
-
if (!questionEmitted) {
|
|
185
|
-
yield {
|
|
186
|
-
type: "question",
|
|
187
|
-
space_id: message.space_id,
|
|
188
|
-
...message.conversation_id ? { conversation_id: message.conversation_id } : {},
|
|
189
|
-
...message.message_id ? { message_id: message.message_id } : {},
|
|
190
|
-
content: message.content
|
|
191
|
-
};
|
|
192
|
-
questionEmitted = true;
|
|
193
|
-
}
|
|
194
|
-
yield* eventsFromMessage(message, previous, message.space_id);
|
|
195
|
-
if (isTerminalStatus(message.status)) yield {
|
|
196
|
-
type: "result",
|
|
197
|
-
space_id: message.space_id,
|
|
198
|
-
conversation_id: message.conversation_id,
|
|
199
|
-
message_id: message.message_id,
|
|
200
|
-
status: message.status,
|
|
201
|
-
message
|
|
202
|
-
};
|
|
203
|
-
previous = message;
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
/**
|
|
207
|
-
* Resolve a `WorkspaceClient` in this preference order:
|
|
208
|
-
*
|
|
209
|
-
* 1. Caller-supplied `options.workspaceClient`.
|
|
210
|
-
* 2. AppKit's per-request execution-context client, when AppKit
|
|
211
|
-
* is installed AND we're inside a request scope.
|
|
212
|
-
* 3. Fresh `new WorkspaceClient({})` (env-var auth via
|
|
213
|
-
* `DATABRICKS_CONFIG_PROFILE` / `DATABRICKS_HOST` /
|
|
214
|
-
* `DATABRICKS_TOKEN`).
|
|
215
|
-
*
|
|
216
|
-
* AppKit is loaded lazily so this package stays usable in
|
|
217
|
-
* non-AppKit environments (e.g. the `poll-chat` smoke test).
|
|
218
|
-
*/
|
|
219
|
-
async function getWorkspaceClient(options) {
|
|
220
|
-
if (options?.workspaceClient) return options.workspaceClient;
|
|
221
|
-
const appkit = await getAppKit();
|
|
222
|
-
if (appkit) try {
|
|
223
|
-
return appkit.getExecutionContext().client;
|
|
224
|
-
} catch {}
|
|
225
|
-
return new WorkspaceClient({});
|
|
226
|
-
}
|
|
227
|
-
async function getAppKit() {
|
|
228
|
-
try {
|
|
229
|
-
return await import("@databricks/appkit");
|
|
230
|
-
} catch {
|
|
231
|
-
return;
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
//#endregion
|
|
236
|
-
//#region packages/genie/src/space.ts
|
|
237
|
-
/**
|
|
238
|
-
* `@dbx-tools/genie` space metadata helpers.
|
|
239
|
-
*
|
|
240
|
-
* Fetches a Genie space's definition (including the opt-in
|
|
241
|
-
* `serialized_space` blob) and extracts the curated starter
|
|
242
|
-
* questions an author configured on the space. The typed SDK
|
|
243
|
-
* `client.genie.getSpace` only returns the directory-listing surface
|
|
244
|
-
* (`title` / `description` / `warehouse_id`); the sample questions
|
|
245
|
-
* live inside `serialized_space`, which the REST API returns only
|
|
246
|
-
* when `include_serialized_space=true`. We hit that endpoint through
|
|
247
|
-
* the workspace client's raw `apiClient` since the typed request
|
|
248
|
-
* shape has no flag for it.
|
|
249
|
-
*/
|
|
250
|
-
const log = logUtils.logger("genie/space");
|
|
251
|
-
/**
|
|
252
|
-
* Fetch a Genie space by id, optionally including its serialized
|
|
253
|
-
* definition. Hits `GET /api/2.0/genie/spaces/<id>` with
|
|
254
|
-
* `include_serialized_space=true` through the raw `apiClient`, then
|
|
255
|
-
* validates the response against {@link GenieSpaceSchema} (unknown
|
|
256
|
-
* fields like `etag` / `parent_path` are stripped).
|
|
257
|
-
*/
|
|
258
|
-
async function getGenieSpace(spaceId, options) {
|
|
259
|
-
const client = options?.workspaceClient ?? new WorkspaceClient({});
|
|
260
|
-
const serialized = options?.serialized !== false;
|
|
261
|
-
const context = options?.context ? apiUtils.toContext(options.context) : void 0;
|
|
262
|
-
const raw = await client.apiClient.request({
|
|
263
|
-
path: `/api/2.0/genie/spaces/${encodeURIComponent(spaceId)}`,
|
|
264
|
-
method: "GET",
|
|
265
|
-
query: serialized ? { include_serialized_space: true } : {},
|
|
266
|
-
headers: new Headers(),
|
|
267
|
-
raw: false
|
|
268
|
-
}, context);
|
|
269
|
-
return GenieSpaceSchema.parse(raw);
|
|
270
|
-
}
|
|
271
|
-
/**
|
|
272
|
-
* Extract the curated starter questions an author configured on a
|
|
273
|
-
* Genie space. Reads `serialized_space -> config.sample_questions[*]
|
|
274
|
-
* .question`. Returns `[]` when the space carries no serialized blob,
|
|
275
|
-
* the blob is unparseable, or no sample questions are configured -
|
|
276
|
-
* so a missing or misconfigured space degrades to "no suggestions"
|
|
277
|
-
* rather than throwing. Order is preserved (the author's ordering)
|
|
278
|
-
* and duplicates are dropped.
|
|
279
|
-
*/
|
|
280
|
-
function genieSampleQuestions(space) {
|
|
281
|
-
const serialized = space.serialized_space;
|
|
282
|
-
if (!serialized) return [];
|
|
283
|
-
let parsed;
|
|
284
|
-
try {
|
|
285
|
-
parsed = JSON.parse(serialized);
|
|
286
|
-
} catch (err) {
|
|
287
|
-
log.warn("serialized-space:parse-error", {
|
|
288
|
-
spaceId: space.space_id,
|
|
289
|
-
error: commonUtils.errorMessage(err)
|
|
290
|
-
});
|
|
291
|
-
return [];
|
|
292
|
-
}
|
|
293
|
-
const sampleQuestions = parsed?.config?.sample_questions;
|
|
294
|
-
if (!Array.isArray(sampleQuestions)) return [];
|
|
295
|
-
const seen = /* @__PURE__ */ new Set();
|
|
296
|
-
const out = [];
|
|
297
|
-
for (const entry of sampleQuestions) {
|
|
298
|
-
const text = stringUtils.firstNonEmpty(entry?.question);
|
|
299
|
-
if (!text || seen.has(text)) continue;
|
|
300
|
-
seen.add(text);
|
|
301
|
-
out.push(text);
|
|
302
|
-
}
|
|
303
|
-
return out;
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
//#endregion
|
|
307
|
-
export { genieChat, genieEventChat, genieSampleQuestions, getGenieSpace };
|