@adminforth/agent 1.45.0 → 1.46.0
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/agentTurnService.ts +526 -0
- package/build.log +1 -1
- package/chatSurfaceService.ts +370 -0
- package/dist/agentTurnService.d.ts +70 -0
- package/dist/agentTurnService.js +453 -0
- package/dist/chatSurfaceService.d.ts +32 -0
- package/dist/chatSurfaceService.js +265 -0
- package/dist/endpoints/chatSurfaces.d.ts +3 -0
- package/dist/endpoints/chatSurfaces.js +91 -0
- package/dist/endpoints/context.d.ts +30 -0
- package/dist/endpoints/context.js +1 -0
- package/dist/endpoints/core.d.ts +3 -0
- package/dist/endpoints/core.js +106 -0
- package/dist/endpoints/sessions.d.ts +3 -0
- package/dist/endpoints/sessions.js +177 -0
- package/dist/errors.d.ts +2 -0
- package/dist/errors.js +9 -0
- package/dist/index.d.ts +4 -47
- package/dist/index.js +37 -917
- package/dist/sessionStore.d.ts +19 -0
- package/dist/sessionStore.js +83 -0
- package/endpoints/chatSurfaces.ts +93 -0
- package/endpoints/context.ts +66 -0
- package/endpoints/core.ts +113 -0
- package/endpoints/sessions.ts +183 -0
- package/errors.ts +10 -0
- package/index.ts +48 -1053
- package/package.json +1 -1
- package/sessionStore.ts +94 -0
- package/agentResponseEvents.ts +0 -1
- package/dist/agentResponseEvents.d.ts +0 -1
- package/dist/agentResponseEvents.js +0 -1
package/index.ts
CHANGED
|
@@ -1,138 +1,33 @@
|
|
|
1
1
|
import type {
|
|
2
|
-
AdminUser,
|
|
3
2
|
AdminForthResource,
|
|
4
|
-
ChatSurfaceAdapter,
|
|
5
|
-
ChatSurfaceEventSink,
|
|
6
|
-
ChatSurfaceIncomingMessage,
|
|
7
|
-
IAdminForthEndpointHandlerInput,
|
|
8
3
|
IAdminForth,
|
|
9
4
|
IHttpServer,
|
|
10
5
|
} from "adminforth";
|
|
11
6
|
|
|
12
|
-
import { AdminForthPlugin
|
|
7
|
+
import { AdminForthPlugin } from "adminforth";
|
|
13
8
|
|
|
14
9
|
import type { PluginOptions } from './types.js';
|
|
15
|
-
import { randomUUID } from 'crypto';
|
|
16
|
-
import { HumanMessage, SystemMessage } from "langchain";
|
|
17
10
|
import { MemorySaver, type BaseCheckpointSaver } from "@langchain/langgraph";
|
|
18
11
|
import { z } from "zod";
|
|
19
|
-
import { createAgentChatModel, callAgent } from "./agent/simpleAgent.js";
|
|
20
12
|
import { AdminForthCheckpointSaver } from "./agent/checkpointer.js";
|
|
21
|
-
import {
|
|
22
|
-
import {
|
|
23
|
-
import {
|
|
24
|
-
import
|
|
25
|
-
import {
|
|
26
|
-
import {
|
|
27
|
-
import
|
|
28
|
-
import {
|
|
13
|
+
import { appendCustomSystemPrompt, buildAgentSystemPrompt, DEFAULT_AGENT_SYSTEM_PROMPT} from "./agent/systemPrompt.js";
|
|
14
|
+
import { setupCoreEndpoints } from "./endpoints/core.js";
|
|
15
|
+
import { setupSessionEndpoints } from "./endpoints/sessions.js";
|
|
16
|
+
import { setupChatSurfaceEndpoints } from "./endpoints/chatSurfaces.js";
|
|
17
|
+
import type { AgentEndpointsContext } from "./endpoints/context.js";
|
|
18
|
+
import { AgentSessionStore } from "./sessionStore.js";
|
|
19
|
+
import { ChatSurfaceService } from "./chatSurfaceService.js";
|
|
20
|
+
import { AgentTurnService } from "./agentTurnService.js";
|
|
29
21
|
|
|
30
22
|
export type { AgentEvent, AgentEventEmitter } from "./agentEvents.js";
|
|
31
23
|
|
|
32
|
-
type MulterFile = {
|
|
33
|
-
buffer: Buffer;
|
|
34
|
-
originalname: string;
|
|
35
|
-
mimetype: string;
|
|
36
|
-
};
|
|
37
|
-
|
|
38
|
-
type ExpressMulterRequest = { file?: MulterFile };
|
|
39
|
-
|
|
40
|
-
type ChatSurfaceConnectAction = {
|
|
41
|
-
type: "url";
|
|
42
|
-
label: string;
|
|
43
|
-
url: string;
|
|
44
|
-
};
|
|
45
|
-
|
|
46
|
-
type ChatSurfaceAdapterWithConnectAction = ChatSurfaceAdapter & {
|
|
47
|
-
createConnectAction?(input: {
|
|
48
|
-
token: string;
|
|
49
|
-
}): ChatSurfaceConnectAction | Promise<ChatSurfaceConnectAction>;
|
|
50
|
-
};
|
|
51
|
-
|
|
52
|
-
type ChatSurfaceLinkTokenPayload = {
|
|
53
|
-
surface: string;
|
|
54
|
-
adminUserId: AdminUser["pk"];
|
|
55
|
-
expiresAt: number;
|
|
56
|
-
};
|
|
57
|
-
|
|
58
|
-
type AgentTurnRunInput = {
|
|
59
|
-
prompt: string;
|
|
60
|
-
sessionId: string;
|
|
61
|
-
turnId: string;
|
|
62
|
-
previousUserMessages: PreviousUserMessage[];
|
|
63
|
-
modeName?: string | null;
|
|
64
|
-
userTimeZone: string;
|
|
65
|
-
currentPage?: CurrentPageContext;
|
|
66
|
-
abortSignal?: AbortSignal;
|
|
67
|
-
adminUser: AdminUser;
|
|
68
|
-
sequenceDebugCollector: ReturnType<typeof createSequenceDebugCollector>;
|
|
69
|
-
emit?: AgentEventEmitter;
|
|
70
|
-
};
|
|
71
|
-
|
|
72
|
-
type RunAndPersistAgentResponseInput =
|
|
73
|
-
Omit<AgentTurnRunInput, "turnId" | "sequenceDebugCollector" | "previousUserMessages"> & {
|
|
74
|
-
failureLogMessage: string;
|
|
75
|
-
abortLogMessage: string;
|
|
76
|
-
};
|
|
77
|
-
|
|
78
|
-
type HandleTurnInput = Omit<RunAndPersistAgentResponseInput, "failureLogMessage" | "abortLogMessage"> & {
|
|
79
|
-
emit: AgentEventEmitter;
|
|
80
|
-
failureLogMessage?: string;
|
|
81
|
-
abortLogMessage?: string;
|
|
82
|
-
};
|
|
83
|
-
|
|
84
|
-
const agentResponseBodySchema = z.object({
|
|
85
|
-
message: z.string(),
|
|
86
|
-
sessionId: z.string(),
|
|
87
|
-
mode: z.string().nullish(),
|
|
88
|
-
timeZone: z.string().optional(),
|
|
89
|
-
currentPage: z.custom<CurrentPageContext>().optional(),
|
|
90
|
-
}).strict();
|
|
91
|
-
|
|
92
|
-
const agentSpeechResponseBodySchema = agentResponseBodySchema.omit({message: true})
|
|
93
|
-
|
|
94
|
-
const addSystemMessageBodySchema = z.object({
|
|
95
|
-
sessionId: z.string(),
|
|
96
|
-
systemMessage: z.string(),
|
|
97
|
-
}).strict();
|
|
98
|
-
|
|
99
|
-
const getSessionsBodySchema = z.object({
|
|
100
|
-
limit: z.number().optional(),
|
|
101
|
-
}).strict();
|
|
102
|
-
|
|
103
|
-
const sessionIdBodySchema = z.object({
|
|
104
|
-
sessionId: z.string(),
|
|
105
|
-
}).strict();
|
|
106
|
-
|
|
107
|
-
const createSessionBodySchema = z.object({
|
|
108
|
-
triggerMessage: z.string().optional(),
|
|
109
|
-
}).strict();
|
|
110
|
-
|
|
111
|
-
const VEGA_LITE_FENCE_START = "```vega-lite";
|
|
112
|
-
const COMPLETE_VEGA_LITE_BLOCK_RE = /```vega-lite[\s\S]*?```/;
|
|
113
|
-
const DEFAULT_ADMIN_USER_EXTERNAL_USER_ID_FIELD = "externalUserId";
|
|
114
|
-
const CHAT_SURFACE_LINK_TOKEN_TTL_MS = 60 * 1000;
|
|
115
|
-
|
|
116
|
-
function isAbortError(error: unknown): boolean {
|
|
117
|
-
return (
|
|
118
|
-
error instanceof DOMException && error.name === "AbortError"
|
|
119
|
-
) || (
|
|
120
|
-
typeof error === "object" &&
|
|
121
|
-
error !== null &&
|
|
122
|
-
"name" in error &&
|
|
123
|
-
(error.name === "AbortError" || error.name === "APIUserAbortError")
|
|
124
|
-
);
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
function getErrorMessage(error: unknown): string {
|
|
128
|
-
return error instanceof Error ? error.message : String(error);
|
|
129
|
-
}
|
|
130
|
-
|
|
131
24
|
export default class AdminForthAgentPlugin extends AdminForthPlugin {
|
|
132
25
|
options: PluginOptions;
|
|
133
26
|
agentSystemPromptPromise: Promise<string>;
|
|
134
27
|
private checkpointer: BaseCheckpointSaver | null = null;
|
|
135
|
-
private
|
|
28
|
+
private sessionStore: AgentSessionStore;
|
|
29
|
+
private agentTurnService: AgentTurnService;
|
|
30
|
+
private chatSurfaceService: ChatSurfaceService;
|
|
136
31
|
private chatSurfaceSettingsPageRegistered = false;
|
|
137
32
|
private parseBody<T>(
|
|
138
33
|
schema: z.ZodType<T>,
|
|
@@ -146,72 +41,6 @@ export default class AdminForthAgentPlugin extends AdminForthPlugin {
|
|
|
146
41
|
}
|
|
147
42
|
return parsed.data;
|
|
148
43
|
}
|
|
149
|
-
private async createNewTurn(sessionId: string, prompt: string, response?: string) {
|
|
150
|
-
const turnId = randomUUID();
|
|
151
|
-
const turnRecord = {
|
|
152
|
-
[this.options.turnResource.idField]: turnId,
|
|
153
|
-
[this.options.turnResource.sessionIdField]: sessionId,
|
|
154
|
-
[this.options.turnResource.promptField]: prompt,
|
|
155
|
-
[this.options.turnResource.responseField]: response || "not_finished",
|
|
156
|
-
};
|
|
157
|
-
const newTurn = await this.adminforth.resource(this.options.turnResource.resourceId).create(turnRecord);
|
|
158
|
-
return newTurn.createdRecord[this.options.turnResource.idField];
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
private async getSessionTurns(sessionId: string) {
|
|
162
|
-
const turns = await this.adminforth.resource(this.options.turnResource.resourceId).list(
|
|
163
|
-
[Filters.EQ(this.options.turnResource.sessionIdField, sessionId)],
|
|
164
|
-
undefined,
|
|
165
|
-
undefined,
|
|
166
|
-
[Sorts.ASC(this.options.turnResource.createdAtField)]
|
|
167
|
-
);
|
|
168
|
-
return turns.map(turn => ({
|
|
169
|
-
prompt: turn[this.options.turnResource.promptField],
|
|
170
|
-
response: turn[this.options.turnResource.responseField],
|
|
171
|
-
}));
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
private async getPreviousUserMessages(sessionId: string) {
|
|
175
|
-
const turns = await this.adminforth.resource(this.options.turnResource.resourceId).list(
|
|
176
|
-
[Filters.EQ(this.options.turnResource.sessionIdField, sessionId)],
|
|
177
|
-
2,
|
|
178
|
-
undefined,
|
|
179
|
-
[Sorts.DESC(this.options.turnResource.createdAtField)]
|
|
180
|
-
);
|
|
181
|
-
return turns
|
|
182
|
-
.reverse()
|
|
183
|
-
.map((turn): PreviousUserMessage => ({
|
|
184
|
-
text: turn[this.options.turnResource.promptField],
|
|
185
|
-
}));
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
private getChatSurfaceSessionId(incoming: ChatSurfaceIncomingMessage) {
|
|
189
|
-
return `${incoming.surface}:${incoming.externalConversationId}`;
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
private async getOrCreateChatSurfaceSession(
|
|
193
|
-
incoming: ChatSurfaceIncomingMessage,
|
|
194
|
-
adminUser: AdminUser,
|
|
195
|
-
) {
|
|
196
|
-
const sessionId = this.getChatSurfaceSessionId(incoming);
|
|
197
|
-
const sessionResource = this.adminforth.resource(this.options.sessionResource.resourceId);
|
|
198
|
-
const session = await sessionResource.get(
|
|
199
|
-
[Filters.EQ(this.options.sessionResource.idField, sessionId)]
|
|
200
|
-
);
|
|
201
|
-
|
|
202
|
-
if (session) {
|
|
203
|
-
return sessionId;
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
await sessionResource.create({
|
|
207
|
-
[this.options.sessionResource.idField]: sessionId,
|
|
208
|
-
[this.options.sessionResource.titleField]: incoming.prompt.slice(0, 40) || "New Session",
|
|
209
|
-
[this.options.sessionResource.askerIdField]: adminUser.pk,
|
|
210
|
-
});
|
|
211
|
-
|
|
212
|
-
return sessionId;
|
|
213
|
-
}
|
|
214
|
-
|
|
215
44
|
private getCheckpointer() {
|
|
216
45
|
if (this.checkpointer) return this.checkpointer;
|
|
217
46
|
|
|
@@ -230,15 +59,26 @@ export default class AdminForthAgentPlugin extends AdminForthPlugin {
|
|
|
230
59
|
].filter((resourceId): resourceId is string => Boolean(resourceId));
|
|
231
60
|
}
|
|
232
61
|
|
|
233
|
-
private getChatSurfaceConnectActionAdapters() {
|
|
234
|
-
return (this.options.chatSurfaceAdapters ?? [])
|
|
235
|
-
.map((adapter) => adapter as ChatSurfaceAdapterWithConnectAction)
|
|
236
|
-
.filter((adapter) => adapter.createConnectAction);
|
|
237
|
-
}
|
|
238
|
-
|
|
239
62
|
constructor(options: PluginOptions) {
|
|
240
63
|
super(options, import.meta.url);
|
|
241
64
|
this.options = options;
|
|
65
|
+
this.sessionStore = new AgentSessionStore(() => this.adminforth, this.options);
|
|
66
|
+
this.agentTurnService = new AgentTurnService({
|
|
67
|
+
getAdminforth: () => this.adminforth,
|
|
68
|
+
getPluginInstanceId: () => this.pluginInstanceId,
|
|
69
|
+
options: this.options,
|
|
70
|
+
sessionStore: this.sessionStore,
|
|
71
|
+
getCheckpointer: this.getCheckpointer.bind(this),
|
|
72
|
+
getInternalAgentResourceIds: this.getInternalAgentResourceIds.bind(this),
|
|
73
|
+
getAgentSystemPrompt: () => this.agentSystemPromptPromise,
|
|
74
|
+
});
|
|
75
|
+
this.chatSurfaceService = new ChatSurfaceService(
|
|
76
|
+
() => this.adminforth,
|
|
77
|
+
this.options,
|
|
78
|
+
this.sessionStore,
|
|
79
|
+
this.agentTurnService.handleTurn.bind(this.agentTurnService),
|
|
80
|
+
this.agentTurnService.runAndPersistAgentResponse.bind(this.agentTurnService),
|
|
81
|
+
);
|
|
242
82
|
this.agentSystemPromptPromise = Promise.resolve(
|
|
243
83
|
appendCustomSystemPrompt(DEFAULT_AGENT_SYSTEM_PROMPT, this.options.systemPrompt),
|
|
244
84
|
);
|
|
@@ -263,7 +103,7 @@ export default class AdminForthAgentPlugin extends AdminForthPlugin {
|
|
|
263
103
|
hasAudioAdapter: Boolean(this.options.audioAdapter),
|
|
264
104
|
}
|
|
265
105
|
});
|
|
266
|
-
if (this.
|
|
106
|
+
if (this.chatSurfaceService.getConnectActionAdapters().length && !this.chatSurfaceSettingsPageRegistered) {
|
|
267
107
|
if (!this.adminforth.config.auth!.userMenuSettingsPages) {
|
|
268
108
|
this.adminforth.config.auth!.userMenuSettingsPages = [];
|
|
269
109
|
}
|
|
@@ -314,869 +154,24 @@ export default class AdminForthAgentPlugin extends AdminForthPlugin {
|
|
|
314
154
|
return `single`;
|
|
315
155
|
}
|
|
316
156
|
|
|
317
|
-
private async runAgentTurn(input: AgentTurnRunInput) {
|
|
318
|
-
let fullResponse = "";
|
|
319
|
-
let bufferedTextDelta = "";
|
|
320
|
-
let isRenderingVegaLite = false;
|
|
321
|
-
const maxTokens = this.options.maxTokens ?? 1000;
|
|
322
|
-
const selectedMode = this.options.modes.find((mode) => mode.name === input.modeName) ?? this.options.modes[0];
|
|
323
|
-
const [primaryModelSpec, summaryModelSpec] = await Promise.all([
|
|
324
|
-
createAgentChatModel({
|
|
325
|
-
adapter: selectedMode.completionAdapter,
|
|
326
|
-
maxTokens,
|
|
327
|
-
purpose: "primary",
|
|
328
|
-
}),
|
|
329
|
-
createAgentChatModel({
|
|
330
|
-
adapter: selectedMode.completionAdapter,
|
|
331
|
-
maxTokens,
|
|
332
|
-
purpose: "summary",
|
|
333
|
-
}),
|
|
334
|
-
]);
|
|
335
|
-
const model = primaryModelSpec.model;
|
|
336
|
-
const summaryModel = summaryModelSpec.model;
|
|
337
|
-
const modelMiddleware = primaryModelSpec.middleware;
|
|
338
|
-
|
|
339
|
-
const userLanguage = await detectUserLanguage(selectedMode.completionAdapter, input.prompt, input.previousUserMessages)
|
|
340
|
-
.catch((error) => {
|
|
341
|
-
if (input.abortSignal?.aborted || isAbortError(error)) {
|
|
342
|
-
throw error;
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
logger.warn(`Failed to detect user language: ${getErrorMessage(error)}`);
|
|
346
|
-
return null;
|
|
347
|
-
});
|
|
348
|
-
const systemPrompt = buildAgentTurnSystemPrompt({
|
|
349
|
-
agentSystemPrompt: await this.agentSystemPromptPromise,
|
|
350
|
-
adminUser: input.adminUser,
|
|
351
|
-
usernameField: this.adminforth.config.auth!.usernameField,
|
|
352
|
-
userLanguage,
|
|
353
|
-
});
|
|
354
|
-
const apiBasedTools = buildApiBasedTools(
|
|
355
|
-
this.adminforth,
|
|
356
|
-
this.getInternalAgentResourceIds(),
|
|
357
|
-
);
|
|
358
|
-
|
|
359
|
-
const stream = await callAgent({
|
|
360
|
-
name: `adminforth-agent-${this.pluginInstanceId}`,
|
|
361
|
-
model,
|
|
362
|
-
summaryModel,
|
|
363
|
-
modelMiddleware,
|
|
364
|
-
checkpointer: this.getCheckpointer(),
|
|
365
|
-
messages: [
|
|
366
|
-
new SystemMessage(systemPrompt),
|
|
367
|
-
new HumanMessage(input.prompt),
|
|
368
|
-
],
|
|
369
|
-
adminUser: input.adminUser,
|
|
370
|
-
adminforth: this.adminforth,
|
|
371
|
-
apiBasedTools,
|
|
372
|
-
customComponentsDir: this.adminforth.config.customization.customComponentsDir ?? "custom",
|
|
373
|
-
sessionId: input.sessionId,
|
|
374
|
-
turnId: input.turnId,
|
|
375
|
-
currentPage: input.currentPage,
|
|
376
|
-
userTimeZone: input.userTimeZone,
|
|
377
|
-
abortSignal: input.abortSignal,
|
|
378
|
-
emitToolCallEvent: (event) => {
|
|
379
|
-
input.sequenceDebugCollector.handleToolCallEvent(event);
|
|
380
|
-
void input.emit?.({
|
|
381
|
-
type: "tool-call",
|
|
382
|
-
data: event,
|
|
383
|
-
});
|
|
384
|
-
},
|
|
385
|
-
sequenceDebugSink: input.sequenceDebugCollector,
|
|
386
|
-
});
|
|
387
|
-
|
|
388
|
-
for await (const rawChunk of stream as AsyncIterable<[any, any]>) {
|
|
389
|
-
if (input.abortSignal?.aborted) {
|
|
390
|
-
throw new DOMException("This operation was aborted", "AbortError");
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
const [token, metadata] = rawChunk;
|
|
394
|
-
|
|
395
|
-
const nodeName =
|
|
396
|
-
typeof metadata?.langgraph_node === "string"
|
|
397
|
-
? metadata.langgraph_node
|
|
398
|
-
: "";
|
|
399
|
-
|
|
400
|
-
if (nodeName && !["model", "model_request"].includes(nodeName)) {
|
|
401
|
-
continue;
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
const blocks = Array.isArray(token?.contentBlocks)
|
|
405
|
-
? token.contentBlocks
|
|
406
|
-
: Array.isArray(token?.content)
|
|
407
|
-
? token.content
|
|
408
|
-
: [];
|
|
409
|
-
const reasoningDelta = blocks
|
|
410
|
-
.filter((b: any) => b?.type === "reasoning")
|
|
411
|
-
.map((b: any) => String(b.reasoning ?? ""))
|
|
412
|
-
.join("");
|
|
413
|
-
|
|
414
|
-
const textDelta = blocks
|
|
415
|
-
.filter((b: any) => b?.type === "text")
|
|
416
|
-
.map((b: any) => String(b.text ?? ""))
|
|
417
|
-
.join("");
|
|
418
|
-
|
|
419
|
-
if (reasoningDelta) {
|
|
420
|
-
await input.emit?.({
|
|
421
|
-
type: "reasoning-delta",
|
|
422
|
-
delta: reasoningDelta,
|
|
423
|
-
});
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
if (textDelta) {
|
|
427
|
-
fullResponse += textDelta;
|
|
428
|
-
bufferedTextDelta += textDelta;
|
|
429
|
-
|
|
430
|
-
if (
|
|
431
|
-
bufferedTextDelta.includes(VEGA_LITE_FENCE_START) &&
|
|
432
|
-
!COMPLETE_VEGA_LITE_BLOCK_RE.test(bufferedTextDelta)
|
|
433
|
-
) {
|
|
434
|
-
if (!isRenderingVegaLite) {
|
|
435
|
-
isRenderingVegaLite = true;
|
|
436
|
-
await input.emit?.({
|
|
437
|
-
type: "rendering",
|
|
438
|
-
phase: "start",
|
|
439
|
-
label: "Rendering...",
|
|
440
|
-
});
|
|
441
|
-
}
|
|
442
|
-
continue;
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
if (isRenderingVegaLite) {
|
|
446
|
-
isRenderingVegaLite = false;
|
|
447
|
-
await input.emit?.({
|
|
448
|
-
type: "rendering",
|
|
449
|
-
phase: "end",
|
|
450
|
-
label: "Rendering...",
|
|
451
|
-
});
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
const streamableLength = bufferedTextDelta.includes(VEGA_LITE_FENCE_START)
|
|
455
|
-
? bufferedTextDelta.length
|
|
456
|
-
: bufferedTextDelta.length - getPartialVegaLiteFenceStartLength(bufferedTextDelta);
|
|
457
|
-
|
|
458
|
-
if (!streamableLength) {
|
|
459
|
-
continue;
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
await input.emit?.({
|
|
463
|
-
type: "text-delta",
|
|
464
|
-
delta: bufferedTextDelta.slice(0, streamableLength),
|
|
465
|
-
});
|
|
466
|
-
bufferedTextDelta = bufferedTextDelta.slice(streamableLength);
|
|
467
|
-
}
|
|
468
|
-
}
|
|
469
|
-
|
|
470
|
-
if (isRenderingVegaLite) {
|
|
471
|
-
await input.emit?.({
|
|
472
|
-
type: "rendering",
|
|
473
|
-
phase: "end",
|
|
474
|
-
label: "Rendering...",
|
|
475
|
-
});
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
if (bufferedTextDelta) {
|
|
479
|
-
await input.emit?.({
|
|
480
|
-
type: "text-delta",
|
|
481
|
-
delta: bufferedTextDelta,
|
|
482
|
-
});
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
return {
|
|
486
|
-
text: fullResponse,
|
|
487
|
-
};
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
private async runAndPersistAgentResponse(input: RunAndPersistAgentResponseInput) {
|
|
491
|
-
const previousUserMessages = await this.getPreviousUserMessages(input.sessionId);
|
|
492
|
-
const turnId = await this.createNewTurn(input.sessionId, input.prompt);
|
|
493
|
-
await this.adminforth.resource(this.options.sessionResource.resourceId).update(input.sessionId, {
|
|
494
|
-
[this.options.sessionResource.createdAtField]: new Date().toISOString(),
|
|
495
|
-
});
|
|
496
|
-
const sequenceDebugCollector = createSequenceDebugCollector();
|
|
497
|
-
let fullResponse = "";
|
|
498
|
-
let aborted = false;
|
|
499
|
-
let failed = false;
|
|
500
|
-
|
|
501
|
-
try {
|
|
502
|
-
const agentResponse = await this.runAgentTurn({
|
|
503
|
-
prompt: input.prompt,
|
|
504
|
-
sessionId: input.sessionId,
|
|
505
|
-
turnId,
|
|
506
|
-
previousUserMessages,
|
|
507
|
-
modeName: input.modeName,
|
|
508
|
-
userTimeZone: input.userTimeZone,
|
|
509
|
-
currentPage: input.currentPage,
|
|
510
|
-
abortSignal: input.abortSignal,
|
|
511
|
-
adminUser: input.adminUser,
|
|
512
|
-
sequenceDebugCollector,
|
|
513
|
-
emit: input.emit,
|
|
514
|
-
});
|
|
515
|
-
fullResponse = agentResponse.text;
|
|
516
|
-
} catch (error) {
|
|
517
|
-
if (input.abortSignal?.aborted || isAbortError(error)) {
|
|
518
|
-
aborted = true;
|
|
519
|
-
logger.info(input.abortLogMessage);
|
|
520
|
-
} else {
|
|
521
|
-
failed = true;
|
|
522
|
-
fullResponse = getErrorMessage(error);
|
|
523
|
-
logger.error(`${input.failureLogMessage}:\n${fullResponse}`);
|
|
524
|
-
}
|
|
525
|
-
}
|
|
526
|
-
|
|
527
|
-
sequenceDebugCollector.flush();
|
|
528
|
-
const turnUpdates: Record<string, unknown> = {
|
|
529
|
-
[this.options.turnResource.responseField]: fullResponse,
|
|
530
|
-
};
|
|
531
|
-
|
|
532
|
-
if (this.options.turnResource.debugField) {
|
|
533
|
-
turnUpdates[this.options.turnResource.debugField] = sequenceDebugCollector.getHistory();
|
|
534
|
-
}
|
|
535
|
-
|
|
536
|
-
await this.adminforth.resource(this.options.turnResource.resourceId).update(turnId, turnUpdates);
|
|
537
|
-
|
|
538
|
-
return {
|
|
539
|
-
text: fullResponse,
|
|
540
|
-
turnId,
|
|
541
|
-
aborted,
|
|
542
|
-
failed,
|
|
543
|
-
};
|
|
544
|
-
}
|
|
545
|
-
|
|
546
|
-
async handleTurn(input: HandleTurnInput) {
|
|
547
|
-
await input.emit({
|
|
548
|
-
type: "turn-started",
|
|
549
|
-
messageId: randomUUID(),
|
|
550
|
-
});
|
|
551
|
-
|
|
552
|
-
const agentResponse = await this.runAndPersistAgentResponse({
|
|
553
|
-
prompt: input.prompt,
|
|
554
|
-
sessionId: input.sessionId,
|
|
555
|
-
modeName: input.modeName,
|
|
556
|
-
userTimeZone: input.userTimeZone,
|
|
557
|
-
currentPage: input.currentPage,
|
|
558
|
-
abortSignal: input.abortSignal,
|
|
559
|
-
adminUser: input.adminUser,
|
|
560
|
-
emit: input.emit,
|
|
561
|
-
failureLogMessage: input.failureLogMessage ?? "Agent response failed",
|
|
562
|
-
abortLogMessage: input.abortLogMessage ?? "Agent response aborted",
|
|
563
|
-
});
|
|
564
|
-
|
|
565
|
-
if (agentResponse.failed) {
|
|
566
|
-
await input.emit({
|
|
567
|
-
type: "error",
|
|
568
|
-
error: agentResponse.text,
|
|
569
|
-
});
|
|
570
|
-
} else if (!agentResponse.aborted) {
|
|
571
|
-
await input.emit({
|
|
572
|
-
type: "response",
|
|
573
|
-
text: agentResponse.text,
|
|
574
|
-
sessionId: input.sessionId,
|
|
575
|
-
turnId: agentResponse.turnId,
|
|
576
|
-
});
|
|
577
|
-
}
|
|
578
|
-
|
|
579
|
-
await input.emit({
|
|
580
|
-
type: "finish",
|
|
581
|
-
});
|
|
582
|
-
|
|
583
|
-
return agentResponse;
|
|
584
|
-
}
|
|
585
|
-
|
|
586
|
-
private createChatSurfaceEventEmitter(sink: ChatSurfaceEventSink): AgentEventEmitter {
|
|
587
|
-
return async (event) => {
|
|
588
|
-
if (event.type === "text-delta") {
|
|
589
|
-
await sink.emit({
|
|
590
|
-
type: "text_delta",
|
|
591
|
-
delta: event.delta,
|
|
592
|
-
});
|
|
593
|
-
return;
|
|
594
|
-
}
|
|
595
|
-
|
|
596
|
-
if (event.type === "response") {
|
|
597
|
-
await sink.emit({
|
|
598
|
-
type: "done",
|
|
599
|
-
text: event.text,
|
|
600
|
-
});
|
|
601
|
-
return;
|
|
602
|
-
}
|
|
603
|
-
|
|
604
|
-
if (event.type === "error") {
|
|
605
|
-
await sink.emit({
|
|
606
|
-
type: "error",
|
|
607
|
-
message: event.error,
|
|
608
|
-
});
|
|
609
|
-
}
|
|
610
|
-
};
|
|
611
|
-
}
|
|
612
|
-
|
|
613
|
-
private createChatSurfaceLinkToken(surface: string, adminUser: AdminUser) {
|
|
614
|
-
for (const [token, payload] of this.chatSurfaceLinkTokens) {
|
|
615
|
-
if (payload.expiresAt <= Date.now()) {
|
|
616
|
-
this.chatSurfaceLinkTokens.delete(token);
|
|
617
|
-
}
|
|
618
|
-
}
|
|
619
|
-
|
|
620
|
-
const token = randomUUID();
|
|
621
|
-
this.chatSurfaceLinkTokens.set(token, {
|
|
622
|
-
surface,
|
|
623
|
-
adminUserId: adminUser.pk,
|
|
624
|
-
expiresAt: Date.now() + CHAT_SURFACE_LINK_TOKEN_TTL_MS,
|
|
625
|
-
});
|
|
626
|
-
|
|
627
|
-
return token;
|
|
628
|
-
}
|
|
629
|
-
|
|
630
|
-
private consumeChatSurfaceLinkToken(surface: string, token: string) {
|
|
631
|
-
const payload = this.chatSurfaceLinkTokens.get(token);
|
|
632
|
-
this.chatSurfaceLinkTokens.delete(token);
|
|
633
|
-
|
|
634
|
-
if (!payload || payload.surface !== surface || payload.expiresAt <= Date.now()) {
|
|
635
|
-
return null;
|
|
636
|
-
}
|
|
637
|
-
|
|
638
|
-
return payload;
|
|
639
|
-
}
|
|
640
|
-
|
|
641
|
-
private async handleChatSurfaceLink(
|
|
642
|
-
incoming: ChatSurfaceIncomingMessage,
|
|
643
|
-
sink: ChatSurfaceEventSink,
|
|
644
|
-
) {
|
|
645
|
-
if (typeof incoming.metadata?.startPayload !== "string") {
|
|
646
|
-
return false;
|
|
647
|
-
}
|
|
648
|
-
|
|
649
|
-
const payload = this.consumeChatSurfaceLinkToken(incoming.surface, incoming.metadata.startPayload);
|
|
650
|
-
if (!payload) {
|
|
651
|
-
await sink.emit({
|
|
652
|
-
type: "error",
|
|
653
|
-
message: "This chat surface link is expired or invalid. Please start linking again from AdminForth.",
|
|
654
|
-
});
|
|
655
|
-
return true;
|
|
656
|
-
}
|
|
657
|
-
const externalUserIdField = this.options.chatExternalIdsField ?? DEFAULT_ADMIN_USER_EXTERNAL_USER_ID_FIELD;
|
|
658
|
-
const authResourceId = this.adminforth.config.auth!.usersResourceId!;
|
|
659
|
-
const authResource = this.adminforth.config.resources.find((resource) => resource.resourceId === authResourceId)!;
|
|
660
|
-
const primaryKeyField = authResource.columns.find((column) => column.primaryKey)!.name!;
|
|
661
|
-
const adminUserRecord = await this.adminforth.resource(authResourceId).get([
|
|
662
|
-
Filters.EQ(primaryKeyField, payload.adminUserId),
|
|
663
|
-
]);
|
|
664
|
-
|
|
665
|
-
await this.adminforth.resource(authResourceId).update(payload.adminUserId, {
|
|
666
|
-
[externalUserIdField]: {
|
|
667
|
-
...(adminUserRecord[externalUserIdField] ?? {}),
|
|
668
|
-
[incoming.surface]: incoming.externalUserId,
|
|
669
|
-
},
|
|
670
|
-
});
|
|
671
|
-
await sink.emit({
|
|
672
|
-
type: "done",
|
|
673
|
-
text: `${incoming.surface} account connected to AdminForth.`,
|
|
674
|
-
});
|
|
675
|
-
|
|
676
|
-
return true;
|
|
677
|
-
}
|
|
678
|
-
|
|
679
|
-
private async handleChatSurfaceMessage(
|
|
680
|
-
adapter: ChatSurfaceAdapter,
|
|
681
|
-
incoming: ChatSurfaceIncomingMessage,
|
|
682
|
-
sink: ChatSurfaceEventSink,
|
|
683
|
-
) {
|
|
684
|
-
if (await this.handleChatSurfaceLink(incoming, sink)) {
|
|
685
|
-
return;
|
|
686
|
-
}
|
|
687
|
-
|
|
688
|
-
const authResourceId = this.adminforth.config.auth!.usersResourceId!;
|
|
689
|
-
const authResource = this.adminforth.config.resources.find((resource) => resource.resourceId === authResourceId)!;
|
|
690
|
-
const primaryKeyField = authResource.columns.find((column) => column.primaryKey)!.name!;
|
|
691
|
-
const externalUserIdField = this.options.chatExternalIdsField ?? DEFAULT_ADMIN_USER_EXTERNAL_USER_ID_FIELD;
|
|
692
|
-
const adminUserRecord = (
|
|
693
|
-
await this.adminforth.resource(authResourceId).list(Filters.IS_NOT_EMPTY(externalUserIdField))
|
|
694
|
-
).find((user) => user[externalUserIdField]?.[adapter.name] === incoming.externalUserId);
|
|
695
|
-
|
|
696
|
-
if (!adminUserRecord) {
|
|
697
|
-
await sink.emit({
|
|
698
|
-
type: "error",
|
|
699
|
-
message: "This chat account is not authorized to use AdminForth Agent.",
|
|
700
|
-
});
|
|
701
|
-
return;
|
|
702
|
-
}
|
|
703
|
-
|
|
704
|
-
const adminUser = {
|
|
705
|
-
pk: adminUserRecord[primaryKeyField],
|
|
706
|
-
username: adminUserRecord[this.adminforth.config.auth!.usernameField],
|
|
707
|
-
dbUser: adminUserRecord,
|
|
708
|
-
};
|
|
709
|
-
|
|
710
|
-
await this.handleTurn({
|
|
711
|
-
prompt: incoming.prompt,
|
|
712
|
-
sessionId: await this.getOrCreateChatSurfaceSession(incoming, adminUser),
|
|
713
|
-
modeName: incoming.modeName,
|
|
714
|
-
userTimeZone: incoming.userTimeZone ?? "UTC",
|
|
715
|
-
adminUser,
|
|
716
|
-
emit: this.createChatSurfaceEventEmitter(sink),
|
|
717
|
-
failureLogMessage: `Agent ${incoming.surface} surface response failed`,
|
|
718
|
-
abortLogMessage: `Agent ${incoming.surface} surface response aborted`,
|
|
719
|
-
});
|
|
720
|
-
}
|
|
721
|
-
|
|
722
157
|
setupEndpoints(server: IHttpServer) {
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
for (const adapter of this.options.chatSurfaceAdapters ?? []) {
|
|
742
|
-
const connectActionAdapter = adapter as ChatSurfaceAdapterWithConnectAction;
|
|
743
|
-
if (connectActionAdapter.createConnectAction) {
|
|
744
|
-
server.endpoint({
|
|
745
|
-
method: "POST",
|
|
746
|
-
path: `/agent/surface/${adapter.name}/connect-action`,
|
|
747
|
-
handler: async ({ adminUser }) => {
|
|
748
|
-
const token = this.createChatSurfaceLinkToken(adapter.name, adminUser);
|
|
749
|
-
const action = await connectActionAdapter.createConnectAction!({ token });
|
|
750
|
-
|
|
751
|
-
return {
|
|
752
|
-
action,
|
|
753
|
-
};
|
|
754
|
-
},
|
|
755
|
-
});
|
|
756
|
-
server.endpoint({
|
|
757
|
-
method: "POST",
|
|
758
|
-
path: `/agent/surface/${adapter.name}/disconnect`,
|
|
759
|
-
handler: async ({ adminUser }) => {
|
|
760
|
-
const externalUserIdField = this.options.chatExternalIdsField ?? DEFAULT_ADMIN_USER_EXTERNAL_USER_ID_FIELD;
|
|
761
|
-
const externalIds = {
|
|
762
|
-
...(adminUser.dbUser[externalUserIdField] ?? {}),
|
|
763
|
-
};
|
|
764
|
-
|
|
765
|
-
delete externalIds[adapter.name];
|
|
766
|
-
|
|
767
|
-
await this.adminforth.resource(this.adminforth.config.auth!.usersResourceId!).update(adminUser.pk, {
|
|
768
|
-
[externalUserIdField]: externalIds,
|
|
769
|
-
});
|
|
770
|
-
|
|
771
|
-
return {
|
|
772
|
-
ok: true,
|
|
773
|
-
};
|
|
774
|
-
},
|
|
775
|
-
});
|
|
776
|
-
}
|
|
777
|
-
|
|
778
|
-
server.endpoint({
|
|
779
|
-
method: "POST",
|
|
780
|
-
noAuth: true,
|
|
781
|
-
path: `/agent/surface/${adapter.name}/webhook`,
|
|
782
|
-
handler: async (ctx: IAdminForthEndpointHandlerInput) => {
|
|
783
|
-
const surfaceContext = {
|
|
784
|
-
body: ctx.body,
|
|
785
|
-
headers: ctx.headers,
|
|
786
|
-
abortSignal: ctx.abortSignal,
|
|
787
|
-
rawRequest: ctx._raw_express_req,
|
|
788
|
-
rawResponse: ctx._raw_express_res,
|
|
789
|
-
};
|
|
790
|
-
const incoming = await adapter.parseIncomingMessage(surfaceContext);
|
|
791
|
-
|
|
792
|
-
if (!incoming) return { ok: true };
|
|
793
|
-
|
|
794
|
-
const sink = await adapter.createEventSink(surfaceContext, incoming);
|
|
795
|
-
|
|
796
|
-
try {
|
|
797
|
-
await this.handleChatSurfaceMessage(adapter, incoming, sink);
|
|
798
|
-
} finally {
|
|
799
|
-
await sink.close?.();
|
|
800
|
-
}
|
|
801
|
-
|
|
802
|
-
return { ok: true };
|
|
803
|
-
},
|
|
804
|
-
});
|
|
805
|
-
}
|
|
806
|
-
|
|
807
|
-
server.endpoint({
|
|
808
|
-
method: 'POST',
|
|
809
|
-
path: `/agent/get-placeholder-messages`,
|
|
810
|
-
handler: async ({ headers, adminUser }) => {
|
|
811
|
-
if (!this.options.placeholderMessages) {
|
|
812
|
-
return {
|
|
813
|
-
messages: [],
|
|
814
|
-
};
|
|
815
|
-
}
|
|
816
|
-
|
|
817
|
-
const messages = await this.options.placeholderMessages({
|
|
818
|
-
adminUser: adminUser,
|
|
819
|
-
headers,
|
|
820
|
-
});
|
|
821
|
-
|
|
822
|
-
return {
|
|
823
|
-
messages,
|
|
824
|
-
};
|
|
825
|
-
}
|
|
826
|
-
});
|
|
827
|
-
server.endpoint({
|
|
828
|
-
method: 'POST',
|
|
829
|
-
path: `/agent/response`,
|
|
830
|
-
handler: async ({ body, adminUser, response, _raw_express_res, abortSignal }) => {
|
|
831
|
-
const data = this.parseBody(agentResponseBodySchema, body, response);
|
|
832
|
-
if (!data) return;
|
|
833
|
-
const emit = createSseEventEmitter(_raw_express_res, {
|
|
834
|
-
vercelAiUiMessageStream: true,
|
|
835
|
-
closeActiveBlockOnToolStart: true,
|
|
836
|
-
});
|
|
837
|
-
|
|
838
|
-
await this.handleTurn({
|
|
839
|
-
prompt: data.message,
|
|
840
|
-
sessionId: data.sessionId,
|
|
841
|
-
modeName: data.mode,
|
|
842
|
-
userTimeZone: data.timeZone ?? 'UTC',
|
|
843
|
-
currentPage: data.currentPage,
|
|
844
|
-
abortSignal,
|
|
845
|
-
adminUser: adminUser,
|
|
846
|
-
emit,
|
|
847
|
-
failureLogMessage: "Agent response streaming failed",
|
|
848
|
-
abortLogMessage: "Agent response streaming aborted by the client",
|
|
849
|
-
});
|
|
850
|
-
return null;
|
|
851
|
-
}
|
|
852
|
-
});
|
|
853
|
-
server.endpoint({
|
|
854
|
-
method: 'POST',
|
|
855
|
-
path: `/agent/speech-response`,
|
|
856
|
-
target: 'upload',
|
|
857
|
-
handler: async ({ body, adminUser, response, _raw_express_req, _raw_express_res, abortSignal }) => {
|
|
858
|
-
const req = _raw_express_req as ExpressMulterRequest;
|
|
859
|
-
const audioAdapter = this.options.audioAdapter;
|
|
860
|
-
if (!audioAdapter) {
|
|
861
|
-
response.setStatus(400, "Audio adapter is not configured for AdminForth Agent");
|
|
862
|
-
return { error: "Audio adapter is not configured for AdminForth Agent" };
|
|
863
|
-
}
|
|
864
|
-
const data = this.parseBody(agentSpeechResponseBodySchema, body, response);
|
|
865
|
-
if (!data) return;
|
|
866
|
-
if (!req.file) {
|
|
867
|
-
response.setStatus(400, "Audio file is required");
|
|
868
|
-
return { error: "Audio file is required" };
|
|
869
|
-
}
|
|
870
|
-
const emit = createSseEventEmitter(_raw_express_res);
|
|
871
|
-
|
|
872
|
-
let transcription;
|
|
873
|
-
|
|
874
|
-
try {
|
|
875
|
-
transcription = await audioAdapter.transcribe({
|
|
876
|
-
buffer: req.file.buffer,
|
|
877
|
-
filename: req.file.originalname,
|
|
878
|
-
mimeType: req.file.mimetype,
|
|
879
|
-
language: "auto",
|
|
880
|
-
abortSignal,
|
|
881
|
-
});
|
|
882
|
-
} catch (error) {
|
|
883
|
-
if (abortSignal.aborted || isAbortError(error)) {
|
|
884
|
-
logger.info("Agent speech transcription aborted by the client");
|
|
885
|
-
await emit({ type: "finish" });
|
|
886
|
-
return null;
|
|
887
|
-
}
|
|
888
|
-
|
|
889
|
-
logger.error(`Agent speech transcription failed:\n${getErrorMessage(error)}`);
|
|
890
|
-
await emit({
|
|
891
|
-
type: "error",
|
|
892
|
-
error: "Speech transcription failed. Check server logs for details.",
|
|
893
|
-
});
|
|
894
|
-
await emit({ type: "finish" });
|
|
895
|
-
return null;
|
|
896
|
-
}
|
|
897
|
-
|
|
898
|
-
if (abortSignal.aborted) {
|
|
899
|
-
await emit({ type: "finish" });
|
|
900
|
-
return null;
|
|
901
|
-
}
|
|
902
|
-
|
|
903
|
-
const prompt = transcription.text;
|
|
904
|
-
if (!prompt) {
|
|
905
|
-
await emit({
|
|
906
|
-
type: "error",
|
|
907
|
-
error: "Speech transcription is empty",
|
|
908
|
-
});
|
|
909
|
-
await emit({ type: "finish" });
|
|
910
|
-
return null;
|
|
911
|
-
}
|
|
912
|
-
await emit({
|
|
913
|
-
type: "transcript",
|
|
914
|
-
text: transcription.text,
|
|
915
|
-
language: transcription.language,
|
|
916
|
-
});
|
|
917
|
-
|
|
918
|
-
const sessionId = data.sessionId as string;
|
|
919
|
-
const currentPage = data.currentPage;
|
|
920
|
-
const agentResponse = await this.runAndPersistAgentResponse({
|
|
921
|
-
prompt,
|
|
922
|
-
sessionId,
|
|
923
|
-
modeName: data.mode,
|
|
924
|
-
userTimeZone: data.timeZone ?? 'UTC',
|
|
925
|
-
currentPage,
|
|
926
|
-
abortSignal,
|
|
927
|
-
adminUser: adminUser,
|
|
928
|
-
emit: async (event) => {
|
|
929
|
-
if (event.type === "tool-call") {
|
|
930
|
-
await emit(event);
|
|
931
|
-
}
|
|
932
|
-
},
|
|
933
|
-
failureLogMessage: "Agent speech response failed",
|
|
934
|
-
abortLogMessage: "Agent speech response aborted by the client",
|
|
935
|
-
});
|
|
936
|
-
|
|
937
|
-
if (agentResponse.aborted) {
|
|
938
|
-
await emit({ type: "finish" });
|
|
939
|
-
return null;
|
|
940
|
-
}
|
|
941
|
-
|
|
942
|
-
if (agentResponse.failed) {
|
|
943
|
-
await emit({
|
|
944
|
-
type: "error",
|
|
945
|
-
error: agentResponse.text,
|
|
946
|
-
});
|
|
947
|
-
await emit({ type: "finish" });
|
|
948
|
-
return null;
|
|
949
|
-
}
|
|
950
|
-
|
|
951
|
-
try {
|
|
952
|
-
await emit({
|
|
953
|
-
type: "speech-response",
|
|
954
|
-
transcript: {
|
|
955
|
-
text: transcription.text,
|
|
956
|
-
language: transcription.language,
|
|
957
|
-
},
|
|
958
|
-
response: {
|
|
959
|
-
text: agentResponse.text,
|
|
960
|
-
},
|
|
961
|
-
sessionId,
|
|
962
|
-
turnId: agentResponse.turnId,
|
|
963
|
-
});
|
|
964
|
-
const speech = await audioAdapter.synthesize({
|
|
965
|
-
text: sanitizeSpeechText(agentResponse.text),
|
|
966
|
-
stream: true,
|
|
967
|
-
streamFormat: "audio",
|
|
968
|
-
format: "pcm",
|
|
969
|
-
abortSignal,
|
|
970
|
-
});
|
|
971
|
-
|
|
972
|
-
await emit({
|
|
973
|
-
type: "audio-start",
|
|
974
|
-
mimeType: speech.mimeType,
|
|
975
|
-
format: speech.format,
|
|
976
|
-
sampleRate: 24000,
|
|
977
|
-
channelCount: 1,
|
|
978
|
-
bitsPerSample: 16,
|
|
979
|
-
});
|
|
980
|
-
|
|
981
|
-
const reader = speech.audioStream.getReader();
|
|
982
|
-
const cancelAudioStream = () => {
|
|
983
|
-
void reader.cancel().catch(() => undefined);
|
|
984
|
-
};
|
|
985
|
-
|
|
986
|
-
try {
|
|
987
|
-
abortSignal.addEventListener("abort", cancelAudioStream, { once: true });
|
|
988
|
-
|
|
989
|
-
while (true) {
|
|
990
|
-
if (abortSignal.aborted) {
|
|
991
|
-
await reader.cancel().catch(() => undefined);
|
|
992
|
-
break;
|
|
993
|
-
}
|
|
994
|
-
|
|
995
|
-
const { value, done } = await reader.read();
|
|
996
|
-
|
|
997
|
-
if (done) {
|
|
998
|
-
break;
|
|
999
|
-
}
|
|
1000
|
-
|
|
1001
|
-
if (abortSignal.aborted) {
|
|
1002
|
-
break;
|
|
1003
|
-
}
|
|
1004
|
-
|
|
1005
|
-
await emit({
|
|
1006
|
-
type: "audio-delta",
|
|
1007
|
-
value,
|
|
1008
|
-
});
|
|
1009
|
-
}
|
|
1010
|
-
} finally {
|
|
1011
|
-
abortSignal.removeEventListener("abort", cancelAudioStream);
|
|
1012
|
-
reader.releaseLock();
|
|
1013
|
-
}
|
|
1014
|
-
|
|
1015
|
-
await emit({ type: "audio-done" });
|
|
1016
|
-
await emit({ type: "finish" });
|
|
1017
|
-
return null;
|
|
1018
|
-
} catch (error) {
|
|
1019
|
-
if (abortSignal.aborted || isAbortError(error)) {
|
|
1020
|
-
logger.info("Agent speech audio streaming aborted by the client");
|
|
1021
|
-
} else {
|
|
1022
|
-
logger.error(`Agent speech audio streaming failed:\n${error}`);
|
|
1023
|
-
await emit({
|
|
1024
|
-
type: "error",
|
|
1025
|
-
error: getErrorMessage(error),
|
|
1026
|
-
});
|
|
1027
|
-
}
|
|
1028
|
-
await emit({ type: "finish" });
|
|
1029
|
-
return null;
|
|
1030
|
-
}
|
|
1031
|
-
}
|
|
1032
|
-
});
|
|
1033
|
-
server.endpoint({
|
|
1034
|
-
method: 'POST',
|
|
1035
|
-
path: `/agent/get-sessions`,
|
|
1036
|
-
handler: async ({body, adminUser, response }) => {
|
|
1037
|
-
const data = this.parseBody(getSessionsBodySchema, body, response);
|
|
1038
|
-
if (!data) return;
|
|
1039
|
-
const userId = adminUser.pk;
|
|
1040
|
-
const limit = data.limit ?? 20;
|
|
1041
|
-
const sessions = await this.adminforth.resource(this.options.sessionResource.resourceId).list(
|
|
1042
|
-
[Filters.EQ(this.options.sessionResource.askerIdField, userId)], limit, undefined, [Sorts.DESC(this.options.sessionResource.createdAtField)]
|
|
1043
|
-
);
|
|
1044
|
-
return {
|
|
1045
|
-
sessions: sessions.map((session) => ({
|
|
1046
|
-
sessionId: session[this.options.sessionResource.idField],
|
|
1047
|
-
title: session[this.options.sessionResource.titleField],
|
|
1048
|
-
timestamp: session[this.options.sessionResource.createdAtField],
|
|
1049
|
-
})),
|
|
1050
|
-
};
|
|
1051
|
-
}
|
|
1052
|
-
});
|
|
1053
|
-
server.endpoint({
|
|
1054
|
-
method: 'POST',
|
|
1055
|
-
path: `/agent/get-session-info`,
|
|
1056
|
-
handler: async ({body, adminUser, response }) => {
|
|
1057
|
-
const parsedBody = sessionIdBodySchema.safeParse(body);
|
|
1058
|
-
if (!parsedBody.success) {
|
|
1059
|
-
response.setStatus(422, parsedBody.error.message);
|
|
1060
|
-
return;
|
|
1061
|
-
}
|
|
1062
|
-
const userId = adminUser.pk;
|
|
1063
|
-
const sessionId = parsedBody.data.sessionId;
|
|
1064
|
-
const session = await this.adminforth.resource(this.options.sessionResource.resourceId).get(
|
|
1065
|
-
[Filters.EQ(this.options.sessionResource.idField, sessionId)]
|
|
1066
|
-
);
|
|
1067
|
-
if (!session) {
|
|
1068
|
-
return {
|
|
1069
|
-
error: 'Session not found'
|
|
1070
|
-
};
|
|
1071
|
-
}
|
|
1072
|
-
if (session[this.options.sessionResource.askerIdField] !== userId) {
|
|
1073
|
-
return {
|
|
1074
|
-
error: 'Unauthorized'
|
|
1075
|
-
};
|
|
1076
|
-
}
|
|
1077
|
-
const turns = await this.getSessionTurns(sessionId);
|
|
1078
|
-
return {
|
|
1079
|
-
session: {
|
|
1080
|
-
sessionId,
|
|
1081
|
-
title: session[this.options.sessionResource.titleField],
|
|
1082
|
-
timestamp: session[this.options.sessionResource.createdAtField],
|
|
1083
|
-
messages: turns.flatMap(turn => {
|
|
1084
|
-
const messages: Array<{ text: string; role: 'user' | 'assistant' }> = [];
|
|
1085
|
-
if (turn.prompt) {
|
|
1086
|
-
messages.push({
|
|
1087
|
-
text: turn.prompt,
|
|
1088
|
-
role: 'user',
|
|
1089
|
-
});
|
|
1090
|
-
}
|
|
1091
|
-
if (turn.response && turn.response !== "not_finished") {
|
|
1092
|
-
messages.push({
|
|
1093
|
-
text: turn.response,
|
|
1094
|
-
role: 'assistant',
|
|
1095
|
-
});
|
|
1096
|
-
}
|
|
1097
|
-
return messages;
|
|
1098
|
-
}),
|
|
1099
|
-
},
|
|
1100
|
-
};
|
|
1101
|
-
}
|
|
1102
|
-
});
|
|
1103
|
-
server.endpoint({
|
|
1104
|
-
method: 'POST',
|
|
1105
|
-
path: `/agent/create-session`,
|
|
1106
|
-
handler: async ({body, adminUser, response }) => {
|
|
1107
|
-
const data = this.parseBody(createSessionBodySchema, body, response);
|
|
1108
|
-
if (!data) return;
|
|
1109
|
-
const triggerMessage = data.triggerMessage;
|
|
1110
|
-
const userId = adminUser.pk;
|
|
1111
|
-
const title = triggerMessage?.slice(0, 40) || "New Session";
|
|
1112
|
-
const newSession = {
|
|
1113
|
-
[this.options.sessionResource.idField]: randomUUID(),
|
|
1114
|
-
[this.options.sessionResource.titleField]: title,
|
|
1115
|
-
[this.options.sessionResource.askerIdField]: userId,
|
|
1116
|
-
};
|
|
1117
|
-
await this.adminforth.resource(this.options.sessionResource.resourceId).create(newSession);
|
|
1118
|
-
return {
|
|
1119
|
-
sessionId: newSession[this.options.sessionResource.idField],
|
|
1120
|
-
title: newSession[this.options.sessionResource.titleField],
|
|
1121
|
-
timestamp: newSession[this.options.sessionResource.createdAtField],
|
|
1122
|
-
messages: []
|
|
1123
|
-
};
|
|
1124
|
-
}
|
|
1125
|
-
});
|
|
1126
|
-
server.endpoint({
|
|
1127
|
-
method: 'POST',
|
|
1128
|
-
path: `/agent/delete-session`,
|
|
1129
|
-
handler: async ({body, adminUser, response }) => {
|
|
1130
|
-
const data = this.parseBody(sessionIdBodySchema, body, response);
|
|
1131
|
-
if (!data) return;
|
|
1132
|
-
const sessionId = data.sessionId;
|
|
1133
|
-
const userId = adminUser.pk;
|
|
1134
|
-
const session = await this.adminforth.resource(this.options.sessionResource.resourceId).get(
|
|
1135
|
-
[Filters.EQ(this.options.sessionResource.idField, sessionId)]
|
|
1136
|
-
);
|
|
1137
|
-
if (!session) {
|
|
1138
|
-
return {
|
|
1139
|
-
error: 'Session not found'
|
|
1140
|
-
};
|
|
1141
|
-
}
|
|
1142
|
-
if (session[this.options.sessionResource.askerIdField] !== userId) {
|
|
1143
|
-
return {
|
|
1144
|
-
error: 'Unauthorized'
|
|
1145
|
-
};
|
|
1146
|
-
}
|
|
1147
|
-
await this.adminforth.resource(this.options.sessionResource.resourceId).delete(sessionId);
|
|
1148
|
-
const turns = await this.adminforth.resource(this.options.turnResource.resourceId).list(
|
|
1149
|
-
[Filters.EQ(this.options.turnResource.sessionIdField, sessionId)]
|
|
1150
|
-
);
|
|
1151
|
-
for (const turn of turns) {
|
|
1152
|
-
await this.adminforth.resource(this.options.turnResource.resourceId).delete(turn[this.options.turnResource.idField]);
|
|
1153
|
-
}
|
|
1154
|
-
return {
|
|
1155
|
-
ok: true
|
|
1156
|
-
};
|
|
1157
|
-
}
|
|
1158
|
-
}),
|
|
1159
|
-
server.endpoint({
|
|
1160
|
-
method: 'POST',
|
|
1161
|
-
path: `/agent/add-system-message-to-turns`,
|
|
1162
|
-
handler: async ({body, response }) => {
|
|
1163
|
-
const data = this.parseBody(addSystemMessageBodySchema, body, response);
|
|
1164
|
-
if (!data) return;
|
|
1165
|
-
await this.createNewTurn(data.sessionId, data.systemMessage);
|
|
1166
|
-
return {
|
|
1167
|
-
ok: true
|
|
1168
|
-
}
|
|
1169
|
-
}
|
|
1170
|
-
})
|
|
1171
|
-
}
|
|
1172
|
-
}
|
|
1173
|
-
|
|
1174
|
-
function getPartialVegaLiteFenceStartLength(text: string): number {
|
|
1175
|
-
for (let length = Math.min(text.length, VEGA_LITE_FENCE_START.length - 1); length > 0; length -= 1) {
|
|
1176
|
-
if (VEGA_LITE_FENCE_START.startsWith(text.slice(-length))) {
|
|
1177
|
-
return length;
|
|
1178
|
-
}
|
|
158
|
+
const endpointContext = {
|
|
159
|
+
adminforth: this.adminforth,
|
|
160
|
+
options: this.options,
|
|
161
|
+
parseBody: this.parseBody.bind(this),
|
|
162
|
+
handleTurn: this.agentTurnService.handleTurn.bind(this.agentTurnService),
|
|
163
|
+
handleSpeechTurn: this.agentTurnService.handleSpeechTurn.bind(this.agentTurnService),
|
|
164
|
+
runAndPersistAgentResponse: this.agentTurnService.runAndPersistAgentResponse.bind(this.agentTurnService),
|
|
165
|
+
getSessionTurns: this.sessionStore.getSessionTurns.bind(this.sessionStore),
|
|
166
|
+
createNewTurn: this.sessionStore.createNewTurn.bind(this.sessionStore),
|
|
167
|
+
createSystemTurn: this.sessionStore.createSystemTurn.bind(this.sessionStore),
|
|
168
|
+
getChatSurfaceConnectActionAdapters: this.chatSurfaceService.getConnectActionAdapters.bind(this.chatSurfaceService),
|
|
169
|
+
createChatSurfaceLinkToken: this.chatSurfaceService.createLinkToken.bind(this.chatSurfaceService),
|
|
170
|
+
handleChatSurfaceMessage: this.chatSurfaceService.handleMessage.bind(this.chatSurfaceService),
|
|
171
|
+
} satisfies AgentEndpointsContext;
|
|
172
|
+
|
|
173
|
+
setupCoreEndpoints(endpointContext, server);
|
|
174
|
+
setupSessionEndpoints(endpointContext, server);
|
|
175
|
+
setupChatSurfaceEndpoints(endpointContext, server);
|
|
1179
176
|
}
|
|
1180
|
-
|
|
1181
|
-
return 0;
|
|
1182
177
|
}
|