@dbx-tools/genie 0.1.58 → 0.1.67
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 +31 -6
- package/dist/index.d.ts +155 -15
- package/dist/index.js +316 -16
- package/package.json +11 -23
- package/{index.ts → src/index.ts} +2 -2
- package/dist/src/chat.d.ts +0 -134
- package/dist/src/chat.js +0 -284
- package/dist/src/space.d.ts +0 -55
- package/dist/src/space.js +0 -90
- package/dist/tsconfig.build.tsbuildinfo +0 -1
package/README.md
CHANGED
|
@@ -104,16 +104,41 @@ Errors propagate by the generator throwing - there is no `error`
|
|
|
104
104
|
variant. Wrap the `for await` in `try / catch` if you need to handle
|
|
105
105
|
failures.
|
|
106
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.
|
|
124
|
+
|
|
125
|
+
```ts
|
|
126
|
+
import { getGenieSpace, genieSampleQuestions } from "@dbx-tools/genie";
|
|
127
|
+
|
|
128
|
+
const space = await getGenieSpace(spaceId);
|
|
129
|
+
const prompts = genieSampleQuestions(space); // string[]
|
|
130
|
+
```
|
|
131
|
+
|
|
107
132
|
## Options
|
|
108
133
|
|
|
109
134
|
`GenieChatOptions` is the same shape for both drivers:
|
|
110
135
|
|
|
111
|
-
| Option | Default
|
|
112
|
-
| ----------------- |
|
|
113
|
-
| `conversationId` | `undefined`
|
|
114
|
-
| `workspaceClient` | resolved (see below)
|
|
115
|
-
| `pollIntervalMs` | `500`
|
|
116
|
-
| `context` | new internal `AbortController` per call
|
|
136
|
+
| Option | Default | Description |
|
|
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`). |
|
|
117
142
|
|
|
118
143
|
### Workspace client resolution
|
|
119
144
|
|
package/dist/index.d.ts
CHANGED
|
@@ -1,17 +1,157 @@
|
|
|
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
|
+
}
|
|
1
35
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
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
|
+
* }
|
|
14
73
|
*/
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
CHANGED
|
@@ -1,17 +1,317 @@
|
|
|
1
|
+
import { GenieMessageSchema, GenieSpaceSchema, eventsFromMessage, isTerminalStatus } from "@dbx-tools/genie-shared";
|
|
2
|
+
import { WorkspaceClient } from "@databricks/sdk-experimental";
|
|
3
|
+
import { apiUtils, commonUtils, logUtils } from "@dbx-tools/shared";
|
|
4
|
+
|
|
5
|
+
export * from "@dbx-tools/genie-shared"
|
|
6
|
+
|
|
7
|
+
//#region packages/genie/src/chat.ts
|
|
1
8
|
/**
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
+
/** Pull the first non-empty string out of a `question` field (string | string[]). */
|
|
272
|
+
function questionText(question) {
|
|
273
|
+
if (typeof question === "string") {
|
|
274
|
+
const trimmed = question.trim();
|
|
275
|
+
return trimmed.length > 0 ? trimmed : void 0;
|
|
276
|
+
}
|
|
277
|
+
if (Array.isArray(question)) {
|
|
278
|
+
for (const part of question) if (typeof part === "string" && part.trim().length > 0) return part.trim();
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* Extract the curated starter questions an author configured on a
|
|
283
|
+
* Genie space. Reads `serialized_space -> config.sample_questions[*]
|
|
284
|
+
* .question`. Returns `[]` when the space carries no serialized blob,
|
|
285
|
+
* the blob is unparseable, or no sample questions are configured -
|
|
286
|
+
* so a missing or misconfigured space degrades to "no suggestions"
|
|
287
|
+
* rather than throwing. Order is preserved (the author's ordering)
|
|
288
|
+
* and duplicates are dropped.
|
|
289
|
+
*/
|
|
290
|
+
function genieSampleQuestions(space) {
|
|
291
|
+
const serialized = space.serialized_space;
|
|
292
|
+
if (!serialized) return [];
|
|
293
|
+
let parsed;
|
|
294
|
+
try {
|
|
295
|
+
parsed = JSON.parse(serialized);
|
|
296
|
+
} catch (err) {
|
|
297
|
+
log.warn("serialized-space:parse-error", {
|
|
298
|
+
spaceId: space.space_id,
|
|
299
|
+
error: commonUtils.errorMessage(err)
|
|
300
|
+
});
|
|
301
|
+
return [];
|
|
302
|
+
}
|
|
303
|
+
const sampleQuestions = parsed?.config?.sample_questions;
|
|
304
|
+
if (!Array.isArray(sampleQuestions)) return [];
|
|
305
|
+
const seen = /* @__PURE__ */ new Set();
|
|
306
|
+
const out = [];
|
|
307
|
+
for (const entry of sampleQuestions) {
|
|
308
|
+
const text = questionText(entry?.question);
|
|
309
|
+
if (!text || seen.has(text)) continue;
|
|
310
|
+
seen.add(text);
|
|
311
|
+
out.push(text);
|
|
312
|
+
}
|
|
313
|
+
return out;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
//#endregion
|
|
317
|
+
export { genieChat, genieEventChat, genieSampleQuestions, getGenieSpace };
|
package/package.json
CHANGED
|
@@ -1,36 +1,24 @@
|
|
|
1
1
|
{
|
|
2
|
-
"
|
|
3
|
-
"
|
|
2
|
+
"name": "@dbx-tools/genie",
|
|
3
|
+
"version": "0.1.67",
|
|
4
|
+
"dependencies": {
|
|
5
|
+
"@databricks/sdk-experimental": "^0.17",
|
|
6
|
+
"@dbx-tools/genie-shared": "0.1.67",
|
|
7
|
+
"@dbx-tools/shared": "0.1.67"
|
|
8
|
+
},
|
|
4
9
|
"exports": {
|
|
5
10
|
".": {
|
|
6
|
-
"source": "./index.ts",
|
|
11
|
+
"source": "./src/index.ts",
|
|
7
12
|
"types": "./dist/index.d.ts",
|
|
8
13
|
"default": "./dist/index.js"
|
|
9
14
|
}
|
|
10
15
|
},
|
|
11
|
-
"name": "@dbx-tools/genie",
|
|
12
|
-
"version": "0.1.58",
|
|
13
|
-
"dependencies": {
|
|
14
|
-
"@databricks/sdk-experimental": "^0.17",
|
|
15
|
-
"@dbx-tools/genie-shared": "0.1.58",
|
|
16
|
-
"@dbx-tools/shared": "0.1.58"
|
|
17
|
-
},
|
|
18
|
-
"module": "index.ts",
|
|
19
16
|
"type": "module",
|
|
17
|
+
"main": "./dist/index.js",
|
|
18
|
+
"types": "./dist/index.d.ts",
|
|
20
19
|
"files": [
|
|
21
|
-
"bin",
|
|
22
20
|
"dist",
|
|
23
|
-
"index*.ts",
|
|
24
21
|
"src"
|
|
25
22
|
],
|
|
26
|
-
"license": "Apache-2.0"
|
|
27
|
-
"homepage": "https://github.com/reggie-db/dbx-tools-js#readme",
|
|
28
|
-
"bugs": {
|
|
29
|
-
"url": "https://github.com/reggie-db/dbx-tools-js/issues"
|
|
30
|
-
},
|
|
31
|
-
"repository": {
|
|
32
|
-
"type": "git",
|
|
33
|
-
"url": "git+https://github.com/reggie-db/dbx-tools-js.git",
|
|
34
|
-
"directory": "packages/genie"
|
|
35
|
-
}
|
|
23
|
+
"license": "Apache-2.0"
|
|
36
24
|
}
|