@dbx-tools/appkit-mastra 0.3.20 → 0.3.22
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 +19 -0
- package/package.json +10 -10
- package/src/chart.ts +64 -9
- package/src/config.ts +16 -0
- package/src/plugin.ts +13 -13
- package/src/server.ts +5 -8
- package/src/serving-sanitize.ts +35 -27
package/README.md
CHANGED
|
@@ -251,6 +251,25 @@ Agents can return `[chart:<id>]` and `[data:<statement_id>]` markers in prose.
|
|
|
251
251
|
The embed route resolves them later, which avoids forcing the language model to
|
|
252
252
|
inline large tables or wait for chart planning before continuing its answer.
|
|
253
253
|
|
|
254
|
+
### Brand The Charts
|
|
255
|
+
|
|
256
|
+
Pass a `brand` to the plugin to theme every generated chart with your brand's
|
|
257
|
+
palette and font; omit it for the default Echarts look.
|
|
258
|
+
|
|
259
|
+
```ts
|
|
260
|
+
import { brand } from "@dbx-tools/shared-core";
|
|
261
|
+
|
|
262
|
+
plugin.mastra({ agents, storage: true, brand: brand.defaultBrandContext });
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
`brand` is the portable `BrandContext` shared across the UI, email, and
|
|
266
|
+
libraries, so charts, email, and the chat UI theme from one source. The chart
|
|
267
|
+
planner derives an Echarts theme from it: a series color cycle seeded from
|
|
268
|
+
`colors.primary` / `colors.accent` (plus a colorblind-friendly spread so
|
|
269
|
+
many-series charts stay legible) and a base text style from `typography.sans` /
|
|
270
|
+
`colors.foreground`. Charts render to canvas, so this is applied server-side on
|
|
271
|
+
the Echarts option rather than through the browser `[data-brand]` CSS bridge.
|
|
272
|
+
|
|
254
273
|
## Model Selection
|
|
255
274
|
|
|
256
275
|
`model.buildModel()` adapts the generic resolver from
|
package/package.json
CHANGED
|
@@ -28,22 +28,22 @@
|
|
|
28
28
|
"express": "^5.1.0",
|
|
29
29
|
"pg": "^8.22.0",
|
|
30
30
|
"zod": "^4.3.6",
|
|
31
|
-
"@dbx-tools/appkit": "0.3.
|
|
32
|
-
"@dbx-tools/
|
|
33
|
-
"@dbx-tools/
|
|
34
|
-
"@dbx-tools/model": "0.3.
|
|
35
|
-
"@dbx-tools/shared-core": "0.3.
|
|
36
|
-
"@dbx-tools/
|
|
37
|
-
"@dbx-tools/shared-genie": "0.3.
|
|
38
|
-
"@dbx-tools/
|
|
39
|
-
"@dbx-tools/shared-model": "0.3.
|
|
31
|
+
"@dbx-tools/appkit": "0.3.22",
|
|
32
|
+
"@dbx-tools/databricks": "0.3.22",
|
|
33
|
+
"@dbx-tools/genie": "0.3.22",
|
|
34
|
+
"@dbx-tools/model": "0.3.22",
|
|
35
|
+
"@dbx-tools/shared-core": "0.3.22",
|
|
36
|
+
"@dbx-tools/shared-mastra": "0.3.22",
|
|
37
|
+
"@dbx-tools/shared-genie": "0.3.22",
|
|
38
|
+
"@dbx-tools/core": "0.3.22",
|
|
39
|
+
"@dbx-tools/shared-model": "0.3.22"
|
|
40
40
|
},
|
|
41
41
|
"main": "index.ts",
|
|
42
42
|
"license": "UNLICENSED",
|
|
43
43
|
"publishConfig": {
|
|
44
44
|
"access": "public"
|
|
45
45
|
},
|
|
46
|
-
"version": "0.3.
|
|
46
|
+
"version": "0.3.22",
|
|
47
47
|
"types": "index.ts",
|
|
48
48
|
"type": "module",
|
|
49
49
|
"exports": {
|
package/src/chart.ts
CHANGED
|
@@ -37,7 +37,7 @@ import { z } from "zod";
|
|
|
37
37
|
import type { MastraPluginConfig } from "./config";
|
|
38
38
|
import { buildModel } from "./model";
|
|
39
39
|
import { model } from "@dbx-tools/shared-model";
|
|
40
|
-
import { async, error, hash, log, string } from "@dbx-tools/shared-core";
|
|
40
|
+
import { async, error, hash, log, string, type BrandContext } from "@dbx-tools/shared-core";
|
|
41
41
|
|
|
42
42
|
const logger = log.logger("mastra/chart");
|
|
43
43
|
|
|
@@ -353,7 +353,7 @@ async function runChartPlanner(
|
|
|
353
353
|
...(abortSignal ? { abortSignal } : {}),
|
|
354
354
|
});
|
|
355
355
|
const plan = chartPlanSchema.parse(result.object);
|
|
356
|
-
const option = planToEchartsOption(plan, title);
|
|
356
|
+
const option = planToEchartsOption(plan, title, config.brand);
|
|
357
357
|
return { chartType: plan.chartType, option };
|
|
358
358
|
}
|
|
359
359
|
|
|
@@ -551,6 +551,50 @@ export async function fetchChart(
|
|
|
551
551
|
}
|
|
552
552
|
}
|
|
553
553
|
|
|
554
|
+
/* ----------------------------- echarts theme ----------------------------- */
|
|
555
|
+
|
|
556
|
+
/**
|
|
557
|
+
* The slice of an Echarts option that carries brand identity: the series
|
|
558
|
+
* color cycle and the base text style (font family + foreground color).
|
|
559
|
+
* Derived from a {@link BrandContext} by {@link brandChartTheme} and merged
|
|
560
|
+
* into every spec by {@link planToEchartsOption}.
|
|
561
|
+
*/
|
|
562
|
+
interface ChartTheme {
|
|
563
|
+
color: string[];
|
|
564
|
+
textStyle: { fontFamily: string; color: string };
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
/**
|
|
568
|
+
* Expand two brand hex colors into a legible categorical cycle. Echarts
|
|
569
|
+
* repeats the `color` array across series / categories, so a good default
|
|
570
|
+
* needs several distinct hues, not two. We seed with the brand primary and
|
|
571
|
+
* accent (the identity colors) and follow with a fixed, colorblind-friendly
|
|
572
|
+
* spread so multi-series / many-slice charts stay readable while the first
|
|
573
|
+
* one or two marks still land on-brand.
|
|
574
|
+
*/
|
|
575
|
+
function brandColorCycle(primary: string, accent: string): string[] {
|
|
576
|
+
const spread = ["#5B8FF9", "#F6BD16", "#E86452", "#6DC8EC", "#945FB9", "#FF9845", "#1E9493"];
|
|
577
|
+
const seen = new Set<string>();
|
|
578
|
+
return [primary, accent, ...spread].filter((c) => {
|
|
579
|
+
const key = c.toLowerCase();
|
|
580
|
+
if (seen.has(key)) return false;
|
|
581
|
+
seen.add(key);
|
|
582
|
+
return true;
|
|
583
|
+
});
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
/**
|
|
587
|
+
* Derive an Echarts {@link ChartTheme} from a brand context: the primary +
|
|
588
|
+
* accent colors seed the series cycle, the sans stack becomes the base font,
|
|
589
|
+
* and the brand foreground becomes the base text color.
|
|
590
|
+
*/
|
|
591
|
+
function brandChartTheme(brand: BrandContext): ChartTheme {
|
|
592
|
+
return {
|
|
593
|
+
color: brandColorCycle(brand.colors.primary, brand.colors.accent),
|
|
594
|
+
textStyle: { fontFamily: brand.typography.sans, color: brand.colors.foreground },
|
|
595
|
+
};
|
|
596
|
+
}
|
|
597
|
+
|
|
554
598
|
/* ----------------------------- echarts expansion ----------------------------- */
|
|
555
599
|
|
|
556
600
|
/**
|
|
@@ -559,10 +603,21 @@ export async function fetchChart(
|
|
|
559
603
|
* compact plan shape; tooltip / animation / color / grid defaults
|
|
560
604
|
* stay consistent across charts and are easy to tune without
|
|
561
605
|
* retraining model behaviour.
|
|
606
|
+
*
|
|
607
|
+
* When `brand` is set, the resolved spec is themed with the brand's series
|
|
608
|
+
* color cycle and base text style (see {@link brandChartTheme}); otherwise
|
|
609
|
+
* Echarts' defaults apply.
|
|
562
610
|
*/
|
|
563
|
-
function planToEchartsOption(
|
|
611
|
+
function planToEchartsOption(
|
|
612
|
+
plan: ChartPlan,
|
|
613
|
+
fallbackTitle: string,
|
|
614
|
+
brand?: BrandContext,
|
|
615
|
+
): Record<string, unknown> {
|
|
564
616
|
const baseTitle = plan.title ?? fallbackTitle;
|
|
565
617
|
const grid = { left: 48, right: 24, top: 56, bottom: 48, containLabel: true };
|
|
618
|
+
const theme = brand ? brandChartTheme(brand) : undefined;
|
|
619
|
+
const themed = (option: Record<string, unknown>): Record<string, unknown> =>
|
|
620
|
+
theme ? { ...theme, ...option } : option;
|
|
566
621
|
|
|
567
622
|
if (plan.chartType === "pie") {
|
|
568
623
|
// Echarts crashes on null pie slices - filter them out.
|
|
@@ -573,7 +628,7 @@ function planToEchartsOption(plan: ChartPlan, fallbackTitle: string): Record<str
|
|
|
573
628
|
(d): d is { name: string; value: number } =>
|
|
574
629
|
d !== null && typeof d === "object" && !Array.isArray(d),
|
|
575
630
|
);
|
|
576
|
-
return {
|
|
631
|
+
return themed({
|
|
577
632
|
title: { text: baseTitle, left: "center" },
|
|
578
633
|
tooltip: { trigger: "item" },
|
|
579
634
|
legend: { bottom: 0 },
|
|
@@ -585,14 +640,14 @@ function planToEchartsOption(plan: ChartPlan, fallbackTitle: string): Record<str
|
|
|
585
640
|
data: slices,
|
|
586
641
|
},
|
|
587
642
|
],
|
|
588
|
-
};
|
|
643
|
+
});
|
|
589
644
|
}
|
|
590
645
|
|
|
591
646
|
if (plan.chartType === "scatter") {
|
|
592
647
|
// Echarts crashes on null scatter points - keep only valid
|
|
593
648
|
// `[x, y]` tuples. Bare numbers / objects / nulls from a
|
|
594
649
|
// mismatched plan get dropped silently.
|
|
595
|
-
return {
|
|
650
|
+
return themed({
|
|
596
651
|
title: { text: baseTitle, left: "center" },
|
|
597
652
|
tooltip: { trigger: "item" },
|
|
598
653
|
legend: { bottom: 0 },
|
|
@@ -604,13 +659,13 @@ function planToEchartsOption(plan: ChartPlan, fallbackTitle: string): Record<str
|
|
|
604
659
|
type: "scatter",
|
|
605
660
|
data: s.data.filter((d): d is [number, number] => Array.isArray(d) && d.length === 2),
|
|
606
661
|
})),
|
|
607
|
-
};
|
|
662
|
+
});
|
|
608
663
|
}
|
|
609
664
|
|
|
610
665
|
// bar / line / area share the same axis layout.
|
|
611
666
|
const isArea = plan.chartType === "area";
|
|
612
667
|
const seriesType = plan.chartType === "bar" ? "bar" : "line";
|
|
613
|
-
return {
|
|
668
|
+
return themed({
|
|
614
669
|
title: { text: baseTitle, left: "center" },
|
|
615
670
|
tooltip: { trigger: "axis" },
|
|
616
671
|
legend: { bottom: 0 },
|
|
@@ -628,7 +683,7 @@ function planToEchartsOption(plan: ChartPlan, fallbackTitle: string): Record<str
|
|
|
628
683
|
smooth: seriesType === "line",
|
|
629
684
|
...(isArea ? { areaStyle: {} } : {}),
|
|
630
685
|
})),
|
|
631
|
-
};
|
|
686
|
+
});
|
|
632
687
|
}
|
|
633
688
|
|
|
634
689
|
/* ----------------------------- render_data tool ----------------------------- */
|
package/src/config.ts
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import type { BasePluginConfig } from "@databricks/appkit";
|
|
11
|
+
import type { BrandContext } from "@dbx-tools/shared-core";
|
|
11
12
|
import type { AgentConfig } from "@mastra/core/agent";
|
|
12
13
|
import { MASTRA_RESOURCE_ID_KEY, MASTRA_THREAD_ID_KEY } from "@mastra/core/request-context";
|
|
13
14
|
import type { PgVectorConfig, PostgresStoreConfig } from "@mastra/pg";
|
|
@@ -442,4 +443,19 @@ export interface MastraPluginConfig extends BasePluginConfig {
|
|
|
442
443
|
* surface.
|
|
443
444
|
*/
|
|
444
445
|
apiAccess?: "scoped" | "full";
|
|
446
|
+
/**
|
|
447
|
+
* Optional brand context applied to charts produced by the built-in
|
|
448
|
+
* `render_data` / `prepare_chart` tools. When set, the chart planner's
|
|
449
|
+
* Echarts output is themed with the brand's palette (series colors derived
|
|
450
|
+
* from `colors.primary` / `colors.accent`) and sans font
|
|
451
|
+
* (`typography.sans`) instead of Echarts' defaults. Omit for the default
|
|
452
|
+
* Echarts look.
|
|
453
|
+
*
|
|
454
|
+
* Pass the portable {@link BrandContext} shared across the UI and
|
|
455
|
+
* libraries (e.g. `brand.defaultBrandContext` from `@dbx-tools/shared-core`,
|
|
456
|
+
* or a customer brand) - the same object the email add-on and the UI
|
|
457
|
+
* `BrandProvider` consume, so a host themes charts, email, and UI from one
|
|
458
|
+
* source.
|
|
459
|
+
*/
|
|
460
|
+
brand?: BrandContext;
|
|
445
461
|
}
|
package/src/plugin.ts
CHANGED
|
@@ -51,11 +51,9 @@ import {
|
|
|
51
51
|
type PluginManifest,
|
|
52
52
|
type ResourceRequirement,
|
|
53
53
|
} from "@databricks/appkit";
|
|
54
|
-
import
|
|
55
|
-
import {
|
|
56
|
-
import
|
|
57
|
-
import type { Pool } from "pg";
|
|
58
|
-
|
|
54
|
+
import { plugin } from "@dbx-tools/appkit";
|
|
55
|
+
import { serving as nodeServing } from "@dbx-tools/model";
|
|
56
|
+
import { error, log, string } from "@dbx-tools/shared-core";
|
|
59
57
|
import {
|
|
60
58
|
feedback,
|
|
61
59
|
routes,
|
|
@@ -63,8 +61,12 @@ import {
|
|
|
63
61
|
type MastraFeedbackRequest,
|
|
64
62
|
type StatementData,
|
|
65
63
|
} from "@dbx-tools/shared-mastra";
|
|
66
|
-
import { serving as nodeServing } from "@dbx-tools/model";
|
|
67
64
|
import { display, type ServingEndpointSummary } from "@dbx-tools/shared-model";
|
|
65
|
+
import type { Agent } from "@mastra/core/agent";
|
|
66
|
+
import { Mastra } from "@mastra/core/mastra";
|
|
67
|
+
import express from "express";
|
|
68
|
+
import type { Pool } from "pg";
|
|
69
|
+
|
|
68
70
|
import { buildAgents, FALLBACK_AGENT_ID, type BuiltAgents } from "./agents";
|
|
69
71
|
import { fetchChart } from "./chart";
|
|
70
72
|
import type { MastraPluginConfig } from "./config";
|
|
@@ -78,8 +80,6 @@ import { attachRoutePatchMiddleware, isMastraRequestAllowed, MastraServer } from
|
|
|
78
80
|
import { resolveServingConfig } from "./serving";
|
|
79
81
|
import { fetchStatementData, STATEMENT_ROW_CAP } from "./statement";
|
|
80
82
|
import { threadsRoute } from "./threads";
|
|
81
|
-
import { error, log, string } from "@dbx-tools/shared-core";
|
|
82
|
-
import { plugin } from "@dbx-tools/appkit";
|
|
83
83
|
|
|
84
84
|
const GENIE_MANIFEST = plugin.data(genie).plugin.manifest;
|
|
85
85
|
const LAKEBASE_MANIFEST = plugin.data(lakebase).plugin.manifest;
|
|
@@ -330,7 +330,7 @@ export class MastraPlugin extends Plugin<MastraPluginConfig> {
|
|
|
330
330
|
// Reads only in-memory build state, so it's synchronous and needs no OBO
|
|
331
331
|
// scoping. Registered before the catch-all, same as `/models`.
|
|
332
332
|
const handleDefaultModel = (req: express.Request, res: express.Response): void => {
|
|
333
|
-
const requested = string.firstNonEmpty(req.params
|
|
333
|
+
const requested = string.firstNonEmpty(req.params.agentId);
|
|
334
334
|
const agentId = requested ?? this.built?.defaultAgentId ?? FALLBACK_AGENT_ID;
|
|
335
335
|
const raw = this.built?.defaultModels[agentId];
|
|
336
336
|
// `"<dynamic>"` (a call-time function) has no fixed id to advertise.
|
|
@@ -378,14 +378,14 @@ export class MastraPlugin extends Plugin<MastraPluginConfig> {
|
|
|
378
378
|
// clean 404; thrown errors bubble through `next(err)`.
|
|
379
379
|
const embedResolvers: Record<string, EmbedResolver> = {
|
|
380
380
|
chart: (req, id, signal) => {
|
|
381
|
-
const timeoutMs = parseTimeoutMs(req.query
|
|
381
|
+
const timeoutMs = parseTimeoutMs(req.query.timeoutMs);
|
|
382
382
|
return fetchChart(id, {
|
|
383
383
|
...(timeoutMs !== undefined ? { timeoutMs } : {}),
|
|
384
384
|
signal,
|
|
385
385
|
});
|
|
386
386
|
},
|
|
387
387
|
data: (req, id, signal) => {
|
|
388
|
-
const limit = parseStatementLimit(req.query
|
|
388
|
+
const limit = parseStatementLimit(req.query.limit);
|
|
389
389
|
return this.userScopedSelf(req).fetchStatement(id, {
|
|
390
390
|
...(limit !== undefined ? { limit } : {}),
|
|
391
391
|
signal,
|
|
@@ -394,8 +394,8 @@ export class MastraPlugin extends Plugin<MastraPluginConfig> {
|
|
|
394
394
|
};
|
|
395
395
|
|
|
396
396
|
router.get(`${routes.MASTRA_ROUTES.embed}/:type/:id`, (req, res, next) => {
|
|
397
|
-
const type = req.params
|
|
398
|
-
const id = req.params
|
|
397
|
+
const type = req.params.type ?? "";
|
|
398
|
+
const id = req.params.id;
|
|
399
399
|
const resolve = embedResolvers[type];
|
|
400
400
|
if (!resolve) {
|
|
401
401
|
res.status(404).json({ error: `unsupported embed type: ${type}` });
|
package/src/server.ts
CHANGED
|
@@ -7,7 +7,9 @@
|
|
|
7
7
|
* @module
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
+
import { randomUUID } from "node:crypto";
|
|
10
11
|
import { getExecutionContext } from "@databricks/appkit";
|
|
12
|
+
import { http, log, object, string, token } from "@dbx-tools/shared-core";
|
|
11
13
|
import { feedback, thread } from "@dbx-tools/shared-mastra";
|
|
12
14
|
import {
|
|
13
15
|
MASTRA_RESOURCE_ID_KEY,
|
|
@@ -17,9 +19,6 @@ import {
|
|
|
17
19
|
import { MastraServer as MastraServerExpress } from "@mastra/express";
|
|
18
20
|
import { trace } from "@opentelemetry/api";
|
|
19
21
|
import type express from "express";
|
|
20
|
-
import { randomUUID } from "node:crypto";
|
|
21
|
-
|
|
22
|
-
import { resolveFeedbackEnabled } from "./mlflow";
|
|
23
22
|
|
|
24
23
|
import {
|
|
25
24
|
MASTRA_REQUEST_ID_KEY,
|
|
@@ -30,7 +29,8 @@ import {
|
|
|
30
29
|
type MastraPluginConfig,
|
|
31
30
|
type User,
|
|
32
31
|
} from "./config";
|
|
33
|
-
import {
|
|
32
|
+
import { resolveFeedbackEnabled } from "./mlflow";
|
|
33
|
+
|
|
34
34
|
import { extractModelOverride, MASTRA_MODEL_OVERRIDE_KEY, resolveServingConfig } from "./serving";
|
|
35
35
|
/**
|
|
36
36
|
* OpenTelemetry's sentinel for "no valid trace" - 32 zero hex chars.
|
|
@@ -154,10 +154,7 @@ export class MastraServer extends MastraServerExpress {
|
|
|
154
154
|
configureRequestContextScopes(req: express.Request, requestContext: RequestContext) {
|
|
155
155
|
const scopes = [
|
|
156
156
|
...object
|
|
157
|
-
.sequence(
|
|
158
|
-
token.getAccessTokenScopes(req),
|
|
159
|
-
token.getAccessTokenScopes(req, "authorization"),
|
|
160
|
-
)
|
|
157
|
+
.sequence(token.getAccessTokenScopes(req), token.getAccessTokenScopes(req, "authorization"))
|
|
161
158
|
.distinct(),
|
|
162
159
|
];
|
|
163
160
|
requestContext.set(MASTRA_SCOPES_KEY, scopes);
|
package/src/serving-sanitize.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { string } from "@dbx-tools/shared-core";
|
|
2
|
+
import { type ChatMessage, type ChatRole, openaiChat } from "@dbx-tools/shared-model";
|
|
2
3
|
/**
|
|
3
4
|
* Repairs Mastra / AI SDK message replays sent to Databricks Model
|
|
4
5
|
* Serving before they hit the OpenAI-compatible `/chat/completions`
|
|
@@ -6,43 +7,46 @@ import { string } from "@dbx-tools/shared-core";
|
|
|
6
7
|
*/
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
10
|
+
* A chat message as it arrives on the serving wire, plus the extended-thinking
|
|
11
|
+
* fields Databricks-hosted Claude adds. The OpenAI-standard part of the shape
|
|
12
|
+
* is {@link ChatMessage} from `@dbx-tools/shared-model`; only the provider
|
|
13
|
+
* extensions this module strips are declared here.
|
|
13
14
|
*/
|
|
14
|
-
export interface ServingChatMessage {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
tool_calls?: Array<{ id: string; type: string; function: unknown }>;
|
|
18
|
-
tool_call_id?: string;
|
|
15
|
+
export interface ServingChatMessage extends ChatMessage {
|
|
16
|
+
/** Narrowed to the roles this repair pass reasons about, plus Claude's `reasoning` turn. */
|
|
17
|
+
role: ChatRole | "reasoning";
|
|
19
18
|
reasoning?: unknown;
|
|
20
19
|
reasoning_content?: unknown;
|
|
21
20
|
}
|
|
22
21
|
|
|
23
|
-
type ServingContentPart = {
|
|
24
|
-
type?: string;
|
|
25
|
-
text?: string;
|
|
26
|
-
[key: string]: unknown;
|
|
27
|
-
};
|
|
28
|
-
|
|
29
22
|
const REASONING_PART_TYPES = new Set(["reasoning", "thinking", "redacted_thinking"]);
|
|
30
23
|
|
|
31
24
|
/**
|
|
32
25
|
* Parse, sanitize, and re-serialize a `/serving-endpoints/...` POST
|
|
33
26
|
* body. Returns the original string verbatim when the body is not
|
|
34
|
-
* JSON
|
|
27
|
+
* JSON or no rewrite was needed.
|
|
35
28
|
*/
|
|
36
29
|
export function rewriteServingBody(body: string): string {
|
|
37
|
-
let parsed:
|
|
30
|
+
let parsed: Record<string, unknown>;
|
|
38
31
|
try {
|
|
39
|
-
parsed = JSON.parse(body)
|
|
32
|
+
parsed = JSON.parse(body) as Record<string, unknown>;
|
|
40
33
|
} catch {
|
|
41
34
|
return body;
|
|
42
35
|
}
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
36
|
+
|
|
37
|
+
// Runs regardless of `messages`: Databricks refuses to parse a body carrying
|
|
38
|
+
// an unknown top-level field, so this failure is not specific to a transcript.
|
|
39
|
+
let changed = openaiChat.stripUnsupportedChatFields(parsed).length > 0;
|
|
40
|
+
|
|
41
|
+
if (Array.isArray(parsed.messages)) {
|
|
42
|
+
const messages = parsed.messages as ServingChatMessage[];
|
|
43
|
+
// Evaluated eagerly, not short-circuited: one transcript can need both
|
|
44
|
+
// reasoning blocks stripped AND a trailing assistant prefill folded back.
|
|
45
|
+
const stripped = stripReasoningFromServingMessages(messages);
|
|
46
|
+
const repaired = repairAssistantPrefill(messages);
|
|
47
|
+
changed = changed || stripped || repaired;
|
|
48
|
+
}
|
|
49
|
+
|
|
46
50
|
return changed ? JSON.stringify(parsed) : body;
|
|
47
51
|
}
|
|
48
52
|
|
|
@@ -145,15 +149,19 @@ export function repairAssistantPrefill(messages: ServingChatMessage[]): boolean
|
|
|
145
149
|
return true;
|
|
146
150
|
}
|
|
147
151
|
|
|
152
|
+
/**
|
|
153
|
+
* Flatten content to the prose a human would read: text parts only, separated
|
|
154
|
+
* by blank lines, since the result is spliced into another turn's message body.
|
|
155
|
+
*/
|
|
148
156
|
function textFromServingContent(content: ServingChatMessage["content"]): string {
|
|
149
|
-
|
|
150
|
-
if (!Array.isArray(content)) return "";
|
|
151
|
-
return content
|
|
152
|
-
.filter((part) => part?.type === "text" && typeof part.text === "string")
|
|
153
|
-
.map((part) => part.text as string)
|
|
154
|
-
.join("\n\n");
|
|
157
|
+
return openaiChat.chatContentToText(content, { separator: "\n\n", types: ["text"] });
|
|
155
158
|
}
|
|
156
159
|
|
|
160
|
+
/**
|
|
161
|
+
* Whether a turn carries nothing worth replaying. Deliberately stricter than
|
|
162
|
+
* "has no text": a part of any other type (an image, a cache marker) counts as
|
|
163
|
+
* content, so a message holding one is kept even though it has no prose.
|
|
164
|
+
*/
|
|
157
165
|
function isEmptyServingContent(content: ServingChatMessage["content"]): boolean {
|
|
158
166
|
if (content === undefined) return true;
|
|
159
167
|
if (typeof content === "string") return content.trim().length === 0;
|