@adminforth/agent 1.44.2 → 1.45.1

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