@dbx-tools/appkit-mastra 0.9.60 → 0.9.61
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 +12 -4
- package/index.ts +2 -2
- package/lib/index.d.ts +2 -2
- package/lib/index.js +3 -3
- package/lib/src/genie.js +3 -1
- package/lib/src/mlflow.d.ts +28 -4
- package/lib/src/mlflow.js +122 -14
- package/lib/src/server.js +3 -1
- package/lib/src/telemetry.d.ts +41 -0
- package/lib/src/telemetry.js +89 -4
- package/package.json +14 -14
- package/src/genie.ts +2 -0
- package/src/mlflow.ts +128 -15
- package/src/server.ts +1 -0
- package/src/telemetry.ts +90 -2
package/src/telemetry.ts
CHANGED
|
@@ -20,11 +20,14 @@
|
|
|
20
20
|
|
|
21
21
|
import { StringDecoder } from "node:string_decoder";
|
|
22
22
|
|
|
23
|
-
import { json, log, object } from "@dbx-tools/shared-core";
|
|
23
|
+
import { json, log, object, stringUtils } from "@dbx-tools/shared-core";
|
|
24
|
+
import { feedback, thread } from "@dbx-tools/shared-mastra";
|
|
24
25
|
import { context, SpanKind, trace, type Span } from "@opentelemetry/api";
|
|
25
26
|
import { getRPCMetadata, RPCType } from "@opentelemetry/core";
|
|
26
27
|
import type express from "express";
|
|
27
28
|
|
|
29
|
+
import { USER_EMAIL_HEADER, USER_ID_HEADER } from "./identity.ts";
|
|
30
|
+
|
|
28
31
|
const logger = log.logger("mastra/telemetry");
|
|
29
32
|
|
|
30
33
|
/** Cap on each payload copied onto a span, so one turn cannot bloat the export. */
|
|
@@ -56,6 +59,34 @@ export const CHAT_RESPONSE_ATTR = "appkit.mastra.chat.response";
|
|
|
56
59
|
export const CHAT_IDENTITY_ATTR = "appkit.mastra.identity.mode";
|
|
57
60
|
/** Whether the streamed turn emitted a Genie tool or progress event. */
|
|
58
61
|
export const CHAT_GENIE_USED_ATTR = "appkit.mastra.genie.used";
|
|
62
|
+
/**
|
|
63
|
+
* MLflow span-type attribute. Databricks maps this (or `gen_ai.operation.name`)
|
|
64
|
+
* onto the type chip in the experiment UI. Custom values such as `GENIE` are
|
|
65
|
+
* allowed.
|
|
66
|
+
*/
|
|
67
|
+
export const MLFLOW_SPAN_TYPE_ATTR = "mlflow.spanType";
|
|
68
|
+
/** GenAI semantic-convention operation name, an alternate span-type source. */
|
|
69
|
+
export const GEN_AI_OPERATION_NAME_ATTR = "gen_ai.operation.name";
|
|
70
|
+
/**
|
|
71
|
+
* Root-span user id the MLflow experiment UI's User column reads from OTel
|
|
72
|
+
* traces. Prefer the forwarded email when present so the column is human.
|
|
73
|
+
*/
|
|
74
|
+
export const MLFLOW_USER_ATTR = "user.id";
|
|
75
|
+
/** Root-span session id that enables the MLflow Sessions tab. */
|
|
76
|
+
export const MLFLOW_SESSION_ATTR = "session.id";
|
|
77
|
+
/**
|
|
78
|
+
* Prefix Databricks/MLflow copies onto the unified-view `tags` map. The
|
|
79
|
+
* suffix after this prefix is the tag name shown in the experiment UI.
|
|
80
|
+
*/
|
|
81
|
+
export const MLFLOW_TRACE_TAG_PREFIX = "mlflow.trace.tag.";
|
|
82
|
+
/** Experiment-UI tag marking a turn that called Genie. */
|
|
83
|
+
export const MLFLOW_GENIE_TAG = "genie";
|
|
84
|
+
/** Root-span type for a Mastra agent turn. */
|
|
85
|
+
export const MLFLOW_SPAN_TYPE_AGENT = "AGENT";
|
|
86
|
+
/** Span type for an `ask_genie` tool call. */
|
|
87
|
+
export const MLFLOW_SPAN_TYPE_GENIE = "GENIE";
|
|
88
|
+
/** Attribute key for the `genie` trace tag. */
|
|
89
|
+
export const MLFLOW_GENIE_TAG_ATTR = `${MLFLOW_TRACE_TAG_PREFIX}${MLFLOW_GENIE_TAG}`;
|
|
59
90
|
|
|
60
91
|
/** Root-span metadata known before a chat request is dispatched. */
|
|
61
92
|
export interface ChatTurnTelemetryOptions {
|
|
@@ -68,6 +99,57 @@ interface TraceTarget {
|
|
|
68
99
|
owned: boolean;
|
|
69
100
|
}
|
|
70
101
|
|
|
102
|
+
/** OpenTelemetry's sentinel for "no valid trace" - 32 zero hex chars. */
|
|
103
|
+
const INVALID_TRACE_ID = "0".repeat(32);
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Publish the turn's MLflow trace id before the handler writes the body.
|
|
107
|
+
*
|
|
108
|
+
* The chat UI only shows thumbs when this header is present. Stamping it here
|
|
109
|
+
* (on the same span the middleware just resolved) avoids relying on async
|
|
110
|
+
* Express continuation still sitting inside `context.with()`.
|
|
111
|
+
*/
|
|
112
|
+
/**
|
|
113
|
+
* Mark the active span as a Genie call so the experiment UI types it `GENIE`
|
|
114
|
+
* and tags the trace `genie=true`. Safe no-op when no recording span is active.
|
|
115
|
+
*/
|
|
116
|
+
export function stampGenieToolSpan(span: Span | undefined = trace.getActiveSpan()): void {
|
|
117
|
+
if (!span?.isRecording()) return;
|
|
118
|
+
span.setAttribute(MLFLOW_SPAN_TYPE_ATTR, MLFLOW_SPAN_TYPE_GENIE);
|
|
119
|
+
span.setAttribute(GEN_AI_OPERATION_NAME_ATTR, "execute_tool");
|
|
120
|
+
span.setAttribute(MLFLOW_GENIE_TAG_ATTR, "true");
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function requestHeader(req: express.Request, name: string): string | undefined {
|
|
124
|
+
const fromMethod =
|
|
125
|
+
typeof req.header === "function" ? stringUtils.trimToNull(req.header(name)) : undefined;
|
|
126
|
+
if (fromMethod) return fromMethod;
|
|
127
|
+
const raw = req.headers?.[name.toLowerCase()];
|
|
128
|
+
const value = Array.isArray(raw) ? raw[0] : raw;
|
|
129
|
+
return stringUtils.trimToNull(typeof value === "string" ? value : undefined) ?? undefined;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function requestQuery(req: express.Request, name: string): string | undefined {
|
|
133
|
+
const raw = req.query?.[name];
|
|
134
|
+
const value = Array.isArray(raw) ? raw[0] : raw;
|
|
135
|
+
return stringUtils.trimToNull(typeof value === "string" ? value : undefined) ?? undefined;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function stampMlflowActor(span: Span, req: express.Request): void {
|
|
139
|
+
const user = requestHeader(req, USER_EMAIL_HEADER) ?? requestHeader(req, USER_ID_HEADER);
|
|
140
|
+
if (user) span.setAttribute(MLFLOW_USER_ATTR, user);
|
|
141
|
+
const session =
|
|
142
|
+
requestHeader(req, thread.THREAD_ID_HEADER) ?? requestQuery(req, thread.THREAD_ID_QUERY);
|
|
143
|
+
if (session) span.setAttribute(MLFLOW_SESSION_ATTR, session);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function stampMlflowTraceId(res: express.Response, span: Span): void {
|
|
147
|
+
if (res.headersSent) return;
|
|
148
|
+
const traceId = span.spanContext().traceId;
|
|
149
|
+
if (!traceId || traceId === INVALID_TRACE_ID) return;
|
|
150
|
+
res.setHeader(feedback.MLFLOW_TRACE_ID_HEADER, `tr-${traceId}`);
|
|
151
|
+
}
|
|
152
|
+
|
|
71
153
|
/** Return one raw prompt when the request is exactly one text-only user message. */
|
|
72
154
|
export function textOnlyChatInput(messages: unknown): string | undefined {
|
|
73
155
|
if (typeof messages === "string") return messages || undefined;
|
|
@@ -276,6 +358,10 @@ export function chatTurnTelemetryMiddleware(
|
|
|
276
358
|
next();
|
|
277
359
|
return;
|
|
278
360
|
}
|
|
361
|
+
stampMlflowTraceId(res, target.span);
|
|
362
|
+
stampMlflowActor(target.span, req);
|
|
363
|
+
target.span.setAttribute(MLFLOW_SPAN_TYPE_ATTR, MLFLOW_SPAN_TYPE_AGENT);
|
|
364
|
+
target.span.setAttribute(GEN_AI_OPERATION_NAME_ATTR, "invoke_agent");
|
|
279
365
|
const identity =
|
|
280
366
|
typeof options.identity === "function" ? options.identity(req) : options.identity;
|
|
281
367
|
if (identity) {
|
|
@@ -311,7 +397,9 @@ export function chatTurnTelemetryMiddleware(
|
|
|
311
397
|
fullOutputRecorded = true;
|
|
312
398
|
};
|
|
313
399
|
const recordGenie = (used: boolean): void => {
|
|
314
|
-
if (used)
|
|
400
|
+
if (!used) return;
|
|
401
|
+
target.span.setAttribute(CHAT_GENIE_USED_ATTR, true);
|
|
402
|
+
target.span.setAttribute(MLFLOW_GENIE_TAG_ATTR, "true");
|
|
315
403
|
};
|
|
316
404
|
const endOwnedSpan = (): void => {
|
|
317
405
|
if (!target.owned || ownedSpanEnded) return;
|