@dbx-tools/ui-mastra 0.9.129 → 0.9.131
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 +45 -7
- package/index.ts +6 -3
- package/package.json +7 -6
- package/src/react/bubbles.tsx +3 -1
- package/src/react/chat-composer.tsx +93 -38
- package/src/react/chat-sessions.ts +9 -0
- package/src/react/chat-stream.ts +12 -0
- package/src/react/chat-view.tsx +2 -0
- package/src/react/embed-slots.tsx +40 -10
- package/src/react/index.ts +5 -1
- package/src/react/mastra-chat.tsx +330 -55
- package/src/react/types.ts +2 -0
- package/src/support/active-runs.ts +8 -2
- package/src/support/export.ts +30 -15
- package/src/support/genie-client.ts +220 -0
- package/src/support/index.ts +4 -1
- package/src/support/mastra-client.ts +49 -14
- package/src/support/thread-persistence.ts +9 -0
- package/src/support/thread-sessions.ts +19 -0
package/src/support/export.ts
CHANGED
|
@@ -67,6 +67,7 @@ export interface ExportBrand {
|
|
|
67
67
|
export interface EmbedResolver {
|
|
68
68
|
chart(id: string): Promise<Chart | undefined>;
|
|
69
69
|
statement(id: string): Promise<StatementData | undefined>;
|
|
70
|
+
visualization(id: string): Promise<StatementData | undefined>;
|
|
70
71
|
}
|
|
71
72
|
|
|
72
73
|
/** Options accepted by {@link exportChat}. */
|
|
@@ -185,14 +186,15 @@ function printViaHiddenIframe(html: string, downloadName: string): void {
|
|
|
185
186
|
|
|
186
187
|
/** One slice of a message: prose, a chart embed, or a data embed. */
|
|
187
188
|
type Segment =
|
|
188
|
-
{ kind: "text"; text: string }
|
|
189
|
+
| { kind: "text"; text: string }
|
|
190
|
+
| { kind: "chart"; id: string }
|
|
191
|
+
| { kind: "data"; id: string }
|
|
192
|
+
| { kind: "visualization"; id: string };
|
|
189
193
|
|
|
190
194
|
/**
|
|
191
|
-
* Split message text into prose
|
|
192
|
-
*
|
|
193
|
-
*
|
|
194
|
-
* non-UUID (fabricated) ids and unknown marker types collapse away so no
|
|
195
|
-
* literal `[type:...]` leaks into the output.
|
|
195
|
+
* Split message text into prose, chart, and data segments. A native Genie
|
|
196
|
+
* visualization exports as its backing table because the interactive AppKit
|
|
197
|
+
* chart has no serializable ECharts spec.
|
|
196
198
|
*/
|
|
197
199
|
function splitSegments(text: string): Segment[] {
|
|
198
200
|
const segments: Segment[] = [];
|
|
@@ -201,8 +203,12 @@ function splitSegments(text: string): Segment[] {
|
|
|
201
203
|
if (marker.start > last) {
|
|
202
204
|
segments.push({ kind: "text", text: text.slice(last, marker.start) });
|
|
203
205
|
}
|
|
204
|
-
if (markers.isUuid(marker.id) &&
|
|
205
|
-
segments.push({ kind:
|
|
206
|
+
if (markers.isUuid(marker.id) && marker.type === "chart") {
|
|
207
|
+
segments.push({ kind: "chart", id: marker.id });
|
|
208
|
+
} else if (markers.isUuid(marker.id) && marker.type === "data") {
|
|
209
|
+
segments.push({ kind: "data", id: marker.id });
|
|
210
|
+
} else if (markers.isUuid(marker.id) && marker.type === "visualization") {
|
|
211
|
+
segments.push({ kind: "visualization", id: marker.id });
|
|
206
212
|
}
|
|
207
213
|
last = marker.end;
|
|
208
214
|
}
|
|
@@ -281,7 +287,7 @@ async function messageBodyHtml(message: UIMessage, resolver: EmbedResolver): Pro
|
|
|
281
287
|
if (svg) parts.push(`<div class="embed embed-chart">${svg}</div>`);
|
|
282
288
|
continue;
|
|
283
289
|
}
|
|
284
|
-
const table = await dataTableHtml(resolver, seg.id);
|
|
290
|
+
const table = await dataTableHtml(resolver, seg.id, seg.kind === "visualization");
|
|
285
291
|
if (table) parts.push(table);
|
|
286
292
|
}
|
|
287
293
|
return parts.join("\n");
|
|
@@ -332,8 +338,12 @@ async function chartSvg(resolver: EmbedResolver, id: string): Promise<string | n
|
|
|
332
338
|
}
|
|
333
339
|
|
|
334
340
|
/** Resolve a data id and render its rows to an HTML table. `null` on miss. */
|
|
335
|
-
async function dataTableHtml(
|
|
336
|
-
|
|
341
|
+
async function dataTableHtml(
|
|
342
|
+
resolver: EmbedResolver,
|
|
343
|
+
id: string,
|
|
344
|
+
visualization = false,
|
|
345
|
+
): Promise<string | null> {
|
|
346
|
+
const data = await safeStatement(resolver, id, visualization);
|
|
337
347
|
if (!data || data.rows.length === 0) return null;
|
|
338
348
|
const head = data.columns.map((c) => `<th>${escapeHtml(stringUtils.toLabel(c))}</th>`).join("");
|
|
339
349
|
const body = data.rows
|
|
@@ -383,7 +393,7 @@ async function messageBodyMarkdown(message: UIMessage, resolver: EmbedResolver):
|
|
|
383
393
|
if (chart?.result) parts.push(`> **Chart:** ${chartTitle(chart)}`);
|
|
384
394
|
continue;
|
|
385
395
|
}
|
|
386
|
-
const table = await dataTableMarkdown(resolver, seg.id);
|
|
396
|
+
const table = await dataTableMarkdown(resolver, seg.id, seg.kind === "visualization");
|
|
387
397
|
if (table) parts.push(table);
|
|
388
398
|
}
|
|
389
399
|
return parts.join("\n\n");
|
|
@@ -400,8 +410,12 @@ function chartTitle(chart: Chart): string {
|
|
|
400
410
|
}
|
|
401
411
|
|
|
402
412
|
/** Render statement rows to a GFM table. `null` on miss / empty. */
|
|
403
|
-
async function dataTableMarkdown(
|
|
404
|
-
|
|
413
|
+
async function dataTableMarkdown(
|
|
414
|
+
resolver: EmbedResolver,
|
|
415
|
+
id: string,
|
|
416
|
+
visualization = false,
|
|
417
|
+
): Promise<string | null> {
|
|
418
|
+
const data = await safeStatement(resolver, id, visualization);
|
|
405
419
|
if (!data || data.rows.length === 0) return null;
|
|
406
420
|
const header = `| ${data.columns.map((c) => mdCell(stringUtils.toLabel(c))).join(" | ")} |`;
|
|
407
421
|
const sep = `| ${data.columns.map(() => "---").join(" | ")} |`;
|
|
@@ -427,9 +441,10 @@ async function safeChart(resolver: EmbedResolver, id: string): Promise<Chart | u
|
|
|
427
441
|
async function safeStatement(
|
|
428
442
|
resolver: EmbedResolver,
|
|
429
443
|
id: string,
|
|
444
|
+
visualization = false,
|
|
430
445
|
): Promise<StatementData | undefined> {
|
|
431
446
|
try {
|
|
432
|
-
return await resolver.statement(id);
|
|
447
|
+
return await (visualization ? resolver.visualization(id) : resolver.statement(id));
|
|
433
448
|
} catch {
|
|
434
449
|
return undefined;
|
|
435
450
|
}
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import { routes, wire, type MastraClientConfig } from "@dbx-tools/shared-mastra";
|
|
2
|
+
import type {
|
|
3
|
+
ListAgentSuspendedRunsResponse,
|
|
4
|
+
ListMemoryThreadMessagesResponse,
|
|
5
|
+
ListMemoryThreadsResponse,
|
|
6
|
+
} from "@mastra/client-js";
|
|
7
|
+
import { validateUIMessages, type UIMessage, type UIMessageChunk } from "ai";
|
|
8
|
+
import { MastraPluginClient, type MastraStreamResponse } from "./mastra-client.ts";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* {@link MastraPluginClient} that talks to one Genie space directly through
|
|
12
|
+
* the Mastra plugin's `/genie/:alias` routes instead of an agent and Mastra
|
|
13
|
+
* Memory. Genie owns all state: its conversations are the threads (the
|
|
14
|
+
* conversation id is the thread id), its messages are the history, and each
|
|
15
|
+
* turn streams the same AI SDK chunks as an agent's `ask_genie` call, so the
|
|
16
|
+
* Mastra chat renders it unchanged.
|
|
17
|
+
*
|
|
18
|
+
* A brand-new thread has no Genie conversation until its first turn; the
|
|
19
|
+
* stream announces the conversation id, which {@link MastraStreamResponse.threadId}
|
|
20
|
+
* reports so the chat adopts it.
|
|
21
|
+
*/
|
|
22
|
+
export class GenieChatClient extends MastraPluginClient {
|
|
23
|
+
/** Genie space alias every route is scoped to. */
|
|
24
|
+
readonly alias: string;
|
|
25
|
+
override readonly backgroundTurns = false;
|
|
26
|
+
override readonly feedbackEnabled = false;
|
|
27
|
+
override readonly threadRename = false;
|
|
28
|
+
override readonly attachments = false;
|
|
29
|
+
/** Thread ids known to be Genie conversations (listed, loaded, or announced). */
|
|
30
|
+
readonly #conversations = new Set<string>();
|
|
31
|
+
/** Transcript message id to Genie message id, for regenerate cleanup. */
|
|
32
|
+
readonly #genieMessageIds = new Map<string, string>();
|
|
33
|
+
|
|
34
|
+
constructor(config: MastraClientConfig, alias: string) {
|
|
35
|
+
super(config);
|
|
36
|
+
this.alias = alias;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
#url(...parts: string[]): string {
|
|
40
|
+
return `${this.basePath}${routes.genieRoutePath(this.alias, ...parts)}`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async #delete(url: string, signal?: AbortSignal): Promise<void> {
|
|
44
|
+
const res = await fetch(url, {
|
|
45
|
+
method: "DELETE",
|
|
46
|
+
credentials: "include",
|
|
47
|
+
...(signal ? { signal } : {}),
|
|
48
|
+
});
|
|
49
|
+
if (!res.ok && res.status !== 404) throw new Error(`HTTP ${res.status}`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Stream one Genie turn, continuing the thread's conversation when it has one. */
|
|
53
|
+
override async streamAgent(params: {
|
|
54
|
+
messages: UIMessage[];
|
|
55
|
+
runId: string;
|
|
56
|
+
threadId?: string;
|
|
57
|
+
signal?: AbortSignal;
|
|
58
|
+
}): Promise<MastraStreamResponse> {
|
|
59
|
+
const userMessage = [...params.messages].reverse().find((message) => message.role === "user");
|
|
60
|
+
const question = (userMessage?.parts ?? [])
|
|
61
|
+
.map((part) => (part.type === "text" ? part.text : ""))
|
|
62
|
+
.join("")
|
|
63
|
+
.trim();
|
|
64
|
+
const conversationId =
|
|
65
|
+
params.threadId && this.#conversations.has(params.threadId) ? params.threadId : undefined;
|
|
66
|
+
const response = await this.uiStream({
|
|
67
|
+
path: this.#url(routes.MASTRA_ROUTES.chat),
|
|
68
|
+
chatId: params.threadId ?? params.runId,
|
|
69
|
+
messages: userMessage ? [userMessage] : [],
|
|
70
|
+
body: { question, ...(conversationId ? { conversationId } : {}) },
|
|
71
|
+
routing: {},
|
|
72
|
+
signal: params.signal,
|
|
73
|
+
});
|
|
74
|
+
let resolved: string | undefined;
|
|
75
|
+
const stream = response.stream.pipeThrough(
|
|
76
|
+
new TransformStream<UIMessageChunk, UIMessageChunk>({
|
|
77
|
+
transform: (chunk, controller) => {
|
|
78
|
+
if (chunk.type === wire.GENIE_CONVERSATION_PART_TYPE) {
|
|
79
|
+
const data = wire.GenieConversationPartDataSchema.safeParse(chunk.data);
|
|
80
|
+
if (data.success) {
|
|
81
|
+
resolved = data.data.conversationId;
|
|
82
|
+
this.#conversations.add(resolved);
|
|
83
|
+
if (userMessage && data.data.messageId) {
|
|
84
|
+
this.#genieMessageIds.set(userMessage.id, data.data.messageId);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
controller.enqueue(chunk);
|
|
89
|
+
},
|
|
90
|
+
}),
|
|
91
|
+
);
|
|
92
|
+
return { headers: response.headers, stream, threadId: () => resolved };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Genie turns are request-scoped; there is no background run to observe. */
|
|
96
|
+
override async observeAgentStream(): Promise<MastraStreamResponse> {
|
|
97
|
+
throw new Error("Direct Genie chat has no background runs");
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Genie turns stop when their stream is aborted. */
|
|
101
|
+
override async abortAgentRun(): Promise<boolean> {
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Genie has no approval-gated tools. */
|
|
106
|
+
override async suspendedRuns(): Promise<ListAgentSuspendedRunsResponse> {
|
|
107
|
+
return { runs: [], total: 0 };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Starter questions for this client's Genie space. */
|
|
111
|
+
override async suggestions(_agentId?: string, signal?: AbortSignal): Promise<string[]> {
|
|
112
|
+
const payload = await this.getJson(
|
|
113
|
+
this.#url(routes.MASTRA_ROUTES.suggestions),
|
|
114
|
+
wire.MastraSuggestionsResponseSchema,
|
|
115
|
+
signal,
|
|
116
|
+
);
|
|
117
|
+
return payload.questions;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** List the caller's Genie conversations as chat threads. */
|
|
121
|
+
override async threads(
|
|
122
|
+
options: { signal?: AbortSignal } = {},
|
|
123
|
+
): Promise<ListMemoryThreadsResponse> {
|
|
124
|
+
const payload = await this.getJson(
|
|
125
|
+
this.#url(routes.MASTRA_ROUTES.threads),
|
|
126
|
+
wire.GenieThreadsResponseSchema,
|
|
127
|
+
options.signal,
|
|
128
|
+
);
|
|
129
|
+
for (const thread of payload.threads) this.#conversations.add(thread.id);
|
|
130
|
+
const threads = payload.threads.map((thread) => ({
|
|
131
|
+
id: thread.id,
|
|
132
|
+
...(thread.title ? { title: thread.title } : {}),
|
|
133
|
+
resourceId: "",
|
|
134
|
+
createdAt: thread.createdAt,
|
|
135
|
+
updatedAt: thread.createdAt,
|
|
136
|
+
metadata: {},
|
|
137
|
+
}));
|
|
138
|
+
return {
|
|
139
|
+
threads,
|
|
140
|
+
total: threads.length,
|
|
141
|
+
page: 0,
|
|
142
|
+
perPage: threads.length,
|
|
143
|
+
hasMore: false,
|
|
144
|
+
} satisfies ListMemoryThreadsResponse;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Load a Genie conversation's full transcript. Genie returns the whole
|
|
149
|
+
* conversation at once, so only the first page carries messages.
|
|
150
|
+
*/
|
|
151
|
+
override async history(options: {
|
|
152
|
+
threadId: string;
|
|
153
|
+
page?: number;
|
|
154
|
+
signal?: AbortSignal;
|
|
155
|
+
}): Promise<ListMemoryThreadMessagesResponse> {
|
|
156
|
+
const page = options.page ?? 0;
|
|
157
|
+
let messages: UIMessage[] = [];
|
|
158
|
+
if (page === 0) {
|
|
159
|
+
const payload = await this.getJson(
|
|
160
|
+
this.#url(routes.MASTRA_ROUTES.threads, options.threadId, routes.MASTRA_ROUTES.messages),
|
|
161
|
+
wire.GenieThreadMessagesResponseSchema,
|
|
162
|
+
options.signal,
|
|
163
|
+
);
|
|
164
|
+
messages = await validateUIMessages({ messages: payload.messages });
|
|
165
|
+
this.#conversations.add(options.threadId);
|
|
166
|
+
// History ids already address their Genie message; the delete route
|
|
167
|
+
// resolves a question id to the message that owns it.
|
|
168
|
+
for (const message of messages) this.#genieMessageIds.set(message.id, message.id);
|
|
169
|
+
}
|
|
170
|
+
return {
|
|
171
|
+
messages: [],
|
|
172
|
+
uiMessages: [...messages].reverse(),
|
|
173
|
+
total: messages.length,
|
|
174
|
+
page,
|
|
175
|
+
perPage: messages.length,
|
|
176
|
+
hasMore: false,
|
|
177
|
+
} satisfies ListMemoryThreadMessagesResponse;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** Delete the Genie conversation behind a thread. */
|
|
181
|
+
override async removeThread(
|
|
182
|
+
threadId: string,
|
|
183
|
+
options: { signal?: AbortSignal } = {},
|
|
184
|
+
): Promise<void> {
|
|
185
|
+
if (!this.#conversations.has(threadId)) return;
|
|
186
|
+
await this.#delete(this.#url(routes.MASTRA_ROUTES.threads, threadId), options.signal);
|
|
187
|
+
this.#conversations.delete(threadId);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** Clearing a Genie thread deletes its conversation; the next turn starts a new one. */
|
|
191
|
+
override async clearHistory(options: { threadId: string; signal?: AbortSignal }): Promise<void> {
|
|
192
|
+
await this.removeThread(options.threadId, options);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** Genie conversations cannot be renamed. */
|
|
196
|
+
override async renameThread(): Promise<void> {
|
|
197
|
+
throw new Error("Genie conversations cannot be renamed");
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** Delete the Genie messages behind the given transcript messages. */
|
|
201
|
+
override async deleteMessages(
|
|
202
|
+
threadId: string,
|
|
203
|
+
messageIds: string[],
|
|
204
|
+
options: { signal?: AbortSignal } = {},
|
|
205
|
+
): Promise<void> {
|
|
206
|
+
if (!this.#conversations.has(threadId)) return;
|
|
207
|
+
const genieIds = new Set(
|
|
208
|
+
messageIds.flatMap((id) => {
|
|
209
|
+
const genieId = this.#genieMessageIds.get(id);
|
|
210
|
+
return genieId ? [genieId] : [];
|
|
211
|
+
}),
|
|
212
|
+
);
|
|
213
|
+
for (const genieId of genieIds) {
|
|
214
|
+
await this.#delete(
|
|
215
|
+
this.#url(routes.MASTRA_ROUTES.threads, threadId, routes.MASTRA_ROUTES.messages, genieId),
|
|
216
|
+
options.signal,
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
package/src/support/index.ts
CHANGED
|
@@ -8,10 +8,12 @@ export * as chartTheme from "./chart-theme.ts";
|
|
|
8
8
|
export * as clipboard from "./clipboard.ts";
|
|
9
9
|
export * as download from "./download.ts";
|
|
10
10
|
export * as exportModule from "./export.ts";
|
|
11
|
+
export * as genieClient from "./genie-client.ts";
|
|
11
12
|
export * as mastraClient from "./mastra-client.ts";
|
|
12
13
|
export * as modelSelection from "./model-selection.ts";
|
|
13
14
|
export * as requestContext from "./request-context.ts";
|
|
14
15
|
export * as threadLabels from "./thread-labels.ts";
|
|
16
|
+
export * as threadPersistence from "./thread-persistence.ts";
|
|
15
17
|
export * as threadSessions from "./thread-sessions.ts";
|
|
16
18
|
export * as threadTabs from "./thread-tabs.ts";
|
|
17
19
|
export * as toolEvents from "./tool-events.ts";
|
|
@@ -20,7 +22,8 @@ export type { StoredActiveRun } from "./active-runs.ts";
|
|
|
20
22
|
export { LIGHT_CHART_CHROME } from "./chart-theme.ts";
|
|
21
23
|
export type { ChartChrome } from "./chart-theme.ts";
|
|
22
24
|
export type { ExportFormat, ExportBrand, EmbedResolver, ExportChatOptions } from "./export.ts";
|
|
23
|
-
export {
|
|
25
|
+
export { GenieChatClient } from "./genie-client.ts";
|
|
26
|
+
export { MastraChatRequestError, MastraPluginClient, useMastraConfig, useMastraClient, useMastraMlflowExperiment, useMastraModels, useMastraDefaultModel, useMastraSuggestions, useMastraThreads, useChartFetch, useStatementFetch, useVisualizationFetch } from "./mastra-client.ts";
|
|
24
27
|
export type { MastraStreamResponse, MastraMemoryThread, ByIdFetchState } from "./mastra-client.ts";
|
|
25
28
|
export { defaultReasoningEffort, modelStorageKey, readStoredModel, storeSelectedModel } from "./model-selection.ts";
|
|
26
29
|
export type { MastraRequestContextInput, MastraRequestContextSource, MastraRequestContextSnapshot } from "./request-context.ts";
|
|
@@ -31,6 +31,12 @@ type NativeAgentClient = ReturnType<MastraClient["getAgent"]>;
|
|
|
31
31
|
export interface MastraStreamResponse {
|
|
32
32
|
headers: Headers;
|
|
33
33
|
stream: ReadableStream<UIMessageChunk>;
|
|
34
|
+
/**
|
|
35
|
+
* Thread id the server assigned to this turn, read after the stream drains.
|
|
36
|
+
* Set by backends whose threads are server-minted (a Genie conversation);
|
|
37
|
+
* the chat adopts it in place of the client-minted id it streamed under.
|
|
38
|
+
*/
|
|
39
|
+
threadId?: () => string | undefined;
|
|
34
40
|
}
|
|
35
41
|
|
|
36
42
|
/** Terminal HTTP failure returned before a chat stream was established. */
|
|
@@ -56,7 +62,8 @@ export type MastraMemoryThread = ListMemoryThreadsResponse["threads"][number];
|
|
|
56
62
|
* - Native Mastra memory history and thread management, plus the model
|
|
57
63
|
* catalogue (`models`),
|
|
58
64
|
* Genie starter prompts (`suggestions`), MLflow feedback logging
|
|
59
|
-
* (`feedback`), and inline embed resolution (`chart
|
|
65
|
+
* (`feedback`), and inline embed resolution (`chart`, `statement`,
|
|
66
|
+
* `visualization`)
|
|
60
67
|
* over the plugin's own routes - all derived from `basePath` +
|
|
61
68
|
* {@link routes.MASTRA_ROUTES}.
|
|
62
69
|
*
|
|
@@ -89,6 +96,10 @@ export class MastraPluginClient extends MastraClient {
|
|
|
89
96
|
readonly chatAlwaysAvailable: boolean;
|
|
90
97
|
/** Whether turns continue server-side after the initiating stream disconnects. */
|
|
91
98
|
readonly backgroundTurns: boolean;
|
|
99
|
+
/** Whether {@link renameThread} is supported by this client's thread store. */
|
|
100
|
+
readonly threadRename: boolean = true;
|
|
101
|
+
/** Whether a chat may attach files to a turn. */
|
|
102
|
+
readonly attachments: boolean = true;
|
|
92
103
|
|
|
93
104
|
constructor(config: MastraClientConfig) {
|
|
94
105
|
super({
|
|
@@ -124,7 +135,7 @@ export class MastraPluginClient extends MastraClient {
|
|
|
124
135
|
return headers;
|
|
125
136
|
}
|
|
126
137
|
|
|
127
|
-
async
|
|
138
|
+
protected async uiStream(options: {
|
|
128
139
|
path: string;
|
|
129
140
|
chatId: string;
|
|
130
141
|
messages: UIMessage[];
|
|
@@ -219,7 +230,7 @@ export class MastraPluginClient extends MastraClient {
|
|
|
219
230
|
requestContext?: MastraRequestContextSnapshot;
|
|
220
231
|
signal?: AbortSignal;
|
|
221
232
|
}): Promise<MastraStreamResponse> {
|
|
222
|
-
return this
|
|
233
|
+
return this.uiStream({
|
|
223
234
|
path: `${this.basePath}${routes.MASTRA_ROUTES.chat}/${encodeURIComponent(params.agentId)}`,
|
|
224
235
|
chatId: params.threadId ?? params.runId,
|
|
225
236
|
messages: params.messages,
|
|
@@ -320,7 +331,7 @@ export class MastraPluginClient extends MastraClient {
|
|
|
320
331
|
signal?: AbortSignal;
|
|
321
332
|
},
|
|
322
333
|
): Promise<MastraStreamResponse> {
|
|
323
|
-
return this
|
|
334
|
+
return this.uiStream({
|
|
324
335
|
path: `${this.basePath}${routes.MASTRA_ROUTES.chat}/${encodeURIComponent(agentId)}`,
|
|
325
336
|
chatId: params.threadId ?? params.runId,
|
|
326
337
|
messages: [],
|
|
@@ -345,7 +356,7 @@ export class MastraPluginClient extends MastraClient {
|
|
|
345
356
|
* chat-capable models for a picker.
|
|
346
357
|
*/
|
|
347
358
|
async models(signal?: AbortSignal): Promise<ServingEndpointSummary[]> {
|
|
348
|
-
const payload = await this
|
|
359
|
+
const payload = await this.getJson(
|
|
349
360
|
`${this.basePath}${routes.MASTRA_ROUTES.models}`,
|
|
350
361
|
wire.ServingEndpointsResponseSchema,
|
|
351
362
|
signal,
|
|
@@ -369,7 +380,7 @@ export class MastraPluginClient extends MastraClient {
|
|
|
369
380
|
|
|
370
381
|
/** Fetch the complete resolved default-model descriptor for an agent. */
|
|
371
382
|
async defaultModelDetails(agentId?: string, signal?: AbortSignal): Promise<DefaultModelResponse> {
|
|
372
|
-
const payload = await this
|
|
383
|
+
const payload = await this.getJson(
|
|
373
384
|
this.#agentScoped(routes.MASTRA_ROUTES.defaultModel, agentId),
|
|
374
385
|
wire.DefaultModelResponseSchema,
|
|
375
386
|
signal,
|
|
@@ -383,7 +394,7 @@ export class MastraPluginClient extends MastraClient {
|
|
|
383
394
|
* Genie space (or it defines none).
|
|
384
395
|
*/
|
|
385
396
|
async suggestions(agentId?: string, signal?: AbortSignal): Promise<string[]> {
|
|
386
|
-
const payload = await this
|
|
397
|
+
const payload = await this.getJson(
|
|
387
398
|
this.#agentScoped(routes.MASTRA_ROUTES.suggestions, agentId),
|
|
388
399
|
wire.MastraSuggestionsResponseSchema,
|
|
389
400
|
signal,
|
|
@@ -513,7 +524,7 @@ export class MastraPluginClient extends MastraClient {
|
|
|
513
524
|
* `CAN_MANAGE`; inability to determine access resolves to `null`.
|
|
514
525
|
*/
|
|
515
526
|
async mlflowExperiment(signal?: AbortSignal): Promise<MastraMlflowExperimentResponse> {
|
|
516
|
-
return this
|
|
527
|
+
return this.getJson(
|
|
517
528
|
`${this.basePath}${routes.MASTRA_ROUTES.mlflowExperiment}`,
|
|
518
529
|
feedback.MastraMlflowExperimentResponseSchema,
|
|
519
530
|
signal,
|
|
@@ -541,6 +552,11 @@ export class MastraPluginClient extends MastraClient {
|
|
|
541
552
|
return this.#embed("data", id, wire.StatementDataSchema.parse, signal);
|
|
542
553
|
}
|
|
543
554
|
|
|
555
|
+
/** Resolve AppKit UI visualization data for a direct Genie statement. */
|
|
556
|
+
visualization(id: string, signal?: AbortSignal): Promise<StatementData | undefined> {
|
|
557
|
+
return this.#embed("visualization", id, wire.StatementDataSchema.parse, signal);
|
|
558
|
+
}
|
|
559
|
+
|
|
544
560
|
/**
|
|
545
561
|
* Compose an agent-scoped URL: `${basePath}${segment}` for the
|
|
546
562
|
* default agent (the mount that does not require an `:agentId`), or
|
|
@@ -566,7 +582,7 @@ export class MastraPluginClient extends MastraClient {
|
|
|
566
582
|
}
|
|
567
583
|
|
|
568
584
|
/** GET + JSON-parse + schema-validate against a route that always 200s. */
|
|
569
|
-
async
|
|
585
|
+
protected async getJson<T>(
|
|
570
586
|
url: string,
|
|
571
587
|
schema: { parse: (raw: unknown) => T },
|
|
572
588
|
signal?: AbortSignal,
|
|
@@ -848,13 +864,16 @@ export const useMastraDefaultModel = (
|
|
|
848
864
|
* Pass `enabled: false` to skip the fetch (e.g. when the caller
|
|
849
865
|
* supplies its own explicit suggestion list), in which case
|
|
850
866
|
* `questions` stays empty. Any fetch error degrades silently to no
|
|
851
|
-
* suggestions - they're a non-critical enhancement.
|
|
867
|
+
* suggestions - they're a non-critical enhancement. `clientOverride`
|
|
868
|
+
* replaces the plugin's default client (e.g. a direct Genie client).
|
|
852
869
|
*/
|
|
853
870
|
export const useMastraSuggestions = (
|
|
854
871
|
agentId?: string,
|
|
855
872
|
enabled = true,
|
|
873
|
+
clientOverride?: MastraPluginClient,
|
|
856
874
|
): { questions: string[]; loading: boolean } => {
|
|
857
|
-
const
|
|
875
|
+
const defaultClient = useMastraClient();
|
|
876
|
+
const client = clientOverride ?? defaultClient;
|
|
858
877
|
const [questions, setQuestions] = useState<string[]>([]);
|
|
859
878
|
const [loading, setLoading] = useState(enabled);
|
|
860
879
|
|
|
@@ -898,17 +917,20 @@ export const useMastraSuggestions = (
|
|
|
898
917
|
* hidden), in which case `threads` stays empty. A failed fetch degrades
|
|
899
918
|
* to an empty list - the conversation list is a non-critical
|
|
900
919
|
* enhancement layered over the always-available default thread.
|
|
920
|
+
* `clientOverride` replaces the plugin's default client.
|
|
901
921
|
*/
|
|
902
922
|
export const useMastraThreads = (
|
|
903
923
|
agentId?: string,
|
|
904
924
|
enabled = true,
|
|
925
|
+
clientOverride?: MastraPluginClient,
|
|
905
926
|
): {
|
|
906
927
|
threads: MastraMemoryThread[];
|
|
907
928
|
loading: boolean;
|
|
908
929
|
error: Error | null;
|
|
909
930
|
refresh: () => void;
|
|
910
931
|
} => {
|
|
911
|
-
const
|
|
932
|
+
const defaultClient = useMastraClient();
|
|
933
|
+
const client = clientOverride ?? defaultClient;
|
|
912
934
|
const [threads, setThreads] = useState<MastraMemoryThread[]>([]);
|
|
913
935
|
const [loading, setLoading] = useState(enabled);
|
|
914
936
|
const [error, setError] = useState<Error | null>(null);
|
|
@@ -952,7 +974,7 @@ export const useMastraThreads = (
|
|
|
952
974
|
/**
|
|
953
975
|
* Hook state for {@link useByIdFetch}. Generic over the
|
|
954
976
|
* resource's payload shape; consumers read `data` / `loading` /
|
|
955
|
-
* `error` exactly as the
|
|
977
|
+
* `error` exactly as the embed slots do. A 404 on
|
|
956
978
|
* the server resolves as `data === undefined` with `error ===
|
|
957
979
|
* null` so the slot renders nothing (matches "expired /
|
|
958
980
|
* unknown id" semantics across all by-id resources).
|
|
@@ -1049,7 +1071,7 @@ function readFetchCache<T>(key: string | undefined): T | undefined {
|
|
|
1049
1071
|
|
|
1050
1072
|
/**
|
|
1051
1073
|
* React hook for any resource the Mastra plugin exposes as
|
|
1052
|
-
* `/<resource>/:id`. Powers
|
|
1074
|
+
* `/<resource>/:id`. Powers the chart, statement, and visualization slots
|
|
1053
1075
|
* in the chat UI; the slot-specific hooks below are thin
|
|
1054
1076
|
* adapters that pick the right cache `key`, `fetcher`, and
|
|
1055
1077
|
* terminal check.
|
|
@@ -1182,3 +1204,16 @@ export const useStatementFetch = (
|
|
|
1182
1204
|
const fetcher = useCallback(() => client.statement(statementId as string), [client, statementId]);
|
|
1183
1205
|
return useByIdFetch<StatementData>(key, fetcher, statementIsTerminal);
|
|
1184
1206
|
};
|
|
1207
|
+
|
|
1208
|
+
/** Fetch a direct Genie statement through the OBO-only visualization resolver. */
|
|
1209
|
+
export const useVisualizationFetch = (
|
|
1210
|
+
statementId: string | undefined,
|
|
1211
|
+
): ByIdFetchState<StatementData> => {
|
|
1212
|
+
const client = useMastraClient();
|
|
1213
|
+
const key = statementId ? `visualization:${statementId}` : undefined;
|
|
1214
|
+
const fetcher = useCallback(
|
|
1215
|
+
() => client.visualization(statementId as string),
|
|
1216
|
+
[client, statementId],
|
|
1217
|
+
);
|
|
1218
|
+
return useByIdFetch<StatementData>(key, fetcher, statementIsTerminal);
|
|
1219
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** Build the browser storage key for one Mastra agent's selected thread. */
|
|
2
|
+
export function threadStorageKey(
|
|
3
|
+
basePath: string,
|
|
4
|
+
agentId: string,
|
|
5
|
+
resourceScope?: string,
|
|
6
|
+
): string {
|
|
7
|
+
const prefix = `dbx-mastra-thread:${basePath}:${agentId}`;
|
|
8
|
+
return resourceScope ? `${prefix}:${encodeURIComponent(resourceScope)}` : prefix;
|
|
9
|
+
}
|
|
@@ -67,6 +67,25 @@ export function isSessionRunning(session: ThreadSession): boolean {
|
|
|
67
67
|
return session.status === "submitted" || session.status === "streaming";
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
+
/** Return the newest user message in a transcript or turn. */
|
|
71
|
+
export function lastUserMessage(messages: readonly UIMessage[]): UIMessage | undefined {
|
|
72
|
+
for (let index = messages.length - 1; index >= 0; index -= 1) {
|
|
73
|
+
const message = messages[index];
|
|
74
|
+
if (message?.role === "user") return message;
|
|
75
|
+
}
|
|
76
|
+
return undefined;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Build a replacement turn that retains the interrupted user request. */
|
|
80
|
+
export function interruptedTurnMessages(
|
|
81
|
+
messages: UIMessage[],
|
|
82
|
+
nextMessage: UIMessage,
|
|
83
|
+
): UIMessage[] {
|
|
84
|
+
const interrupted = lastUserMessage(messages);
|
|
85
|
+
if (!interrupted || interrupted.id === nextMessage.id) return [nextMessage];
|
|
86
|
+
return [interrupted, nextMessage];
|
|
87
|
+
}
|
|
88
|
+
|
|
70
89
|
/** Append a steer to the queue (oldest first). Returns a new array. */
|
|
71
90
|
export function enqueueSteer<T extends QueuedSteer>(queue: T[], steer: T): T[] {
|
|
72
91
|
return [...queue, steer];
|