@dbx-tools/genie 0.1.18
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/dist/index.d.ts +33 -0
- package/dist/index.js +33 -0
- package/dist/src/chat.d.ts +137 -0
- package/dist/src/chat.js +251 -0
- package/dist/tsconfig.build.tsbuildinfo +1 -0
- package/index.ts +34 -0
- package/package.json +35 -0
- package/src/chat.ts +317 -0
package/index.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@dbx-tools/genie` public surface.
|
|
3
|
+
*
|
|
4
|
+
* - {@link genieChat}: low-level async generator. Yields every
|
|
5
|
+
* poll-observed `GenieMessage` for a single turn against a
|
|
6
|
+
* Genie space. Multi-turn conversations are driven by the
|
|
7
|
+
* caller (thread the `conversation_id` off each yielded
|
|
8
|
+
* message into the next call's `options.conversationId`).
|
|
9
|
+
* - {@link genieEventChat}: high-level async generator. Drives
|
|
10
|
+
* `genieChat` and yields a strongly-typed
|
|
11
|
+
* {@link GenieChatEvent} stream of flat `{type, ...fields}`
|
|
12
|
+
* records (`message`, `status`, `attachment`, `thinking`,
|
|
13
|
+
* `text`, `query`, `statement`, `rows`,
|
|
14
|
+
* `suggested_questions`, `result`). The final yield for any
|
|
15
|
+
* terminal turn is always `{ type: "result", ... }`.
|
|
16
|
+
*
|
|
17
|
+
* Wire-format types, the {@link GenieChatEvent} discriminated
|
|
18
|
+
* union, per-event detectors (`detectStatus`, `detectThinking`,
|
|
19
|
+
* `detectAttachmentAdded`, `detectText`, `detectQuery`,
|
|
20
|
+
* `detectStatement`, `detectRows`, `detectSuggestedQuestions`),
|
|
21
|
+
* the `eventsFromMessage` sync generator, and terminal-status
|
|
22
|
+
* helpers all live in `@dbx-tools/genie-shared` and are
|
|
23
|
+
* re-exported here so a single `from "@dbx-tools/genie"` import
|
|
24
|
+
* works for server-side consumers. Browser-side consumers should
|
|
25
|
+
* import from `@dbx-tools/genie-shared` directly to avoid pulling
|
|
26
|
+
* in the Node-only `chat.ts` runtime.
|
|
27
|
+
*
|
|
28
|
+
* Browser safety: `chat.ts` pulls in `WorkspaceClient` and is
|
|
29
|
+
* Node-only; the re-exports from `@dbx-tools/genie-shared` are
|
|
30
|
+
* pure (types + sync functions) and safe for any runtime.
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
export * from "./src/chat.js";
|
|
34
|
+
export * from "@dbx-tools/genie-shared";
|
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"main": "./dist/index.js",
|
|
3
|
+
"types": "./dist/index.d.ts",
|
|
4
|
+
"exports": {
|
|
5
|
+
".": {
|
|
6
|
+
"source": "./index.ts",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"default": "./dist/index.js"
|
|
9
|
+
}
|
|
10
|
+
},
|
|
11
|
+
"name": "@dbx-tools/genie",
|
|
12
|
+
"version": "0.1.18",
|
|
13
|
+
"dependencies": {
|
|
14
|
+
"@databricks/sdk-experimental": "^0.17",
|
|
15
|
+
"@dbx-tools/genie-shared": "0.1.18",
|
|
16
|
+
"@dbx-tools/shared": "0.1.18"
|
|
17
|
+
},
|
|
18
|
+
"module": "index.ts",
|
|
19
|
+
"type": "module",
|
|
20
|
+
"files": [
|
|
21
|
+
"dist",
|
|
22
|
+
"index*.ts",
|
|
23
|
+
"src"
|
|
24
|
+
],
|
|
25
|
+
"license": "Apache-2.0",
|
|
26
|
+
"homepage": "https://github.com/reggie-db/dbx-tools-appkit#readme",
|
|
27
|
+
"bugs": {
|
|
28
|
+
"url": "https://github.com/reggie-db/dbx-tools-appkit/issues"
|
|
29
|
+
},
|
|
30
|
+
"repository": {
|
|
31
|
+
"type": "git",
|
|
32
|
+
"url": "git+https://github.com/reggie-db/dbx-tools-appkit.git",
|
|
33
|
+
"directory": "packages/genie"
|
|
34
|
+
}
|
|
35
|
+
}
|
package/src/chat.ts
ADDED
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@dbx-tools/genie` chat driver.
|
|
3
|
+
*
|
|
4
|
+
* Two async generators ship from this file. Both take a single
|
|
5
|
+
* `content` string for one turn against a Genie space; for
|
|
6
|
+
* multi-turn conversations, the caller drives the loop and
|
|
7
|
+
* threads the `conversation_id` returned on each
|
|
8
|
+
* `GenieMessage` into the next call's `options.conversationId`.
|
|
9
|
+
*
|
|
10
|
+
* - {@link genieChat}: low-level. Yields every poll-observed
|
|
11
|
+
* `GenieMessage` verbatim. Cancellation, conversation seeding,
|
|
12
|
+
* distinct-filtering, and SDK quirks (Waiter stripping) live
|
|
13
|
+
* here. Use directly when you want the raw stream.
|
|
14
|
+
* - {@link genieEventChat}: high-level. Wraps `genieChat` and
|
|
15
|
+
* emits semantic, deduplicated events as a typed
|
|
16
|
+
* `{ type, payload }` stream (see {@link GenieChatEvent}). The
|
|
17
|
+
* final yield for a successful turn is always
|
|
18
|
+
* `{ type: "result", payload }` carrying the terminal
|
|
19
|
+
* `GenieMessage`. Errors propagate by the generator throwing -
|
|
20
|
+
* there is no `"error"` variant.
|
|
21
|
+
*
|
|
22
|
+
* Pick the layer that matches your consumer. Iterating UI / agent
|
|
23
|
+
* code that wants every message verbatim should use `genieChat`.
|
|
24
|
+
* Subscribers that want to react to "Genie is thinking about X"
|
|
25
|
+
* or "Genie produced text Y" should use `genieEventChat`.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import { WorkspaceClient } from "@databricks/sdk-experimental";
|
|
29
|
+
import {
|
|
30
|
+
eventsFromMessage,
|
|
31
|
+
isTerminalStatus,
|
|
32
|
+
type GenieChatEvent,
|
|
33
|
+
type GenieMessage,
|
|
34
|
+
} from "@dbx-tools/genie-shared";
|
|
35
|
+
import { apiUtils, commonUtils } from "@dbx-tools/shared";
|
|
36
|
+
|
|
37
|
+
/* -------------------------- shared options -------------------------- */
|
|
38
|
+
|
|
39
|
+
/** Options accepted by both {@link genieChat} and {@link genieEventChat}. */
|
|
40
|
+
export interface GenieChatOptions {
|
|
41
|
+
/**
|
|
42
|
+
* Seed conversation id. When set, this turn appends to the
|
|
43
|
+
* existing conversation (via `createMessage`) instead of opening
|
|
44
|
+
* a new one. Use it to thread a multi-turn conversation: read
|
|
45
|
+
* `conversation_id` off the prior turn's terminal `GenieMessage`
|
|
46
|
+
* (or the `result` event's `payload.conversation_id`) and pass
|
|
47
|
+
* it into the next call.
|
|
48
|
+
*/
|
|
49
|
+
conversationId?: string;
|
|
50
|
+
/**
|
|
51
|
+
* Explicit `WorkspaceClient`. Defaults to AppKit's per-request
|
|
52
|
+
* execution-context client when AppKit is installed and we're
|
|
53
|
+
* inside a request; falls back to a fresh `new WorkspaceClient({})`
|
|
54
|
+
* (env-var auth) otherwise.
|
|
55
|
+
*/
|
|
56
|
+
workspaceClient?: WorkspaceClient;
|
|
57
|
+
/** Poll cadence in milliseconds between successive `getMessage` calls (default 500). */
|
|
58
|
+
pollIntervalMs?: number;
|
|
59
|
+
/**
|
|
60
|
+
* External cancellation. Accepts a WHATWG `AbortSignal` or a
|
|
61
|
+
* fully-built SDK `Context` (see `apiUtils.ContextLike`).
|
|
62
|
+
* Aborting it cancels every in-flight SDK call and the next
|
|
63
|
+
* inter-poll sleep.
|
|
64
|
+
*/
|
|
65
|
+
context?: apiUtils.ContextLike;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/* ----------------------- low-level: genieChat ----------------------- */
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* One turn against a Genie space, yielded as a stream of
|
|
72
|
+
* `GenieMessage` snapshots.
|
|
73
|
+
*
|
|
74
|
+
* Turn lifecycle:
|
|
75
|
+
*
|
|
76
|
+
* - No `options.conversationId`: open a new conversation via
|
|
77
|
+
* `client.genie.startConversation`. The opened conversation id
|
|
78
|
+
* surfaces on every yielded `GenieMessage` (`.conversation_id`)
|
|
79
|
+
* so the caller can thread it into a follow-up call.
|
|
80
|
+
* - With `options.conversationId`: append to that conversation
|
|
81
|
+
* via `client.genie.createMessage`.
|
|
82
|
+
* - In both cases, after the create/start the driver polls
|
|
83
|
+
* `client.genie.getMessage` every `options.pollIntervalMs`
|
|
84
|
+
* (default 500ms) until the message reaches a terminal
|
|
85
|
+
* status, then yields the terminal snapshot and returns.
|
|
86
|
+
*
|
|
87
|
+
* Cancellation: a single internal `AbortController` covers the
|
|
88
|
+
* whole turn. `options.context` is tied into that controller so an
|
|
89
|
+
* external abort tears down every in-flight SDK call AND the
|
|
90
|
+
* inter-poll sleep. Breaking out of the `for await` does the same
|
|
91
|
+
* via the `try / finally`.
|
|
92
|
+
*
|
|
93
|
+
* @example
|
|
94
|
+
* // Single turn.
|
|
95
|
+
* for await (const m of genieChat(spaceId, "Top 5 stores?")) {
|
|
96
|
+
* render(m);
|
|
97
|
+
* }
|
|
98
|
+
*
|
|
99
|
+
* @example
|
|
100
|
+
* // Multi-turn: caller threads the conversation id.
|
|
101
|
+
* let conversationId: string | undefined;
|
|
102
|
+
* for (const question of questions) {
|
|
103
|
+
* for await (const m of genieChat(spaceId, question, { conversationId })) {
|
|
104
|
+
* conversationId = m.conversation_id ?? conversationId;
|
|
105
|
+
* render(m);
|
|
106
|
+
* }
|
|
107
|
+
* }
|
|
108
|
+
*/
|
|
109
|
+
export async function* genieChat(
|
|
110
|
+
space_id: string,
|
|
111
|
+
content: string,
|
|
112
|
+
options?: GenieChatOptions,
|
|
113
|
+
): AsyncGenerator<GenieMessage, void, void> {
|
|
114
|
+
const controller = new AbortController();
|
|
115
|
+
try {
|
|
116
|
+
const client = await getWorkspaceClient(options);
|
|
117
|
+
// Build the SDK Context ONCE. Building it inside the poll
|
|
118
|
+
// producer would re-attach an abort listener to
|
|
119
|
+
// `options.context` on every poll iteration (via
|
|
120
|
+
// `apiUtils.toContext` -> `tieAbortSignal`), eventually
|
|
121
|
+
// tripping Node's `MaxListenersExceededWarning`.
|
|
122
|
+
const context = apiUtils.toContext(controller, options?.context);
|
|
123
|
+
let conversationId = options?.conversationId;
|
|
124
|
+
let messageId: string | undefined;
|
|
125
|
+
|
|
126
|
+
const pollProducer = async (
|
|
127
|
+
ctx: commonUtils.PollContext<GenieMessage>,
|
|
128
|
+
): Promise<GenieMessage> => {
|
|
129
|
+
if (!conversationId) {
|
|
130
|
+
// First poll: open the conversation. Refuse to retry: if
|
|
131
|
+
// `startConversation` returned a response without a
|
|
132
|
+
// `conversation_id`, retrying would just open conversation
|
|
133
|
+
// after conversation.
|
|
134
|
+
if (ctx.attempt > 0) {
|
|
135
|
+
throw new Error(
|
|
136
|
+
"Genie did not return a conversation id; refusing to retry",
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
const startResponse = await client.genie.startConversation(
|
|
140
|
+
{ space_id, content },
|
|
141
|
+
context,
|
|
142
|
+
);
|
|
143
|
+
conversationId = startResponse.conversation_id;
|
|
144
|
+
messageId = startResponse.message_id;
|
|
145
|
+
return startResponse.message!;
|
|
146
|
+
}
|
|
147
|
+
if (!messageId) {
|
|
148
|
+
// First poll of a follow-up turn: append to the seeded
|
|
149
|
+
// conversation. `client.genie.createMessage` returns a
|
|
150
|
+
// `Waiter<GenieMessage>` (`{ ...message, wait: async () =>
|
|
151
|
+
// ... }`). Strip `wait` here so downstream serializers
|
|
152
|
+
// (e.g. yaml.stringify in poll-chat) don't choke on the
|
|
153
|
+
// AsyncFunction value.
|
|
154
|
+
const { wait: _wait, ...createResponse } =
|
|
155
|
+
await client.genie.createMessage(
|
|
156
|
+
{ space_id, conversation_id: conversationId, content },
|
|
157
|
+
context,
|
|
158
|
+
);
|
|
159
|
+
messageId = createResponse.message_id;
|
|
160
|
+
return createResponse;
|
|
161
|
+
}
|
|
162
|
+
// Subsequent polls: re-fetch the current message until its
|
|
163
|
+
// status becomes terminal.
|
|
164
|
+
return await client.genie.getMessage(
|
|
165
|
+
{
|
|
166
|
+
space_id,
|
|
167
|
+
conversation_id: conversationId,
|
|
168
|
+
message_id: messageId,
|
|
169
|
+
},
|
|
170
|
+
context,
|
|
171
|
+
);
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
yield* commonUtils.poll(pollProducer, {
|
|
175
|
+
intervalMs: options?.pollIntervalMs ?? 500,
|
|
176
|
+
// Skip yielding identical consecutive snapshots; Genie
|
|
177
|
+
// often returns the exact same payload twice during quiet
|
|
178
|
+
// periods. `poll` does a deep equal on the previous yield.
|
|
179
|
+
filter: "distinct",
|
|
180
|
+
// Stop after the terminal message is yielded. `poll` checks
|
|
181
|
+
// the predicate AFTER yielding, so the terminal message
|
|
182
|
+
// still reaches the consumer.
|
|
183
|
+
predicate: (m) => !isTerminalStatus(m.status),
|
|
184
|
+
// Wake the inter-poll sleep on abort so a `for await` break
|
|
185
|
+
// (or external abort) tears down promptly instead of waiting
|
|
186
|
+
// out the interval.
|
|
187
|
+
signal: controller.signal,
|
|
188
|
+
});
|
|
189
|
+
} finally {
|
|
190
|
+
// Cancels any still-pending SDK call and the inter-poll sleep
|
|
191
|
+
// whether we're unwinding from a normal return, a consumer
|
|
192
|
+
// break, or a thrown error. Idempotent.
|
|
193
|
+
controller.abort();
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/* ---------------------- high-level: genieEventChat ------------------ */
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* One turn against a Genie space, yielded as a typed
|
|
201
|
+
* {@link GenieChatEvent} stream. Drives {@link genieChat}
|
|
202
|
+
* underneath and decorates each snapshot with the derived events
|
|
203
|
+
* the field-level diff produced. Stream order:
|
|
204
|
+
*
|
|
205
|
+
* 1. `{ type: "message", message }` - the raw `GenieMessage`,
|
|
206
|
+
* once per poll yield.
|
|
207
|
+
* 2. `{ type: "question", content, message_id, ... }` fires
|
|
208
|
+
* exactly once, on the FIRST `message` yield. We read
|
|
209
|
+
* `content` and `message_id` straight off the snapshot so
|
|
210
|
+
* every downstream event for this turn shares the same
|
|
211
|
+
* `message_id` (the question included) - subscribers can
|
|
212
|
+
* group everything for one Genie call under that one key.
|
|
213
|
+
* 3. Any of `status` / `attachment` / `thinking` / `text` /
|
|
214
|
+
* `query` / `statement` / `rows` / `suggested_questions` the
|
|
215
|
+
* diff against the prior snapshot produced.
|
|
216
|
+
* 4. On the terminal snapshot, `{ type: "result", ... }` as
|
|
217
|
+
* the final yield.
|
|
218
|
+
*
|
|
219
|
+
* Errors propagate by the generator throwing - there's no
|
|
220
|
+
* `"error"` variant. Wrap the `for await` in `try/catch` if you
|
|
221
|
+
* need to handle failures.
|
|
222
|
+
*
|
|
223
|
+
* @example
|
|
224
|
+
* for await (const event of genieEventChat(spaceId, "Top stores?")) {
|
|
225
|
+
* switch (event.type) {
|
|
226
|
+
* case "thinking":
|
|
227
|
+
* console.log("[thinking]", event.thought_type, event.text);
|
|
228
|
+
* break;
|
|
229
|
+
* case "text":
|
|
230
|
+
* console.log("[text]", event.text);
|
|
231
|
+
* break;
|
|
232
|
+
* case "result":
|
|
233
|
+
* console.log("[done]", event.status);
|
|
234
|
+
* break;
|
|
235
|
+
* }
|
|
236
|
+
* }
|
|
237
|
+
*/
|
|
238
|
+
export async function* genieEventChat(
|
|
239
|
+
space_id: string,
|
|
240
|
+
content: string,
|
|
241
|
+
options?: GenieChatOptions,
|
|
242
|
+
): AsyncGenerator<GenieChatEvent, void, void> {
|
|
243
|
+
// Diff source for the current turn. Always `undefined` on the
|
|
244
|
+
// first snapshot so the initial status / attachments emit
|
|
245
|
+
// fresh; updated to the most recent snapshot after each yield.
|
|
246
|
+
let previous: GenieMessage | undefined;
|
|
247
|
+
// The `question` event is deferred to the first `message` yield
|
|
248
|
+
// so it can carry the assigned `message_id` (subscribers use it
|
|
249
|
+
// as the grouping key for every event in this turn). The first
|
|
250
|
+
// snapshot is the earliest point that id exists.
|
|
251
|
+
let questionEmitted = false;
|
|
252
|
+
for await (const message of genieChat(space_id, content, options)) {
|
|
253
|
+
yield { type: "message", message };
|
|
254
|
+
if (!questionEmitted) {
|
|
255
|
+
yield {
|
|
256
|
+
type: "question",
|
|
257
|
+
space_id: message.space_id,
|
|
258
|
+
...(message.conversation_id
|
|
259
|
+
? { conversation_id: message.conversation_id }
|
|
260
|
+
: {}),
|
|
261
|
+
...(message.message_id ? { message_id: message.message_id } : {}),
|
|
262
|
+
content: message.content,
|
|
263
|
+
};
|
|
264
|
+
questionEmitted = true;
|
|
265
|
+
}
|
|
266
|
+
yield* eventsFromMessage(message, previous, message.space_id);
|
|
267
|
+
if (isTerminalStatus(message.status)) {
|
|
268
|
+
yield {
|
|
269
|
+
type: "result",
|
|
270
|
+
space_id: message.space_id,
|
|
271
|
+
conversation_id: message.conversation_id,
|
|
272
|
+
message_id: message.message_id,
|
|
273
|
+
status: message.status,
|
|
274
|
+
message,
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
previous = message;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/* ---------------------- workspace client helper --------------------- */
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Resolve a `WorkspaceClient` in this preference order:
|
|
285
|
+
*
|
|
286
|
+
* 1. Caller-supplied `options.workspaceClient`.
|
|
287
|
+
* 2. AppKit's per-request execution-context client, when AppKit
|
|
288
|
+
* is installed AND we're inside a request scope.
|
|
289
|
+
* 3. Fresh `new WorkspaceClient({})` (env-var auth via
|
|
290
|
+
* `DATABRICKS_CONFIG_PROFILE` / `DATABRICKS_HOST` /
|
|
291
|
+
* `DATABRICKS_TOKEN`).
|
|
292
|
+
*
|
|
293
|
+
* AppKit is loaded lazily so this package stays usable in
|
|
294
|
+
* non-AppKit environments (e.g. the `poll-chat` smoke test).
|
|
295
|
+
*/
|
|
296
|
+
async function getWorkspaceClient(
|
|
297
|
+
options?: GenieChatOptions,
|
|
298
|
+
): Promise<WorkspaceClient> {
|
|
299
|
+
if (options?.workspaceClient) return options.workspaceClient;
|
|
300
|
+
const appkit = await getAppKit();
|
|
301
|
+
if (appkit) {
|
|
302
|
+
try {
|
|
303
|
+
return appkit.getExecutionContext().client;
|
|
304
|
+
} catch {
|
|
305
|
+
// Not inside an AppKit request context; fall through to env.
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
return new WorkspaceClient({});
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
async function getAppKit() {
|
|
312
|
+
try {
|
|
313
|
+
return await import("@databricks/appkit");
|
|
314
|
+
} catch {
|
|
315
|
+
return undefined;
|
|
316
|
+
}
|
|
317
|
+
}
|