@dbx-tools/appkit-generator 0.0.0-stage → 0.9.138
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/LICENSE +202 -0
- package/README.md +181 -2
- package/index.ts +44 -0
- package/lib/index.d.ts +40 -0
- package/lib/index.js +30 -0
- package/lib/src/assets.d.ts +33 -0
- package/lib/src/assets.js +224 -0
- package/lib/src/config.d.ts +75 -0
- package/lib/src/config.js +31 -0
- package/lib/src/dashboard.d.ts +30 -0
- package/lib/src/dashboard.js +257 -0
- package/lib/src/data.d.ts +17 -0
- package/lib/src/data.js +114 -0
- package/lib/src/defaults.d.ts +39 -0
- package/lib/src/defaults.js +36 -0
- package/lib/src/generate.d.ts +51 -0
- package/lib/src/generate.js +163 -0
- package/lib/src/genie.d.ts +51 -0
- package/lib/src/genie.js +84 -0
- package/lib/src/kpi.d.ts +22 -0
- package/lib/src/kpi.js +74 -0
- package/lib/src/manifest.json +72 -0
- package/lib/src/namespace.d.ts +36 -0
- package/lib/src/namespace.js +119 -0
- package/lib/src/operations.d.ts +42 -0
- package/lib/src/operations.js +96 -0
- package/lib/src/palette.d.ts +10 -0
- package/lib/src/palette.js +69 -0
- package/lib/src/plan.d.ts +21 -0
- package/lib/src/plan.js +33 -0
- package/lib/src/plugin.d.ts +619 -0
- package/lib/src/plugin.js +205 -0
- package/lib/src/research.d.ts +78 -0
- package/lib/src/research.js +235 -0
- package/lib/src/schema.d.ts +52 -0
- package/lib/src/schema.js +82 -0
- package/lib/src/structured.d.ts +27 -0
- package/lib/src/structured.js +30 -0
- package/lib/src/workflow.d.ts +314 -0
- package/lib/src/workflow.js +171 -0
- package/lib/tsconfig.tsbuildinfo +1 -0
- package/package.json +218 -4
- package/src/assets.ts +248 -0
- package/src/config.ts +98 -0
- package/src/dashboard.ts +295 -0
- package/src/data.ts +167 -0
- package/src/defaults.ts +61 -0
- package/src/generate.ts +250 -0
- package/src/genie.ts +111 -0
- package/src/kpi.ts +106 -0
- package/src/manifest.json +72 -0
- package/src/namespace.ts +192 -0
- package/src/operations.ts +171 -0
- package/src/palette.ts +81 -0
- package/src/plan.ts +59 -0
- package/src/plugin.ts +244 -0
- package/src/research.ts +382 -0
- package/src/schema.ts +114 -0
- package/src/structured.ts +54 -0
- package/src/workflow.ts +221 -0
package/src/config.ts
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AppKit generator-plugin configuration and environment resolution.
|
|
3
|
+
*
|
|
4
|
+
* @module
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { BasePluginConfig } from "@databricks/appkit";
|
|
8
|
+
import { configUtils } from "@dbx-tools/core";
|
|
9
|
+
import { stringUtils } from "@dbx-tools/shared-core";
|
|
10
|
+
import manifest from "./manifest.json" with { type: "json" };
|
|
11
|
+
import {
|
|
12
|
+
DEFAULT_MAX_SOURCES,
|
|
13
|
+
DEFAULT_SOURCE_MAX_LENGTH,
|
|
14
|
+
MAX_SOURCES,
|
|
15
|
+
MAX_SOURCE_MAX_LENGTH,
|
|
16
|
+
} from "./research.ts";
|
|
17
|
+
|
|
18
|
+
/** Default ceiling for one complete generator operation. */
|
|
19
|
+
export const DEFAULT_GENERATOR_TIMEOUT_MS = 240_000;
|
|
20
|
+
|
|
21
|
+
/** AppKit configuration accepted by the generator plugin. */
|
|
22
|
+
export interface GeneratorPluginConfig extends BasePluginConfig {
|
|
23
|
+
/** Model endpoint or loose model name used for structured synthesis. */
|
|
24
|
+
model?: string;
|
|
25
|
+
/** Maximum cited pages fetched per research operation. */
|
|
26
|
+
maxSources?: number;
|
|
27
|
+
/** Maximum readable characters retained from each fetched page. */
|
|
28
|
+
sourceMaxLength?: number;
|
|
29
|
+
/** Overall operation timeout in milliseconds. */
|
|
30
|
+
timeoutMs?: number;
|
|
31
|
+
/** Unity Catalog containing all generated per-user schemas. */
|
|
32
|
+
catalog?: string;
|
|
33
|
+
/** SQL warehouse used for data generation and validation. */
|
|
34
|
+
warehouseId?: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Fully resolved generator configuration. */
|
|
38
|
+
export interface ResolvedGeneratorConfig {
|
|
39
|
+
/** Optional pinned synthesis model. */
|
|
40
|
+
model?: string;
|
|
41
|
+
/** Bounded cited-page count. */
|
|
42
|
+
maxSources: number;
|
|
43
|
+
/** Character cap per fetched source. */
|
|
44
|
+
sourceMaxLength: number;
|
|
45
|
+
/** Overall operation timeout in milliseconds. */
|
|
46
|
+
timeoutMs: number;
|
|
47
|
+
/** Unity Catalog containing generated schemas, when configured. */
|
|
48
|
+
catalog?: string;
|
|
49
|
+
/** SQL warehouse used for mutations, when configured. */
|
|
50
|
+
warehouseId?: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** JSON Schema published by the generator plugin manifest. */
|
|
54
|
+
export const GENERATOR_CONFIG_SCHEMA = manifest.config.schema;
|
|
55
|
+
|
|
56
|
+
/** Resolve explicit plugin values over environment values and defaults. */
|
|
57
|
+
export function resolveGeneratorConfig(
|
|
58
|
+
config: GeneratorPluginConfig = {},
|
|
59
|
+
): ResolvedGeneratorConfig {
|
|
60
|
+
const model =
|
|
61
|
+
stringUtils.trimToNull(config.model) ??
|
|
62
|
+
configUtils.text("GENERATOR_MODEL", configUtils.ENV_ONLY);
|
|
63
|
+
const catalog =
|
|
64
|
+
stringUtils.trimToNull(config.catalog) ??
|
|
65
|
+
configUtils.text("GENERATOR_CATALOG", configUtils.ENV_ONLY);
|
|
66
|
+
const warehouseId =
|
|
67
|
+
stringUtils.trimToNull(config.warehouseId) ??
|
|
68
|
+
configUtils.text("DATABRICKS_WAREHOUSE_ID", configUtils.ENV_ONLY);
|
|
69
|
+
return {
|
|
70
|
+
...(model ? { model } : {}),
|
|
71
|
+
...(catalog ? { catalog } : {}),
|
|
72
|
+
...(warehouseId ? { warehouseId } : {}),
|
|
73
|
+
maxSources: Math.min(
|
|
74
|
+
configUtils.positiveInt(
|
|
75
|
+
config.maxSources,
|
|
76
|
+
"GENERATOR_MAX_SOURCES",
|
|
77
|
+
DEFAULT_MAX_SOURCES,
|
|
78
|
+
configUtils.ENV_ONLY,
|
|
79
|
+
),
|
|
80
|
+
MAX_SOURCES,
|
|
81
|
+
),
|
|
82
|
+
sourceMaxLength: Math.min(
|
|
83
|
+
configUtils.positiveInt(
|
|
84
|
+
config.sourceMaxLength,
|
|
85
|
+
"GENERATOR_SOURCE_MAX_LENGTH",
|
|
86
|
+
DEFAULT_SOURCE_MAX_LENGTH,
|
|
87
|
+
configUtils.ENV_ONLY,
|
|
88
|
+
),
|
|
89
|
+
MAX_SOURCE_MAX_LENGTH,
|
|
90
|
+
),
|
|
91
|
+
timeoutMs: configUtils.positiveInt(
|
|
92
|
+
config.timeoutMs,
|
|
93
|
+
"GENERATOR_TIMEOUT_MS",
|
|
94
|
+
DEFAULT_GENERATOR_TIMEOUT_MS,
|
|
95
|
+
configUtils.ENV_ONLY,
|
|
96
|
+
),
|
|
97
|
+
};
|
|
98
|
+
}
|
package/src/dashboard.ts
ADDED
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deterministic AI/BI dashboard assembly and OBO publication.
|
|
3
|
+
*
|
|
4
|
+
* @module
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { ExecutionError } from "@databricks/appkit";
|
|
8
|
+
import { databricks } from "@dbx-tools/appkit";
|
|
9
|
+
import { hash } from "@dbx-tools/shared-core";
|
|
10
|
+
import type { BrandContext } from "@dbx-tools/shared-core/brand-utils";
|
|
11
|
+
import {
|
|
12
|
+
GeneratedDashboardSchema,
|
|
13
|
+
type GeneratedDashboard,
|
|
14
|
+
type GeneratedMetric,
|
|
15
|
+
type GeneratedSchema,
|
|
16
|
+
type GeneratorPlan,
|
|
17
|
+
} from "@dbx-tools/shared-generator/workflow";
|
|
18
|
+
import { z } from "zod";
|
|
19
|
+
import { resolveGeneratorIdentity } from "./namespace.ts";
|
|
20
|
+
|
|
21
|
+
const SerializedDashboardSchema = z
|
|
22
|
+
.object({
|
|
23
|
+
datasets: z.array(z.record(z.string(), z.unknown())).min(1),
|
|
24
|
+
pages: z.array(z.record(z.string(), z.unknown())).min(1),
|
|
25
|
+
uiSettings: z.record(z.string(), z.unknown()),
|
|
26
|
+
})
|
|
27
|
+
.strict();
|
|
28
|
+
|
|
29
|
+
/** Inputs for dashboard assembly and publication. */
|
|
30
|
+
export interface CreateGeneratorDashboardInput {
|
|
31
|
+
/** Generated schema containing the fact table. */
|
|
32
|
+
location: GeneratedSchema;
|
|
33
|
+
/** Validated data and metric plan. */
|
|
34
|
+
plan: GeneratorPlan;
|
|
35
|
+
/** Brand applied to the dashboard theme. */
|
|
36
|
+
brand: BrandContext;
|
|
37
|
+
/** SQL warehouse used by the dashboard. */
|
|
38
|
+
warehouseId: string;
|
|
39
|
+
/** Company display name. */
|
|
40
|
+
company: string;
|
|
41
|
+
/** Demo purpose. */
|
|
42
|
+
purpose: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Compile one metric into a reusable Lakeview dataset measure. */
|
|
46
|
+
function measure(metric: GeneratedMetric) {
|
|
47
|
+
const expression =
|
|
48
|
+
metric.aggregation === "count"
|
|
49
|
+
? "COUNT(`*`)"
|
|
50
|
+
: `${metric.aggregation === "sum" ? "SUM" : "AVG"}(\`${metric.column}\`)`;
|
|
51
|
+
return {
|
|
52
|
+
displayName: metric.label,
|
|
53
|
+
description: metric.definition,
|
|
54
|
+
expression,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Counter widget bound to one dataset measure. */
|
|
59
|
+
function counterWidget(metric: GeneratedMetric, index: number) {
|
|
60
|
+
const fieldName = `measure(${metric.label})`;
|
|
61
|
+
return {
|
|
62
|
+
widget: {
|
|
63
|
+
name: `kpi_${metric.name}`,
|
|
64
|
+
queries: [
|
|
65
|
+
{
|
|
66
|
+
name: "main_query",
|
|
67
|
+
query: {
|
|
68
|
+
datasetName: "generated_facts",
|
|
69
|
+
fields: [{ name: fieldName, expression: `MEASURE(\`${metric.label}\`)` }],
|
|
70
|
+
disaggregated: false,
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
],
|
|
74
|
+
spec: {
|
|
75
|
+
version: 2,
|
|
76
|
+
widgetType: "counter",
|
|
77
|
+
encodings: {
|
|
78
|
+
value: {
|
|
79
|
+
fieldName,
|
|
80
|
+
displayName: metric.label,
|
|
81
|
+
...(metric.unit === "percent"
|
|
82
|
+
? {
|
|
83
|
+
format: {
|
|
84
|
+
type: "number-percent",
|
|
85
|
+
decimalPlaces: { type: "max", places: 2 },
|
|
86
|
+
},
|
|
87
|
+
}
|
|
88
|
+
: {}),
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
frame: { showTitle: true, title: metric.label },
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
position: {
|
|
95
|
+
x: (index % 3) * 4,
|
|
96
|
+
y: 2 + Math.floor(index / 3) * 3,
|
|
97
|
+
width: 4,
|
|
98
|
+
height: 3,
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Build one complete dashboard with automatic companion Genie enabled. */
|
|
104
|
+
export function buildGeneratorDashboard(input: CreateGeneratorDashboardInput) {
|
|
105
|
+
const fullTable = `${input.location.catalog}.${input.location.schema}.${input.plan.table}`;
|
|
106
|
+
const dateColumn = input.plan.columns.find(({ generator }) => generator === "date")?.name;
|
|
107
|
+
const categoryColumn = input.plan.columns.find(({ generator }) => generator === "category")?.name;
|
|
108
|
+
const metric = input.plan.metrics[0]!;
|
|
109
|
+
const metricField = `measure(${metric.label})`;
|
|
110
|
+
const kpiRows = Math.ceil(input.plan.metrics.length / 3);
|
|
111
|
+
const chartY = 2 + kpiRows * 3;
|
|
112
|
+
const layout: Record<string, unknown>[] = [
|
|
113
|
+
{
|
|
114
|
+
widget: {
|
|
115
|
+
name: "header",
|
|
116
|
+
multilineTextboxSpec: {
|
|
117
|
+
lines: [`# ${input.company}: ${input.purpose}\n`, `\n`, input.plan.story],
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
position: { x: 0, y: 0, width: 12, height: 2 },
|
|
121
|
+
},
|
|
122
|
+
...input.plan.metrics.map(counterWidget),
|
|
123
|
+
];
|
|
124
|
+
if (dateColumn) {
|
|
125
|
+
layout.push({
|
|
126
|
+
widget: {
|
|
127
|
+
name: "metric_trend",
|
|
128
|
+
queries: [
|
|
129
|
+
{
|
|
130
|
+
name: "main_query",
|
|
131
|
+
query: {
|
|
132
|
+
datasetName: "generated_facts",
|
|
133
|
+
fields: [
|
|
134
|
+
{
|
|
135
|
+
name: `daily(${dateColumn})`,
|
|
136
|
+
expression: `DATE_TRUNC("DAY", \`${dateColumn}\`)`,
|
|
137
|
+
},
|
|
138
|
+
{ name: metricField, expression: `MEASURE(\`${metric.label}\`)` },
|
|
139
|
+
],
|
|
140
|
+
disaggregated: false,
|
|
141
|
+
},
|
|
142
|
+
},
|
|
143
|
+
],
|
|
144
|
+
spec: {
|
|
145
|
+
version: 3,
|
|
146
|
+
widgetType: "line",
|
|
147
|
+
encodings: {
|
|
148
|
+
x: {
|
|
149
|
+
fieldName: `daily(${dateColumn})`,
|
|
150
|
+
scale: { type: "temporal" },
|
|
151
|
+
displayName: "Date",
|
|
152
|
+
},
|
|
153
|
+
y: {
|
|
154
|
+
fieldName: metricField,
|
|
155
|
+
scale: { type: "quantitative" },
|
|
156
|
+
displayName: metric.label,
|
|
157
|
+
},
|
|
158
|
+
},
|
|
159
|
+
frame: { showTitle: true, title: `${metric.label} trend` },
|
|
160
|
+
},
|
|
161
|
+
},
|
|
162
|
+
position: { x: 0, y: chartY, width: 7, height: 6 },
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
if (categoryColumn) {
|
|
166
|
+
layout.push({
|
|
167
|
+
widget: {
|
|
168
|
+
name: "metric_by_category",
|
|
169
|
+
queries: [
|
|
170
|
+
{
|
|
171
|
+
name: "main_query",
|
|
172
|
+
query: {
|
|
173
|
+
datasetName: "generated_facts",
|
|
174
|
+
fields: [
|
|
175
|
+
{ name: categoryColumn, expression: `\`${categoryColumn}\`` },
|
|
176
|
+
{ name: metricField, expression: `MEASURE(\`${metric.label}\`)` },
|
|
177
|
+
],
|
|
178
|
+
disaggregated: false,
|
|
179
|
+
},
|
|
180
|
+
},
|
|
181
|
+
],
|
|
182
|
+
spec: {
|
|
183
|
+
version: 3,
|
|
184
|
+
widgetType: "bar",
|
|
185
|
+
encodings: {
|
|
186
|
+
x: {
|
|
187
|
+
fieldName: categoryColumn,
|
|
188
|
+
scale: { type: "categorical" },
|
|
189
|
+
displayName: categoryColumn,
|
|
190
|
+
},
|
|
191
|
+
y: {
|
|
192
|
+
fieldName: metricField,
|
|
193
|
+
scale: { type: "quantitative" },
|
|
194
|
+
displayName: metric.label,
|
|
195
|
+
},
|
|
196
|
+
},
|
|
197
|
+
frame: { showTitle: true, title: `${metric.label} by ${categoryColumn}` },
|
|
198
|
+
},
|
|
199
|
+
},
|
|
200
|
+
position: { x: 7, y: chartY, width: 5, height: 6 },
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
return SerializedDashboardSchema.parse({
|
|
204
|
+
datasets: [
|
|
205
|
+
{
|
|
206
|
+
name: "generated_facts",
|
|
207
|
+
displayName: input.plan.table,
|
|
208
|
+
asset_name: fullTable,
|
|
209
|
+
columns: input.plan.metrics.map(measure),
|
|
210
|
+
},
|
|
211
|
+
],
|
|
212
|
+
pages: [
|
|
213
|
+
{
|
|
214
|
+
name: "overview",
|
|
215
|
+
displayName: "Overview",
|
|
216
|
+
pageType: "PAGE_TYPE_CANVAS",
|
|
217
|
+
layoutVersion: "GRID_V1",
|
|
218
|
+
layout,
|
|
219
|
+
},
|
|
220
|
+
],
|
|
221
|
+
uiSettings: {
|
|
222
|
+
theme: {
|
|
223
|
+
canvasBackgroundColor: {
|
|
224
|
+
light: input.brand.colors.background,
|
|
225
|
+
dark: input.brand.colors.foreground,
|
|
226
|
+
},
|
|
227
|
+
widgetBackgroundColor: {
|
|
228
|
+
light: input.brand.colors.surface,
|
|
229
|
+
dark: input.brand.colors.primary,
|
|
230
|
+
},
|
|
231
|
+
fontColor: {
|
|
232
|
+
light: input.brand.colors.foreground,
|
|
233
|
+
dark: input.brand.colors.background,
|
|
234
|
+
},
|
|
235
|
+
selectionColor: {
|
|
236
|
+
light: input.brand.colors.primary,
|
|
237
|
+
dark: input.brand.colors.accent,
|
|
238
|
+
},
|
|
239
|
+
visualizationColors: [
|
|
240
|
+
input.brand.colors.primary,
|
|
241
|
+
input.brand.colors.accent,
|
|
242
|
+
input.brand.colors.primaryHover,
|
|
243
|
+
input.brand.colors.muted,
|
|
244
|
+
],
|
|
245
|
+
widgetHeaderAlignment: "LEFT",
|
|
246
|
+
},
|
|
247
|
+
genieSpace: {
|
|
248
|
+
isEnabled: true,
|
|
249
|
+
enablementMode: "AUTO",
|
|
250
|
+
},
|
|
251
|
+
},
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/** Create and publish a dashboard as the active OBO user. */
|
|
256
|
+
export async function createGeneratorDashboard(
|
|
257
|
+
input: CreateGeneratorDashboardInput,
|
|
258
|
+
signal?: AbortSignal,
|
|
259
|
+
): Promise<GeneratedDashboard> {
|
|
260
|
+
const identity = await resolveGeneratorIdentity();
|
|
261
|
+
if (identity.principal !== input.location.principal) {
|
|
262
|
+
throw new ExecutionError("Dashboard principal does not match the active OBO user");
|
|
263
|
+
}
|
|
264
|
+
const legacy = identity.userClient.toLegacyWorkspaceClient();
|
|
265
|
+
const parentPath = `/Workspace/Users/${identity.principal}/.dbx-tools/generator/${input.location.schema}`;
|
|
266
|
+
const context = signal ? databricks.toContext(signal) : undefined;
|
|
267
|
+
await legacy.workspace.mkdirs({ path: parentPath }, context);
|
|
268
|
+
const created = await legacy.lakeview.create(
|
|
269
|
+
{
|
|
270
|
+
dashboard: {
|
|
271
|
+
display_name: `${input.company} ${input.purpose} ${hash.id(6)}`,
|
|
272
|
+
parent_path: parentPath,
|
|
273
|
+
warehouse_id: input.warehouseId,
|
|
274
|
+
serialized_dashboard: JSON.stringify(buildGeneratorDashboard(input)),
|
|
275
|
+
},
|
|
276
|
+
},
|
|
277
|
+
context,
|
|
278
|
+
);
|
|
279
|
+
if (!created.dashboard_id) {
|
|
280
|
+
throw new ExecutionError("Lakeview create returned no dashboard id");
|
|
281
|
+
}
|
|
282
|
+
await legacy.lakeview.publish(
|
|
283
|
+
{
|
|
284
|
+
dashboard_id: created.dashboard_id,
|
|
285
|
+
warehouse_id: input.warehouseId,
|
|
286
|
+
embed_credentials: false,
|
|
287
|
+
},
|
|
288
|
+
context,
|
|
289
|
+
);
|
|
290
|
+
return GeneratedDashboardSchema.parse({
|
|
291
|
+
dashboardId: created.dashboard_id,
|
|
292
|
+
...(created.path ? { path: created.path } : {}),
|
|
293
|
+
companionGenie: "AUTO",
|
|
294
|
+
});
|
|
295
|
+
}
|
package/src/data.ts
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deterministic Databricks SQL compilation, execution, and validation.
|
|
3
|
+
*
|
|
4
|
+
* @module
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { ExecutionError, type WorkspaceClient } from "@databricks/appkit";
|
|
8
|
+
import { databricks } from "@dbx-tools/appkit";
|
|
9
|
+
import { asyncUtils } from "@dbx-tools/shared-core";
|
|
10
|
+
import { MaterializedGeneratorDataSchema } from "@dbx-tools/shared-generator/workflow";
|
|
11
|
+
import type {
|
|
12
|
+
GeneratedColumn,
|
|
13
|
+
GeneratedMetric,
|
|
14
|
+
GeneratedSchema,
|
|
15
|
+
GeneratorPlan,
|
|
16
|
+
MaterializedGeneratorData,
|
|
17
|
+
} from "@dbx-tools/shared-generator/workflow";
|
|
18
|
+
import { resolveGeneratorIdentity } from "./namespace.ts";
|
|
19
|
+
|
|
20
|
+
const TERMINAL_STATEMENT_STATES = new Set(["SUCCEEDED", "FAILED", "CANCELED", "CLOSED"]);
|
|
21
|
+
type StatementResponse = Awaited<ReturnType<WorkspaceClient["statementExecution"]["getStatement"]>>;
|
|
22
|
+
|
|
23
|
+
/** Quote one already-validated SQL identifier. */
|
|
24
|
+
function quoteIdentifier(value: string): string {
|
|
25
|
+
return `\`${value.replaceAll("`", "``")}\``;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Quote one SQL string literal. */
|
|
29
|
+
function quoteLiteral(value: string): string {
|
|
30
|
+
return `'${value.replaceAll("'", "''")}'`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Compile one safe column generator into a SQL expression. */
|
|
34
|
+
function columnExpression(column: GeneratedColumn): string {
|
|
35
|
+
const salt = quoteLiteral(column.name);
|
|
36
|
+
switch (column.generator) {
|
|
37
|
+
case "id":
|
|
38
|
+
return `concat('ID-', lpad(cast(id as string), 8, '0'))`;
|
|
39
|
+
case "date":
|
|
40
|
+
return `date_sub(current_date(), pmod(xxhash64(id, ${salt}), ${column.days}))`;
|
|
41
|
+
case "category": {
|
|
42
|
+
const values = column.values.map(quoteLiteral);
|
|
43
|
+
const fallback = `element_at(array(${values.join(", ")}), pmod(xxhash64(id, ${salt}), ${values.length}) + 1)`;
|
|
44
|
+
return `case when pmod(xxhash64(id, ${salt}), 100) < 55 then ${values[0]} when pmod(xxhash64(id, ${salt}), 100) < 80 then ${values[1]} else ${fallback} end`;
|
|
45
|
+
}
|
|
46
|
+
case "number": {
|
|
47
|
+
const spread = column.maximum - column.minimum;
|
|
48
|
+
return `round((${column.minimum} + (pmod(xxhash64(id, ${salt}), 1000000) / 999999.0) * ${spread}) * case when pmod(xxhash64(id, ${salt}, 'anomaly'), 100) < 8 then 1.65 else 1 end, ${column.decimals})`;
|
|
49
|
+
}
|
|
50
|
+
case "boolean":
|
|
51
|
+
return `pmod(xxhash64(id, ${salt}), 10000) < ${Math.round(column.trueProbability * 10_000)}`;
|
|
52
|
+
default: {
|
|
53
|
+
const exhaustive: never = column;
|
|
54
|
+
throw new TypeError(`Unsupported generated column: ${String(exhaustive)}`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Compile the generated table statement without accepting model-authored SQL. */
|
|
60
|
+
export function compileGeneratorTableSql(location: GeneratedSchema, plan: GeneratorPlan): string {
|
|
61
|
+
const columns = plan.columns.map(
|
|
62
|
+
(column) => `${columnExpression(column)} AS ${quoteIdentifier(column.name)}`,
|
|
63
|
+
);
|
|
64
|
+
return [
|
|
65
|
+
`CREATE OR REPLACE TABLE ${quoteIdentifier(location.catalog)}.${quoteIdentifier(location.schema)}.${quoteIdentifier(plan.table)}`,
|
|
66
|
+
`COMMENT ${quoteLiteral(plan.story)}`,
|
|
67
|
+
"AS SELECT",
|
|
68
|
+
` ${columns.join(",\n ")}`,
|
|
69
|
+
`FROM range(${plan.rowCount})`,
|
|
70
|
+
].join("\n");
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Compile one KPI validation query from its typed aggregate. */
|
|
74
|
+
export function compileMetricValidationSql(table: string, metric: GeneratedMetric): string {
|
|
75
|
+
const aggregate =
|
|
76
|
+
metric.aggregation === "count"
|
|
77
|
+
? "count(*)"
|
|
78
|
+
: `${metric.aggregation === "sum" ? "sum" : "avg"}(${quoteIdentifier(metric.column!)})`;
|
|
79
|
+
return `SELECT ${aggregate} AS ${quoteIdentifier(metric.name)} FROM ${table}`;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Execute one statement and wait for its terminal response. */
|
|
83
|
+
export async function executeGeneratorStatement(
|
|
84
|
+
client: WorkspaceClient,
|
|
85
|
+
warehouseId: string,
|
|
86
|
+
statement: string,
|
|
87
|
+
location: Pick<GeneratedSchema, "catalog" | "schema">,
|
|
88
|
+
signal?: AbortSignal,
|
|
89
|
+
): Promise<StatementResponse> {
|
|
90
|
+
const context = signal ? databricks.toContext(signal) : undefined;
|
|
91
|
+
let response = await client.statementExecution.executeStatement(
|
|
92
|
+
{
|
|
93
|
+
warehouse_id: warehouseId,
|
|
94
|
+
statement,
|
|
95
|
+
catalog: location.catalog,
|
|
96
|
+
schema: location.schema,
|
|
97
|
+
wait_timeout: "50s",
|
|
98
|
+
on_wait_timeout: "CONTINUE",
|
|
99
|
+
},
|
|
100
|
+
context,
|
|
101
|
+
);
|
|
102
|
+
if (!TERMINAL_STATEMENT_STATES.has(response.status?.state ?? "")) {
|
|
103
|
+
const statementId = response.statement_id;
|
|
104
|
+
if (!statementId) throw new ExecutionError("Statement execution returned no statement id");
|
|
105
|
+
for await (const polled of asyncUtils.poll<StatementResponse>(
|
|
106
|
+
({ signal: pollSignal }) =>
|
|
107
|
+
client.statementExecution.getStatement(
|
|
108
|
+
{ statement_id: statementId },
|
|
109
|
+
databricks.toContext(pollSignal),
|
|
110
|
+
),
|
|
111
|
+
{
|
|
112
|
+
intervalMs: 1_000,
|
|
113
|
+
timeoutMs: 180_000,
|
|
114
|
+
...(signal ? { signal } : {}),
|
|
115
|
+
predicate: (value) => !TERMINAL_STATEMENT_STATES.has(value.status?.state ?? ""),
|
|
116
|
+
},
|
|
117
|
+
)) {
|
|
118
|
+
response = polled;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
if (response.status?.state !== "SUCCEEDED") {
|
|
122
|
+
throw new ExecutionError(
|
|
123
|
+
response.status?.error?.message ??
|
|
124
|
+
`Statement ended in ${response.status?.state ?? "UNKNOWN"}`,
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
return response;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Materialize generated data as the OBO user and validate every planned KPI. */
|
|
131
|
+
export async function materializeGeneratorData(
|
|
132
|
+
location: GeneratedSchema,
|
|
133
|
+
plan: GeneratorPlan,
|
|
134
|
+
warehouseId: string,
|
|
135
|
+
signal?: AbortSignal,
|
|
136
|
+
): Promise<MaterializedGeneratorData> {
|
|
137
|
+
const identity = await resolveGeneratorIdentity();
|
|
138
|
+
if (identity.principal !== location.principal) {
|
|
139
|
+
throw new ExecutionError("Generator schema principal does not match the active OBO user");
|
|
140
|
+
}
|
|
141
|
+
await executeGeneratorStatement(
|
|
142
|
+
identity.userClient,
|
|
143
|
+
warehouseId,
|
|
144
|
+
compileGeneratorTableSql(location, plan),
|
|
145
|
+
location,
|
|
146
|
+
signal,
|
|
147
|
+
);
|
|
148
|
+
const fullTable = `${location.catalog}.${location.schema}.${plan.table}`;
|
|
149
|
+
const validationStatements: Record<string, string> = {};
|
|
150
|
+
for (const metric of plan.metrics) {
|
|
151
|
+
const response = await executeGeneratorStatement(
|
|
152
|
+
identity.userClient,
|
|
153
|
+
warehouseId,
|
|
154
|
+
compileMetricValidationSql(fullTable, metric),
|
|
155
|
+
location,
|
|
156
|
+
signal,
|
|
157
|
+
);
|
|
158
|
+
if (!response.statement_id) {
|
|
159
|
+
throw new ExecutionError(`Validation for ${metric.name} returned no statement id`);
|
|
160
|
+
}
|
|
161
|
+
validationStatements[metric.name] = response.statement_id;
|
|
162
|
+
}
|
|
163
|
+
return MaterializedGeneratorDataSchema.parse({
|
|
164
|
+
tables: [fullTable],
|
|
165
|
+
validationStatements,
|
|
166
|
+
});
|
|
167
|
+
}
|
package/src/defaults.ts
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AppKit interceptor settings for researched brand generation.
|
|
3
|
+
*
|
|
4
|
+
* @module
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { brandRequestSubject, type BrandGenerationRequest } from "./schema.ts";
|
|
8
|
+
|
|
9
|
+
/** AppKit execute configuration used by this package. */
|
|
10
|
+
export type BrandExecuteConfig = {
|
|
11
|
+
cache?: { enabled?: boolean; ttl?: number; cacheKey?: (string | number | object)[] };
|
|
12
|
+
retry?: { enabled?: boolean; attempts?: number; initialDelay?: number; maxDelay?: number };
|
|
13
|
+
timeout?: number;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
/** Execution settings accepted by `Plugin.execute()`. */
|
|
17
|
+
export type BrandExecutionSettings = {
|
|
18
|
+
default: BrandExecuteConfig;
|
|
19
|
+
user?: BrandExecuteConfig;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/** Cache lifetime for a generated branding package. */
|
|
23
|
+
export const BRAND_CACHE_TTL_SECONDS = 21_600;
|
|
24
|
+
|
|
25
|
+
/** Cache-key version for the generated package shape and asset policy. */
|
|
26
|
+
export const BRAND_CACHE_VERSION = 5;
|
|
27
|
+
|
|
28
|
+
/** Attempts for the idempotent structured synthesis call. */
|
|
29
|
+
export const BRAND_GENERATION_ATTEMPTS = 2;
|
|
30
|
+
|
|
31
|
+
/** Build request-specific interceptor settings for brand generation. */
|
|
32
|
+
export function brandGenerationSettings(
|
|
33
|
+
request: BrandGenerationRequest,
|
|
34
|
+
options: {
|
|
35
|
+
timeoutMs: number;
|
|
36
|
+
model?: string;
|
|
37
|
+
maxSources: number;
|
|
38
|
+
sourceMaxLength: number;
|
|
39
|
+
},
|
|
40
|
+
): BrandExecutionSettings {
|
|
41
|
+
return {
|
|
42
|
+
default: {
|
|
43
|
+
cache: {
|
|
44
|
+
enabled: true,
|
|
45
|
+
ttl: BRAND_CACHE_TTL_SECONDS,
|
|
46
|
+
cacheKey: [
|
|
47
|
+
"brand",
|
|
48
|
+
"generate",
|
|
49
|
+
BRAND_CACHE_VERSION,
|
|
50
|
+
brandRequestSubject(request).toLowerCase(),
|
|
51
|
+
request.website ?? "",
|
|
52
|
+
options.model ?? "",
|
|
53
|
+
options.maxSources,
|
|
54
|
+
options.sourceMaxLength,
|
|
55
|
+
],
|
|
56
|
+
},
|
|
57
|
+
retry: { enabled: true, attempts: BRAND_GENERATION_ATTEMPTS },
|
|
58
|
+
timeout: options.timeoutMs,
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
}
|