@dbx-tools/appkit-mastra 0.1.13 → 0.1.14
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 +304 -645
- package/index.ts +46 -38
- package/package.json +58 -45
- package/src/agents.ts +224 -66
- package/src/chart.ts +531 -429
- package/src/config.ts +270 -19
- package/src/filesystems.ts +1090 -0
- package/src/genie.ts +1000 -660
- package/src/history.ts +166 -79
- package/src/mcp.ts +105 -0
- package/src/memory.ts +94 -92
- package/src/mlflow.ts +149 -0
- package/src/model.ts +75 -408
- package/src/observability.ts +121 -69
- package/src/pagination.ts +34 -0
- package/src/plugin.ts +552 -67
- package/src/processors.ts +168 -0
- package/src/rest.ts +67 -0
- package/src/server.ts +232 -45
- package/src/serving-sanitize.ts +167 -0
- package/src/serving.ts +27 -243
- package/src/statement.ts +89 -0
- package/src/storage-schema.ts +41 -0
- package/src/summarize.ts +176 -0
- package/src/threads.ts +338 -0
- package/src/workspaces.ts +346 -0
- package/src/writer.ts +44 -0
- package/tsconfig.json +41 -0
- package/dist/index.d.ts +0 -20
- package/dist/index.js +0 -20
- package/dist/src/agents.d.ts +0 -306
- package/dist/src/agents.js +0 -403
- package/dist/src/chart.d.ts +0 -170
- package/dist/src/chart.js +0 -491
- package/dist/src/config.d.ts +0 -183
- package/dist/src/config.js +0 -12
- package/dist/src/genie.d.ts +0 -131
- package/dist/src/genie.js +0 -630
- package/dist/src/history.d.ts +0 -67
- package/dist/src/history.js +0 -172
- package/dist/src/memory.d.ts +0 -100
- package/dist/src/memory.js +0 -242
- package/dist/src/model.d.ts +0 -159
- package/dist/src/model.js +0 -427
- package/dist/src/observability.d.ts +0 -33
- package/dist/src/observability.js +0 -71
- package/dist/src/plugin.d.ts +0 -130
- package/dist/src/plugin.js +0 -283
- package/dist/src/processors/strip-stale-charts.d.ts +0 -29
- package/dist/src/processors/strip-stale-charts.js +0 -96
- package/dist/src/server.d.ts +0 -46
- package/dist/src/server.js +0 -123
- package/dist/src/serving.d.ts +0 -156
- package/dist/src/serving.js +0 -231
- package/dist/src/tools/email.d.ts +0 -74
- package/dist/src/tools/email.js +0 -122
- package/dist/tsconfig.build.tsbuildinfo +0 -1
- package/src/processors/strip-stale-charts.ts +0 -105
- package/src/tools/email.ts +0 -147
package/src/memory.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* with two independent knobs:
|
|
6
6
|
*
|
|
7
7
|
* - **Storage** (threads / messages via `PostgresStore`): defaults to
|
|
8
|
-
* **per-agent** namespacing via
|
|
8
|
+
* **per-agent** namespacing via {@link agentStorageSchemaName} so
|
|
9
9
|
* conversation history stays isolated between agents in the same
|
|
10
10
|
* database. `PostgresStore` auto-creates the schema with
|
|
11
11
|
* `CREATE SCHEMA IF NOT EXISTS` on init.
|
|
@@ -27,26 +27,48 @@
|
|
|
27
27
|
* is registered); per-agent settings cascade on top of that.
|
|
28
28
|
*/
|
|
29
29
|
|
|
30
|
-
import {
|
|
31
|
-
import { logUtils, pluginUtils } from "@dbx-tools/appkit-shared";
|
|
30
|
+
import { getUsernameWithApiLookup } from "@databricks/appkit";
|
|
32
31
|
import { fastembed } from "@mastra/fastembed";
|
|
33
32
|
import { Memory } from "@mastra/memory";
|
|
34
33
|
import { PgVector, PostgresStore } from "@mastra/pg";
|
|
35
34
|
import { randomUUID } from "node:crypto";
|
|
36
|
-
import
|
|
35
|
+
import { Pool, type PoolConfig } from "pg";
|
|
37
36
|
|
|
38
|
-
import type {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
} from "./
|
|
42
|
-
import
|
|
37
|
+
import type { MastraAgentDefinition, MastraMemoryConfigOverride } from "./agents";
|
|
38
|
+
import type { MastraPluginConfig } from "./config";
|
|
39
|
+
import { agentStorageSchemaName } from "./storage-schema";
|
|
40
|
+
import { summaryModel, TITLE_INSTRUCTIONS } from "./summarize";
|
|
41
|
+
import { log } from "@dbx-tools/shared-core";
|
|
43
42
|
|
|
44
|
-
const
|
|
43
|
+
const logger = log.logger("mastra/memory");
|
|
45
44
|
|
|
46
|
-
/**
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
45
|
+
/**
|
|
46
|
+
* Build a dedicated **service-principal** Lakebase pool for Mastra
|
|
47
|
+
* memory from the lakebase plugin's resolved SP pg config.
|
|
48
|
+
*
|
|
49
|
+
* The plugin's `exports().pool` is a `RoutingPool` that switches to
|
|
50
|
+
* the per-user (OBO) pool whenever a query runs inside an `asUser`
|
|
51
|
+
* scope - exactly the context the mastra plugin establishes around
|
|
52
|
+
* every chat turn. Memory (threads / messages + semantic recall) must
|
|
53
|
+
* instead always act as the app service principal: it owns the
|
|
54
|
+
* auto-created `mastra_*` schemas (a per-user role usually can't
|
|
55
|
+
* `CREATE SCHEMA`) and is shared across users, so it cannot inherit a
|
|
56
|
+
* request's OBO identity.
|
|
57
|
+
*
|
|
58
|
+
* `pgConfig` must be the plugin's `exports().getPgConfig()` evaluated
|
|
59
|
+
* **outside** any `asUser` scope (i.e. during setup), so it carries
|
|
60
|
+
* the SP connection target, OAuth token-refresh `password` callback,
|
|
61
|
+
* and any `lakebase({ pool })` tuning overrides - all of which this
|
|
62
|
+
* pool inherits. See the call site in `plugin.ts`.
|
|
63
|
+
*/
|
|
64
|
+
export async function createServicePrincipalPool(pgConfig: PoolConfig): Promise<Pool> {
|
|
65
|
+
// `getPgConfig()` resolves the SP username synchronously from
|
|
66
|
+
// `PGUSER` / `DATABRICKS_CLIENT_ID`; fall back to the async API
|
|
67
|
+
// lookup (e.g. local dev authenticating via PAT) so the pool always
|
|
68
|
+
// has an identity to connect with.
|
|
69
|
+
const user = pgConfig.user ?? (await getUsernameWithApiLookup());
|
|
70
|
+
return new Pool({ ...pgConfig, user });
|
|
71
|
+
}
|
|
50
72
|
|
|
51
73
|
/** Effective per-knob setting after the plugin/agent cascade. */
|
|
52
74
|
type StorageSetting = MastraAgentDefinition["storage"];
|
|
@@ -54,65 +76,43 @@ type MemorySetting = MastraAgentDefinition["memory"];
|
|
|
54
76
|
|
|
55
77
|
/**
|
|
56
78
|
* True when any plugin-level or per-agent setting could need the
|
|
57
|
-
* Lakebase pool. Used by `plugin.ts` to gate
|
|
58
|
-
*
|
|
59
|
-
*
|
|
79
|
+
* Lakebase pool. Used by `plugin.ts` to gate creation of the
|
|
80
|
+
* service-principal pool and the {@link MemoryBuilder} that consumes
|
|
81
|
+
* it; when false neither is built.
|
|
60
82
|
*/
|
|
61
83
|
export function needsLakebase(config: MastraPluginConfig): boolean {
|
|
62
84
|
if (settingNeedsSharedPool(config.storage)) return true;
|
|
63
85
|
if (settingNeedsSharedPool(config.memory)) return true;
|
|
64
86
|
const defs = collectAgentDefinitions(config);
|
|
65
|
-
return defs.some(
|
|
66
|
-
(d) =>
|
|
67
|
-
settingNeedsSharedPool(d.storage) || settingNeedsSharedPool(d.memory),
|
|
68
|
-
);
|
|
87
|
+
return defs.some((d) => settingNeedsSharedPool(d.storage) || settingNeedsSharedPool(d.memory));
|
|
69
88
|
}
|
|
70
89
|
|
|
71
90
|
/**
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
*/
|
|
77
|
-
export function resolveLakebasePool(
|
|
78
|
-
context: pluginUtils.PluginContextLike | undefined,
|
|
79
|
-
caller: MastraPluginConfig,
|
|
80
|
-
): LakebasePool {
|
|
81
|
-
return pluginUtils.require(context, lakebase, caller).exports().pool;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/**
|
|
85
|
-
* Construct a per-agent {@link Memory} factory. Caches the shared
|
|
86
|
-
* `PgVector` singleton (built on first need) and the lazily-resolved
|
|
87
|
-
* Lakebase pool so each agent build is O(1) after the first.
|
|
91
|
+
* Construct a per-agent {@link Memory} factory bound to the supplied
|
|
92
|
+
* service-principal pool (see {@link createServicePrincipalPool}).
|
|
93
|
+
* Caches the shared `PgVector` singleton (built on first need) so each
|
|
94
|
+
* agent build is O(1) after the first.
|
|
88
95
|
*/
|
|
89
96
|
export function createMemoryBuilder(
|
|
90
97
|
config: MastraPluginConfig,
|
|
91
|
-
|
|
98
|
+
servicePrincipalPool: Pool,
|
|
92
99
|
): MemoryBuilder {
|
|
93
|
-
return new MemoryBuilder(config,
|
|
100
|
+
return new MemoryBuilder(config, servicePrincipalPool);
|
|
94
101
|
}
|
|
95
102
|
|
|
96
103
|
/**
|
|
97
|
-
* Builds one `Memory` per agent
|
|
98
|
-
*
|
|
99
|
-
* registering N agents stays cheap.
|
|
104
|
+
* Builds one `Memory` per agent against a shared service-principal
|
|
105
|
+
* Lakebase pool. Per-instance state keeps the shared `PgVector` alive
|
|
106
|
+
* across calls so registering N agents stays cheap.
|
|
100
107
|
*/
|
|
101
108
|
export class MemoryBuilder {
|
|
102
109
|
private sharedVector: PgVector | undefined;
|
|
103
|
-
private pool: LakebasePool | undefined;
|
|
104
110
|
|
|
105
111
|
constructor(
|
|
106
112
|
private readonly config: MastraPluginConfig,
|
|
107
|
-
private readonly
|
|
113
|
+
private readonly servicePrincipalPool: Pool,
|
|
108
114
|
) {}
|
|
109
115
|
|
|
110
|
-
/**
|
|
111
|
-
* Build a `Memory` for `agentId` after the plugin/agent cascade.
|
|
112
|
-
* Returns `undefined` when the agent has neither storage nor a
|
|
113
|
-
* vector store enabled - Mastra accepts a missing `memory` field
|
|
114
|
-
* and treats the agent as stateless.
|
|
115
|
-
*/
|
|
116
116
|
/**
|
|
117
117
|
* Build the Mastra-instance-level storage used for workflow
|
|
118
118
|
* snapshots. Returns `undefined` when plugin-level `storage` is
|
|
@@ -120,7 +120,7 @@ export class MemoryBuilder {
|
|
|
120
120
|
* the `requireApproval` flow) will not be available.
|
|
121
121
|
*
|
|
122
122
|
* The store lives in a dedicated `mastra_instance` schema so it
|
|
123
|
-
* never collides with per-agent
|
|
123
|
+
* never collides with per-agent {@link agentStorageSchemaName} namespaces.
|
|
124
124
|
* Workflow snapshots are not per-agent state; they belong to the
|
|
125
125
|
* `Mastra` instance that owns the workflow execution.
|
|
126
126
|
*/
|
|
@@ -129,18 +129,22 @@ export class MemoryBuilder {
|
|
|
129
129
|
if (!setting) return undefined;
|
|
130
130
|
if (typeof setting === "object") {
|
|
131
131
|
return new PostgresStore(
|
|
132
|
-
withId(setting, "mastra-store__instance") as ConstructorParameters<
|
|
133
|
-
typeof PostgresStore
|
|
134
|
-
>[0],
|
|
132
|
+
withId(setting, "mastra-store__instance") as ConstructorParameters<typeof PostgresStore>[0],
|
|
135
133
|
);
|
|
136
134
|
}
|
|
137
135
|
return new PostgresStore({
|
|
138
136
|
id: "mastra-store__instance",
|
|
139
137
|
schemaName: "mastra_instance",
|
|
140
|
-
pool: this.
|
|
138
|
+
pool: this.servicePrincipalPool,
|
|
141
139
|
});
|
|
142
140
|
}
|
|
143
141
|
|
|
142
|
+
/**
|
|
143
|
+
* Build a `Memory` for `agentId` after the plugin/agent cascade.
|
|
144
|
+
* Returns `undefined` when the agent has neither storage nor a
|
|
145
|
+
* vector store enabled - Mastra accepts a missing `memory` field
|
|
146
|
+
* and treats the agent as stateless.
|
|
147
|
+
*/
|
|
144
148
|
forAgent(agentId: string, def: MastraAgentDefinition): Memory | undefined {
|
|
145
149
|
const storageSetting = def.storage ?? this.config.storage;
|
|
146
150
|
const memorySetting = def.memory ?? this.config.memory;
|
|
@@ -148,20 +152,16 @@ export class MemoryBuilder {
|
|
|
148
152
|
const storage = this.buildStorage(agentId, storageSetting);
|
|
149
153
|
const vector = this.buildVector(memorySetting);
|
|
150
154
|
if (!storage && !vector) {
|
|
151
|
-
|
|
155
|
+
logger.debug("agent:stateless", { agentId });
|
|
152
156
|
return undefined;
|
|
153
157
|
}
|
|
154
158
|
|
|
155
|
-
|
|
159
|
+
logger.debug("agent:configured", {
|
|
156
160
|
agentId,
|
|
157
161
|
storage: storage !== undefined,
|
|
158
162
|
vector: vector !== undefined,
|
|
159
163
|
vectorMode:
|
|
160
|
-
vector === undefined
|
|
161
|
-
? "off"
|
|
162
|
-
: typeof memorySetting === "object"
|
|
163
|
-
? "dedicated"
|
|
164
|
-
: "shared",
|
|
164
|
+
vector === undefined ? "off" : typeof memorySetting === "object" ? "dedicated" : "shared",
|
|
165
165
|
});
|
|
166
166
|
|
|
167
167
|
return new Memory({
|
|
@@ -169,32 +169,39 @@ export class MemoryBuilder {
|
|
|
169
169
|
...(vector ? { vector, embedder: fastembed } : {}),
|
|
170
170
|
options: {
|
|
171
171
|
lastMessages: 10,
|
|
172
|
-
...(vector
|
|
173
|
-
|
|
172
|
+
...(vector ? { semanticRecall: { topK: 3, messageRange: 2 } } : {}),
|
|
173
|
+
// Auto-name each thread from its opening turn so the
|
|
174
|
+
// conversation list the UI renders shows meaningful titles
|
|
175
|
+
// instead of raw ids. Titling runs on the small / fast chat
|
|
176
|
+
// tier (see `summarize.ts`) rather than the agent's primary
|
|
177
|
+
// model, so naming a thread never spends the heavyweight model.
|
|
178
|
+
// Only meaningful when storage is on; harmless otherwise.
|
|
179
|
+
...(storage
|
|
180
|
+
? {
|
|
181
|
+
generateTitle: {
|
|
182
|
+
model: summaryModel(this.config),
|
|
183
|
+
instructions: TITLE_INSTRUCTIONS,
|
|
184
|
+
},
|
|
185
|
+
}
|
|
174
186
|
: {}),
|
|
175
187
|
},
|
|
176
188
|
});
|
|
177
189
|
}
|
|
178
190
|
|
|
179
|
-
private buildStorage(
|
|
180
|
-
agentId: string,
|
|
181
|
-
setting: StorageSetting,
|
|
182
|
-
): PostgresStore | undefined {
|
|
191
|
+
private buildStorage(agentId: string, setting: StorageSetting): PostgresStore | undefined {
|
|
183
192
|
if (!setting) return undefined;
|
|
184
193
|
if (typeof setting === "boolean") {
|
|
185
194
|
return new PostgresStore({
|
|
186
195
|
id: `mastra-store__${agentId}`,
|
|
187
|
-
schemaName:
|
|
188
|
-
pool: this.
|
|
196
|
+
schemaName: agentStorageSchemaName(agentId),
|
|
197
|
+
pool: this.servicePrincipalPool,
|
|
189
198
|
});
|
|
190
199
|
}
|
|
191
200
|
// Cast: `withId` guarantees `id` is set, but the distributive
|
|
192
201
|
// Omit + `id?: string` shape doesn't structurally narrow to the
|
|
193
202
|
// discriminated union members. Runtime shape is identical.
|
|
194
203
|
return new PostgresStore(
|
|
195
|
-
withId(setting, `mastra-store__${agentId}`) as ConstructorParameters<
|
|
196
|
-
typeof PostgresStore
|
|
197
|
-
>[0],
|
|
204
|
+
withId(setting, `mastra-store__${agentId}`) as ConstructorParameters<typeof PostgresStore>[0],
|
|
198
205
|
);
|
|
199
206
|
}
|
|
200
207
|
|
|
@@ -215,17 +222,10 @@ export class MemoryBuilder {
|
|
|
215
222
|
|
|
216
223
|
private getSharedVector(): PgVector {
|
|
217
224
|
if (!this.sharedVector) {
|
|
218
|
-
this.sharedVector = buildSharedPgVector(this.
|
|
225
|
+
this.sharedVector = buildSharedPgVector(this.servicePrincipalPool);
|
|
219
226
|
}
|
|
220
227
|
return this.sharedVector;
|
|
221
228
|
}
|
|
222
|
-
|
|
223
|
-
private requirePool(): LakebasePool {
|
|
224
|
-
if (!this.pool) {
|
|
225
|
-
this.pool = resolveLakebasePool(this.context, this.config);
|
|
226
|
-
}
|
|
227
|
-
return this.pool;
|
|
228
|
-
}
|
|
229
229
|
}
|
|
230
230
|
|
|
231
231
|
/**
|
|
@@ -248,9 +248,17 @@ export class MemoryBuilder {
|
|
|
248
248
|
* the lakebase pool from then on. The placeholder pool is `.end()`'d
|
|
249
249
|
* so its socket book-keeping is released.
|
|
250
250
|
*/
|
|
251
|
-
function buildSharedPgVector(pool:
|
|
251
|
+
function buildSharedPgVector(pool: Pool): PgVector {
|
|
252
252
|
const vector = new PgVector({
|
|
253
253
|
id: `pg${randomUUID()}`,
|
|
254
|
+
// Keep the recall index out of `public`: on a Lakebase database the app
|
|
255
|
+
// service principal has no CREATE on `public` (PG15+ locks it down), so a
|
|
256
|
+
// default-schema PgVector fails on CREATE INDEX with "permission denied for
|
|
257
|
+
// schema public". PgVector runs CREATE SCHEMA IF NOT EXISTS for a named
|
|
258
|
+
// schema, so the SP creates and owns `mastra_instance` (the same
|
|
259
|
+
// instance-level schema the workflow-snapshot store uses) and the index
|
|
260
|
+
// lands there. Table names don't collide with the storage tables.
|
|
261
|
+
schemaName: "mastra_instance",
|
|
254
262
|
host: "-1",
|
|
255
263
|
port: -1,
|
|
256
264
|
database: "_",
|
|
@@ -258,7 +266,7 @@ function buildSharedPgVector(pool: LakebasePool): PgVector {
|
|
|
258
266
|
password: "_",
|
|
259
267
|
});
|
|
260
268
|
const placeholder = vector.pool;
|
|
261
|
-
vector.pool = pool
|
|
269
|
+
vector.pool = pool;
|
|
262
270
|
void placeholder.end().catch(() => undefined);
|
|
263
271
|
return vector;
|
|
264
272
|
}
|
|
@@ -266,23 +274,17 @@ function buildSharedPgVector(pool: LakebasePool): PgVector {
|
|
|
266
274
|
/** Per-agent dedicated `PgVector` (rare; opt-in via object override). */
|
|
267
275
|
function buildPgVector(setting: MastraMemoryConfigOverride): PgVector {
|
|
268
276
|
return new PgVector(
|
|
269
|
-
withId(setting, `pg-vector__${randomUUID()}`) as ConstructorParameters<
|
|
270
|
-
typeof PgVector
|
|
271
|
-
>[0],
|
|
277
|
+
withId(setting, `pg-vector__${randomUUID()}`) as ConstructorParameters<typeof PgVector>[0],
|
|
272
278
|
);
|
|
273
279
|
}
|
|
274
280
|
|
|
275
281
|
/** True when this setting requires the shared Lakebase pool. */
|
|
276
|
-
function settingNeedsSharedPool(
|
|
277
|
-
setting: StorageSetting | MemorySetting | undefined,
|
|
278
|
-
): boolean {
|
|
282
|
+
function settingNeedsSharedPool(setting: StorageSetting | MemorySetting | undefined): boolean {
|
|
279
283
|
return setting === true;
|
|
280
284
|
}
|
|
281
285
|
|
|
282
286
|
/** Walk the three shapes of `config.agents` into a flat list. */
|
|
283
|
-
function collectAgentDefinitions(
|
|
284
|
-
config: MastraPluginConfig,
|
|
285
|
-
): MastraAgentDefinition[] {
|
|
287
|
+
function collectAgentDefinitions(config: MastraPluginConfig): MastraAgentDefinition[] {
|
|
286
288
|
const agents = config.agents;
|
|
287
289
|
if (!agents) return [];
|
|
288
290
|
if (Array.isArray(agents)) return agents;
|
package/src/mlflow.ts
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MLflow user-feedback logging: detect whether MLflow tracing is wired
|
|
3
|
+
* for this deployment, and log a thumbs / comment as a trace
|
|
4
|
+
* *assessment* via the Databricks MLflow REST API.
|
|
5
|
+
*
|
|
6
|
+
* Feedback attaches to a trace, and the plugin's spans reach MLflow
|
|
7
|
+
* through the same OTel pipeline as every other AppKit span (see
|
|
8
|
+
* `observability.ts`). MLflow derives its trace id from the OpenTelemetry
|
|
9
|
+
* trace id (`tr-<hex(otelTraceId)>`), so the server stamps the active
|
|
10
|
+
* trace id on each turn's response and the client sends it back here.
|
|
11
|
+
*
|
|
12
|
+
* There is no MLflow JS SDK, so this posts to the assessments REST
|
|
13
|
+
* endpoint directly using the OBO-scoped workspace client (the feedback
|
|
14
|
+
* is thus attributed to the signed-in user). Trace export is
|
|
15
|
+
* asynchronous, so the just-finished trace may not exist in MLflow yet
|
|
16
|
+
* when the user reacts; the log call retries briefly on "not found"
|
|
17
|
+
* before giving up softly.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { feedback } from "@dbx-tools/shared-mastra";
|
|
21
|
+
import { databricksFetch, readResponseJson, readResponseText } from "./rest";
|
|
22
|
+
import { async, error, log } from "@dbx-tools/shared-core";
|
|
23
|
+
import { appkit } from "@dbx-tools/appkit";
|
|
24
|
+
|
|
25
|
+
const logger = log.logger("mastra/mlflow");
|
|
26
|
+
|
|
27
|
+
/** Workspace client carried on an AppKit execution context. */
|
|
28
|
+
type WorkspaceClient = appkit.WorkspaceClientLike;
|
|
29
|
+
|
|
30
|
+
/** Assessments REST path for a trace. `3.0` is the current MLflow API version. */
|
|
31
|
+
const assessmentsPath = (traceId: string): string =>
|
|
32
|
+
`/api/3.0/mlflow/traces/${encodeURIComponent(traceId)}/assessments`;
|
|
33
|
+
|
|
34
|
+
/** Number of times to retry a "trace not found" response before giving up. */
|
|
35
|
+
const NOT_FOUND_RETRIES = 3;
|
|
36
|
+
/** Base backoff between "trace not found" retries, in ms (grows linearly). */
|
|
37
|
+
const NOT_FOUND_BACKOFF_MS = 1200;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Whether MLflow feedback logging is available for this deployment.
|
|
41
|
+
*
|
|
42
|
+
* Enabled when an OTLP exporter endpoint is configured (traces are
|
|
43
|
+
* actually shipped somewhere) AND an MLflow experiment is named - the
|
|
44
|
+
* two signals that the OTLP backend is MLflow and traces will
|
|
45
|
+
* materialize there. Both are standard env vars, so no plugin config is
|
|
46
|
+
* required; a deployment opts in simply by wiring MLflow tracing.
|
|
47
|
+
*/
|
|
48
|
+
export function mlflowEnabled(): boolean {
|
|
49
|
+
const hasExporter = Boolean(
|
|
50
|
+
process.env.OTEL_EXPORTER_OTLP_ENDPOINT?.trim() ||
|
|
51
|
+
process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT?.trim(),
|
|
52
|
+
);
|
|
53
|
+
const hasExperiment = Boolean(
|
|
54
|
+
process.env.MLFLOW_EXPERIMENT_ID?.trim() || process.env.MLFLOW_EXPERIMENT_NAME?.trim(),
|
|
55
|
+
);
|
|
56
|
+
return hasExporter && hasExperiment;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Resolve whether user feedback is enabled from the plugin's optional
|
|
61
|
+
* `config.feedback` override: the explicit boolean wins, otherwise fall
|
|
62
|
+
* back to auto-detecting MLflow tracing ({@link mlflowEnabled}). Shared
|
|
63
|
+
* by the plugin's client-config gate and the server's trace-id header so
|
|
64
|
+
* the two never disagree.
|
|
65
|
+
*/
|
|
66
|
+
export function resolveFeedbackEnabled(explicit: boolean | undefined): boolean {
|
|
67
|
+
return explicit ?? mlflowEnabled();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Parameters for {@link logFeedback}. */
|
|
71
|
+
export interface LogFeedbackParams {
|
|
72
|
+
/** MLflow trace id the assessment attaches to (`tr-<hex>`). */
|
|
73
|
+
traceId: string;
|
|
74
|
+
/** Assessment name; defaults per whether a value or a comment is sent. */
|
|
75
|
+
name?: string;
|
|
76
|
+
/** Thumbs / rating / label value. Omit for a comment-only submission. */
|
|
77
|
+
value?: boolean | number | string;
|
|
78
|
+
/** Freeform comment: the rationale alongside a value, or the value itself when none. */
|
|
79
|
+
comment?: string;
|
|
80
|
+
/** Identity the feedback is attributed to (user email / id). */
|
|
81
|
+
sourceId?: string;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Log a HUMAN feedback assessment to a trace. Returns the created
|
|
86
|
+
* assessment id on success, or `undefined` when the trace can't be
|
|
87
|
+
* found (even after retrying for export lag) or the request otherwise
|
|
88
|
+
* fails - callers surface that as a soft "not recorded" rather than an
|
|
89
|
+
* error, keeping the chat usable.
|
|
90
|
+
*/
|
|
91
|
+
export async function logFeedback(
|
|
92
|
+
client: WorkspaceClient,
|
|
93
|
+
params: LogFeedbackParams,
|
|
94
|
+
): Promise<string | undefined> {
|
|
95
|
+
// A comment with no thumbs value is logged as text feedback; a value
|
|
96
|
+
// (with an optional comment as the rationale) is the thumbs path.
|
|
97
|
+
const hasValue = params.value !== undefined;
|
|
98
|
+
const name =
|
|
99
|
+
params.name?.trim() ||
|
|
100
|
+
(hasValue ? feedback.DEFAULT_FEEDBACK_NAME : feedback.DEFAULT_COMMENT_NAME);
|
|
101
|
+
const value = hasValue ? params.value : params.comment;
|
|
102
|
+
const assessment: Record<string, unknown> = {
|
|
103
|
+
trace_id: params.traceId,
|
|
104
|
+
assessment_name: name,
|
|
105
|
+
source: {
|
|
106
|
+
source_type: "HUMAN",
|
|
107
|
+
source_id: params.sourceId?.trim() || "user",
|
|
108
|
+
},
|
|
109
|
+
feedback: { value },
|
|
110
|
+
...(hasValue && params.comment?.trim() ? { rationale: params.comment } : {}),
|
|
111
|
+
};
|
|
112
|
+
const body = { assessment };
|
|
113
|
+
|
|
114
|
+
for (let attempt = 0; attempt <= NOT_FOUND_RETRIES; attempt++) {
|
|
115
|
+
let res: Response;
|
|
116
|
+
try {
|
|
117
|
+
res = await databricksFetch(client, assessmentsPath(params.traceId), {
|
|
118
|
+
method: "POST",
|
|
119
|
+
body,
|
|
120
|
+
});
|
|
121
|
+
} catch (err) {
|
|
122
|
+
logger.warn("feedback request failed", {
|
|
123
|
+
traceId: params.traceId,
|
|
124
|
+
error: error.errorMessage(err),
|
|
125
|
+
});
|
|
126
|
+
return undefined;
|
|
127
|
+
}
|
|
128
|
+
if (res.ok) {
|
|
129
|
+
const parsed = await readResponseJson(res);
|
|
130
|
+
const assessmentId =
|
|
131
|
+
(parsed as { assessment?: { assessment_id?: unknown } })?.assessment?.assessment_id ??
|
|
132
|
+
(parsed as { assessment_id?: unknown })?.assessment_id;
|
|
133
|
+
return typeof assessmentId === "string" ? assessmentId : "";
|
|
134
|
+
}
|
|
135
|
+
// Trace export is async; a fresh trace may not exist yet. Retry a
|
|
136
|
+
// few times with a short backoff before giving up softly.
|
|
137
|
+
if (res.status === 404 && attempt < NOT_FOUND_RETRIES) {
|
|
138
|
+
await async.sleep(NOT_FOUND_BACKOFF_MS * (attempt + 1));
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
logger.warn("feedback not recorded", {
|
|
142
|
+
traceId: params.traceId,
|
|
143
|
+
status: res.status,
|
|
144
|
+
body: await readResponseText(res),
|
|
145
|
+
});
|
|
146
|
+
return undefined;
|
|
147
|
+
}
|
|
148
|
+
return undefined;
|
|
149
|
+
}
|