@adminforth/agent 1.37.0 → 1.38.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.
Files changed (55) hide show
  1. package/agent/languageDetect.ts +0 -8
  2. package/agent/simpleAgent.ts +5 -5
  3. package/agent/systemPrompt.ts +35 -4
  4. package/agent/toolCallEvents.ts +31 -2
  5. package/agent/tools/apiTool.ts +1 -1
  6. package/agentResponseEvents.ts +197 -0
  7. package/apiBasedTools.ts +118 -284
  8. package/build.log +12 -2
  9. package/custom/ChatSurface.vue +31 -21
  10. package/custom/composables/agentAudio/agent-processing.mp3 +0 -0
  11. package/custom/composables/agentStore/constants.ts +8 -1
  12. package/custom/composables/agentStore/useAgentSessions.ts +85 -12
  13. package/custom/composables/useAgentAudio.ts +392 -0
  14. package/custom/composables/useAgentStore.ts +52 -5
  15. package/custom/conversation_area/ConversationArea.vue +1 -1
  16. package/custom/conversation_area/MessageRenderer.vue +12 -1
  17. package/custom/conversation_area/SystemMessageRenderer.vue +28 -0
  18. package/custom/conversation_area/TextRenderer.vue +4 -3
  19. package/custom/conversation_area/ToolRenderer.vue +1 -1
  20. package/custom/package.json +2 -1
  21. package/custom/pnpm-lock.yaml +29 -0
  22. package/custom/speech_recognition_frontend/AudioLines.vue +97 -0
  23. package/custom/speech_recognition_frontend/MicrophoneButon.vue +157 -0
  24. package/custom/speech_recognition_frontend/types/voice-activity-detection.d.ts +22 -0
  25. package/custom/speech_recognition_frontend/voiceActivityDetection.ts +151 -0
  26. package/custom/types.ts +52 -2
  27. package/dist/agent/languageDetect.js +0 -6
  28. package/dist/agent/simpleAgent.js +4 -3
  29. package/dist/agent/systemPrompt.js +24 -3
  30. package/dist/agent/toolCallEvents.js +24 -2
  31. package/dist/agent/tools/apiTool.js +1 -1
  32. package/dist/agentResponseEvents.js +141 -0
  33. package/dist/apiBasedTools.js +95 -211
  34. package/dist/custom/ChatSurface.vue +31 -21
  35. package/dist/custom/composables/agentAudio/agent-processing.mp3 +0 -0
  36. package/dist/custom/composables/agentStore/constants.ts +8 -1
  37. package/dist/custom/composables/agentStore/useAgentSessions.ts +85 -12
  38. package/dist/custom/composables/useAgentAudio.ts +392 -0
  39. package/dist/custom/composables/useAgentStore.ts +52 -5
  40. package/dist/custom/conversation_area/ConversationArea.vue +1 -1
  41. package/dist/custom/conversation_area/MessageRenderer.vue +12 -1
  42. package/dist/custom/conversation_area/SystemMessageRenderer.vue +28 -0
  43. package/dist/custom/conversation_area/TextRenderer.vue +4 -3
  44. package/dist/custom/conversation_area/ToolRenderer.vue +1 -1
  45. package/dist/custom/package.json +2 -1
  46. package/dist/custom/pnpm-lock.yaml +29 -0
  47. package/dist/custom/speech_recognition_frontend/AudioLines.vue +97 -0
  48. package/dist/custom/speech_recognition_frontend/MicrophoneButon.vue +157 -0
  49. package/dist/custom/speech_recognition_frontend/types/voice-activity-detection.d.ts +22 -0
  50. package/dist/custom/speech_recognition_frontend/voiceActivityDetection.ts +151 -0
  51. package/dist/custom/types.ts +52 -2
  52. package/dist/index.js +290 -400
  53. package/index.ts +318 -492
  54. package/package.json +3 -2
  55. package/types.ts +1 -1
package/dist/index.js CHANGED
@@ -18,68 +18,44 @@ import { AdminForthPlugin, logger, Filters, Sorts } from "adminforth";
18
18
  import { randomUUID } from 'crypto';
19
19
  import { HumanMessage, SystemMessage } from "langchain";
20
20
  import { MemorySaver } from "@langchain/langgraph";
21
- import { createAgentChatModel, callAgent, } from "./agent/simpleAgent.js";
21
+ import { z } from "zod";
22
+ import { createAgentChatModel, callAgent } from "./agent/simpleAgent.js";
22
23
  import { AdminForthCheckpointSaver } from "./agent/checkpointer.js";
23
24
  import { createSequenceDebugCollector } from "./agent/middleware/sequenceDebug.js";
24
- import { detectUserLanguage, formatLanguagePrompt, } from "./agent/languageDetect.js";
25
- import { prepareApiBasedTools as buildApiBasedTools, } from './apiBasedTools.js';
26
- import { appendCustomSystemPrompt, buildAgentSystemPrompt, DEFAULT_AGENT_SYSTEM_PROMPT, } from "./agent/systemPrompt.js";
27
- import { ALWAYS_AVAILABLE_API_TOOL_NAMES } from "./agent/tools/index.js";
28
- function isAggregateErrorLike(error) {
29
- return typeof error === "object" && error !== null && Array.isArray(error.errors);
30
- }
31
- function formatAgentError(error) {
32
- var _a, _b;
33
- if (isAggregateErrorLike(error)) {
34
- const nestedErrors = error.errors
35
- .map((nestedError, index) => {
36
- var _a;
37
- if (nestedError instanceof Error) {
38
- return `${index + 1}. ${(_a = nestedError.stack) !== null && _a !== void 0 ? _a : nestedError.message}`;
39
- }
40
- return `${index + 1}. ${String(nestedError)}`;
41
- })
42
- .join("\n");
43
- return `${(_a = error.stack) !== null && _a !== void 0 ? _a : error.message}\nNested errors:\n${nestedErrors}`;
44
- }
45
- if (error instanceof Error) {
46
- return (_b = error.stack) !== null && _b !== void 0 ? _b : error.message;
47
- }
48
- return String(error);
49
- }
50
- function formatAgentResponseError(error) {
51
- if (isAggregateErrorLike(error)) {
52
- const nestedErrors = error.errors.map(formatAgentResponseError);
53
- if (nestedErrors.length) {
54
- return nestedErrors.join("\n");
25
+ import { detectUserLanguage } from "./agent/languageDetect.js";
26
+ import { prepareApiBasedTools as buildApiBasedTools } from './apiBasedTools.js';
27
+ import { createAgentEventStream } from "./agentResponseEvents.js";
28
+ import { appendCustomSystemPrompt, buildAgentSystemPrompt, buildAgentTurnSystemPrompt, DEFAULT_AGENT_SYSTEM_PROMPT } from "./agent/systemPrompt.js";
29
+ const agentResponseBodySchema = z.object({
30
+ message: z.string(),
31
+ sessionId: z.string(),
32
+ mode: z.string().nullish(),
33
+ timeZone: z.string().optional(),
34
+ currentPage: z.custom().optional(),
35
+ }).strict();
36
+ const agentSpeechResponseBodySchema = agentResponseBodySchema.omit({ message: true });
37
+ const addSystemMessageBodySchema = z.object({
38
+ sessionId: z.string(),
39
+ systemMessage: z.string(),
40
+ }).strict();
41
+ const getSessionsBodySchema = z.object({
42
+ limit: z.number().optional(),
43
+ }).strict();
44
+ const sessionIdBodySchema = z.object({
45
+ sessionId: z.string(),
46
+ }).strict();
47
+ const createSessionBodySchema = z.object({
48
+ triggerMessage: z.string().optional(),
49
+ }).strict();
50
+ export default class AdminForthAgentPlugin extends AdminForthPlugin {
51
+ parseBody(schema, body, response) {
52
+ const parsed = schema.safeParse(body !== null && body !== void 0 ? body : {});
53
+ if (!parsed.success) {
54
+ response.setStatus(422, parsed.error.message);
55
+ return null;
55
56
  }
56
- return error.message || "Agent response failed";
57
- }
58
- if (error instanceof Error) {
59
- return error.toString();
57
+ return parsed.data;
60
58
  }
61
- return String(error);
62
- }
63
- function formatAdminUserPrompt(adminUser, usernameField) {
64
- const dbUser = adminUser.dbUser;
65
- const adminUserContext = {
66
- id: adminUser.pk,
67
- email: dbUser[usernameField],
68
- };
69
- return [
70
- "Current admin user context:",
71
- JSON.stringify(adminUserContext, null, 2),
72
- "Use this admin user email when the user asks to send information to themselves, the current admin, or the logged-in user.",
73
- ].join("\n");
74
- }
75
- function assertRequiredApiTool(apiBasedTools, toolName) {
76
- if (toolName in apiBasedTools) {
77
- return;
78
- }
79
- const availableToolNames = Object.keys(apiBasedTools).sort().join(", ");
80
- throw new Error(`Required API tool "${toolName}" is missing from AdminForth Agent tools. Available tools: ${availableToolNames}`);
81
- }
82
- export default class AdminForthAgentPlugin extends AdminForthPlugin {
83
59
  createNewTurn(sessionId, prompt, response) {
84
60
  return __awaiter(this, void 0, void 0, function* () {
85
61
  const turnId = randomUUID();
@@ -93,20 +69,6 @@ export default class AdminForthAgentPlugin extends AdminForthPlugin {
93
69
  return newTurn.createdRecord[this.options.turnResource.idField];
94
70
  });
95
71
  }
96
- updateTurn(turnId, updates) {
97
- return __awaiter(this, void 0, void 0, function* () {
98
- yield this.adminforth.resource(this.options.turnResource.resourceId).update(turnId, updates);
99
- return { ok: true };
100
- });
101
- }
102
- updateSessionDate(sessionId) {
103
- return __awaiter(this, void 0, void 0, function* () {
104
- yield this.adminforth.resource(this.options.sessionResource.resourceId).update(sessionId, {
105
- [this.options.sessionResource.createdAtField]: new Date().toISOString(),
106
- });
107
- return { ok: true };
108
- });
109
- }
110
72
  getSessionTurns(sessionId) {
111
73
  return __awaiter(this, void 0, void 0, function* () {
112
74
  const turns = yield this.adminforth.resource(this.options.turnResource.resourceId).list([Filters.EQ(this.options.turnResource.sessionIdField, sessionId)], undefined, undefined, [Sorts.ASC(this.options.turnResource.createdAtField)]);
@@ -116,42 +78,9 @@ export default class AdminForthAgentPlugin extends AdminForthPlugin {
116
78
  }));
117
79
  });
118
80
  }
119
- getModeModels(mode, maxTokens) {
120
- return __awaiter(this, void 0, void 0, function* () {
121
- const cachedModels = this.modelsByModeName.get(mode.name);
122
- if (cachedModels) {
123
- return yield cachedModels;
124
- }
125
- const modelsPromise = Promise.all([
126
- createAgentChatModel({
127
- adapter: mode.completionAdapter,
128
- maxTokens,
129
- purpose: "primary",
130
- }),
131
- createAgentChatModel({
132
- adapter: mode.completionAdapter,
133
- maxTokens,
134
- purpose: "summary",
135
- }),
136
- ]).then(([primaryModel, summaryModel]) => ({
137
- model: primaryModel.model,
138
- summaryModel: summaryModel.model,
139
- modelMiddleware: primaryModel.middleware,
140
- }));
141
- this.modelsByModeName.set(mode.name, modelsPromise);
142
- try {
143
- return yield modelsPromise;
144
- }
145
- catch (error) {
146
- this.modelsByModeName.delete(mode.name);
147
- throw error;
148
- }
149
- });
150
- }
151
81
  getCheckpointer() {
152
- if (this.checkpointer) {
82
+ if (this.checkpointer)
153
83
  return this.checkpointer;
154
- }
155
84
  this.checkpointer = this.options.checkpointResource
156
85
  ? new AdminForthCheckpointSaver(this.adminforth, this.options)
157
86
  : new MemorySaver();
@@ -167,9 +96,7 @@ export default class AdminForthAgentPlugin extends AdminForthPlugin {
167
96
  }
168
97
  constructor(options) {
169
98
  super(options, import.meta.url);
170
- this.apiBasedTools = {};
171
99
  this.checkpointer = null;
172
- this.modelsByModeName = new Map();
173
100
  this.options = options;
174
101
  this.agentSystemPromptPromise = Promise.resolve(appendCustomSystemPrompt(DEFAULT_AGENT_SYSTEM_PROMPT, this.options.systemPrompt));
175
102
  this.shouldHaveSingleInstancePerWholeApp = () => false;
@@ -197,7 +124,7 @@ export default class AdminForthAgentPlugin extends AdminForthPlugin {
197
124
  hasAudioAdapter: Boolean(this.options.audioAdapter),
198
125
  }
199
126
  });
200
- if (!this.pluginOptions.sessionResource) {
127
+ if (!this.options.sessionResource) {
201
128
  throw new Error("sessionResource is required for AdminForthAgentPlugin");
202
129
  }
203
130
  });
@@ -216,25 +143,35 @@ export default class AdminForthAgentPlugin extends AdminForthPlugin {
216
143
  var _a, e_1, _b, _c;
217
144
  var _d, _e, _f, _g;
218
145
  let fullResponse = "";
219
- const maxTokens = (_d = this.options.maxTokens) !== null && _d !== void 0 ? _d : 10000;
146
+ const maxTokens = (_d = this.options.maxTokens) !== null && _d !== void 0 ? _d : 1000;
220
147
  const selectedMode = (_e = this.options.modes.find((mode) => mode.name === input.modeName)) !== null && _e !== void 0 ? _e : this.options.modes[0];
221
- const { model, summaryModel, modelMiddleware } = yield this.getModeModels(selectedMode, maxTokens);
148
+ const [primaryModelSpec, summaryModelSpec] = yield Promise.all([
149
+ createAgentChatModel({
150
+ adapter: selectedMode.completionAdapter,
151
+ maxTokens,
152
+ purpose: "primary",
153
+ }),
154
+ createAgentChatModel({
155
+ adapter: selectedMode.completionAdapter,
156
+ maxTokens,
157
+ purpose: "summary",
158
+ }),
159
+ ]);
160
+ const model = primaryModelSpec.model;
161
+ const summaryModel = summaryModelSpec.model;
162
+ const modelMiddleware = primaryModelSpec.middleware;
222
163
  const userLanguage = yield detectUserLanguage(selectedMode.completionAdapter, input.prompt)
223
164
  .catch((error) => {
224
- logger.warn(`Failed to detect user language: ${error instanceof Error ? error.message : String(error)}`);
165
+ logger.warn(`Failed to detect user language: ${error.message}`);
225
166
  return null;
226
167
  });
227
- const systemPrompt = [
228
- yield this.agentSystemPromptPromise,
229
- formatAdminUserPrompt(input.adminUser, this.adminforth.config.auth.usernameField),
230
- formatLanguagePrompt(userLanguage),
231
- ].join("\n\n");
168
+ const systemPrompt = buildAgentTurnSystemPrompt({
169
+ agentSystemPrompt: yield this.agentSystemPromptPromise,
170
+ adminUser: input.adminUser,
171
+ usernameField: this.adminforth.config.auth.usernameField,
172
+ userLanguage,
173
+ });
232
174
  const apiBasedTools = buildApiBasedTools(this.adminforth, this.getInternalAgentResourceIds());
233
- for (const toolName of ALWAYS_AVAILABLE_API_TOOL_NAMES) {
234
- assertRequiredApiTool(apiBasedTools, toolName);
235
- }
236
- assertRequiredApiTool(apiBasedTools, "update_record");
237
- this.apiBasedTools = apiBasedTools;
238
175
  const stream = yield callAgent({
239
176
  name: `adminforth-agent-${this.pluginInstanceId}`,
240
177
  model,
@@ -252,8 +189,8 @@ export default class AdminForthAgentPlugin extends AdminForthPlugin {
252
189
  sessionId: input.sessionId,
253
190
  turnId: input.turnId,
254
191
  currentPage: input.currentPage,
255
- httpExtra: input.httpExtra,
256
192
  userTimeZone: input.userTimeZone,
193
+ abortSignal: input.abortSignal,
257
194
  emitToolCallEvent: (event) => {
258
195
  var _a;
259
196
  input.sequenceDebugCollector.handleToolCallEvent(event);
@@ -307,11 +244,67 @@ export default class AdminForthAgentPlugin extends AdminForthPlugin {
307
244
  };
308
245
  });
309
246
  }
247
+ runAndPersistAgentResponse(input) {
248
+ return __awaiter(this, void 0, void 0, function* () {
249
+ var _a, _b;
250
+ const turnId = yield this.createNewTurn(input.sessionId, input.prompt);
251
+ yield this.adminforth.resource(this.options.sessionResource.resourceId).update(input.sessionId, {
252
+ [this.options.sessionResource.createdAtField]: new Date().toISOString(),
253
+ });
254
+ const sequenceDebugCollector = createSequenceDebugCollector();
255
+ let fullResponse = "";
256
+ let aborted = false;
257
+ let failed = false;
258
+ try {
259
+ const agentResponse = yield this.runAgentTurn({
260
+ prompt: input.prompt,
261
+ sessionId: input.sessionId,
262
+ turnId,
263
+ modeName: input.modeName,
264
+ userTimeZone: input.userTimeZone,
265
+ currentPage: input.currentPage,
266
+ abortSignal: input.abortSignal,
267
+ adminUser: input.adminUser,
268
+ sequenceDebugCollector,
269
+ emitToolCallEvent: input.emitToolCallEvent,
270
+ emitReasoningDelta: input.emitReasoningDelta,
271
+ emitTextDelta: input.emitTextDelta,
272
+ });
273
+ fullResponse = agentResponse.text;
274
+ }
275
+ catch (error) {
276
+ if ((_a = input.abortSignal) === null || _a === void 0 ? void 0 : _a.aborted) {
277
+ aborted = true;
278
+ logger.info(input.abortLogMessage);
279
+ }
280
+ else {
281
+ failed = true;
282
+ logger.error(`${input.failureLogMessage}:\n${error.message}`);
283
+ fullResponse = error.message;
284
+ (_b = input.emitErrorResponse) === null || _b === void 0 ? void 0 : _b.call(input, fullResponse);
285
+ }
286
+ }
287
+ sequenceDebugCollector.flush();
288
+ const turnUpdates = {
289
+ [this.options.turnResource.responseField]: fullResponse,
290
+ };
291
+ if (this.options.turnResource.debugField) {
292
+ turnUpdates[this.options.turnResource.debugField] = sequenceDebugCollector.getHistory();
293
+ }
294
+ yield this.adminforth.resource(this.options.turnResource.resourceId).update(turnId, turnUpdates);
295
+ return {
296
+ text: fullResponse,
297
+ turnId,
298
+ aborted,
299
+ failed,
300
+ };
301
+ });
302
+ }
310
303
  setupEndpoints(server) {
311
304
  server.endpoint({
312
305
  method: 'POST',
313
306
  path: `/agent/get-placeholder-messages`,
314
- handler: (_a) => __awaiter(this, [_a], void 0, function* ({ body, query, headers, cookies, adminUser, response, requestUrl }) {
307
+ handler: (_a) => __awaiter(this, [_a], void 0, function* ({ headers, adminUser }) {
315
308
  if (!this.options.placeholderMessages) {
316
309
  return {
317
310
  messages: [],
@@ -319,14 +312,7 @@ export default class AdminForthAgentPlugin extends AdminForthPlugin {
319
312
  }
320
313
  const messages = yield this.options.placeholderMessages({
321
314
  adminUser,
322
- httpExtra: {
323
- body,
324
- query,
325
- headers,
326
- cookies,
327
- requestUrl,
328
- response,
329
- },
315
+ headers,
330
316
  });
331
317
  return {
332
318
  messages,
@@ -336,330 +322,230 @@ export default class AdminForthAgentPlugin extends AdminForthPlugin {
336
322
  server.endpoint({
337
323
  method: 'POST',
338
324
  path: `/agent/response`,
339
- handler: (_a) => __awaiter(this, [_a], void 0, function* ({ body, query, headers, cookies, adminUser, response, requestUrl, _raw_express_res, abortSignal }) {
325
+ handler: (_a) => __awaiter(this, [_a], void 0, function* ({ body, adminUser, response, _raw_express_res, abortSignal }) {
340
326
  var _b;
341
- const res = _raw_express_res;
327
+ const data = this.parseBody(agentResponseBodySchema, body, response);
328
+ if (!data)
329
+ return;
330
+ const stream = createAgentEventStream(_raw_express_res, { vercelAiUiMessageStream: true, closeActiveBlockOnToolStart: true });
342
331
  const messageId = randomUUID();
343
- const prompt = body.message;
344
- const userTimeZone = (_b = body.timeZone) !== null && _b !== void 0 ? _b : 'UTC';
345
- const currentPage = body.currentPage;
346
- const sessionId = body.sessionId || (adminUser === null || adminUser === void 0 ? void 0 : adminUser.pk) || (adminUser === null || adminUser === void 0 ? void 0 : adminUser.username) || 'default';
347
- const turnId = yield this.createNewTurn(sessionId, prompt);
348
- yield this.updateSessionDate(sessionId);
349
- let fullResponse = "";
350
- let isStreamClosed = false;
351
- const sequenceDebugCollector = createSequenceDebugCollector();
352
- res.writeHead(200, {
353
- 'Content-Type': 'text/event-stream',
354
- 'Cache-Control': 'no-cache',
355
- 'Connection': 'keep-alive',
356
- 'x-vercel-ai-ui-message-stream': 'v1',
332
+ stream.start(messageId);
333
+ yield this.runAndPersistAgentResponse({
334
+ prompt: data.message,
335
+ sessionId: data.sessionId,
336
+ modeName: data.mode,
337
+ userTimeZone: (_b = data.timeZone) !== null && _b !== void 0 ? _b : 'UTC',
338
+ currentPage: data.currentPage,
339
+ abortSignal,
340
+ adminUser,
341
+ emitToolCallEvent: stream.toolCall,
342
+ emitReasoningDelta: stream.reasoningDelta,
343
+ emitTextDelta: stream.textDelta,
344
+ emitErrorResponse: stream.textDelta,
345
+ failureLogMessage: "Agent response streaming failed",
346
+ abortLogMessage: "Agent response streaming aborted by the client",
357
347
  });
358
- const send = (obj) => {
359
- if (isStreamClosed || res.writableEnded || res.destroyed) {
360
- return;
361
- }
362
- res.write(`data: ${JSON.stringify(obj)}\n\n`);
363
- };
364
- const emitToolCallEvent = (event) => {
365
- if (event.phase === "start") {
366
- endActiveBlock();
367
- }
368
- send({
369
- type: "data-tool-call",
370
- data: event,
371
- });
372
- };
373
- let activeBlock = null;
374
- const endActiveBlock = () => {
375
- if (!activeBlock) {
376
- return;
377
- }
378
- send({
379
- type: `${activeBlock.type}-end`,
380
- id: activeBlock.id,
381
- });
382
- activeBlock = null;
383
- };
384
- const startBlock = (type) => {
385
- if ((activeBlock === null || activeBlock === void 0 ? void 0 : activeBlock.type) === type) {
386
- return activeBlock.id;
387
- }
388
- endActiveBlock();
389
- const id = randomUUID();
390
- activeBlock = { type, id };
391
- send({
392
- type: `${type}-start`,
393
- id,
394
- });
395
- return id;
396
- };
397
- const endStream = () => {
398
- if (isStreamClosed || res.writableEnded || res.destroyed) {
399
- return;
400
- }
401
- endActiveBlock();
402
- send({
403
- type: 'finish',
404
- });
405
- res.write(`data: [DONE]\n\n`);
406
- isStreamClosed = true;
407
- res.end();
408
- };
409
- try {
410
- send({
411
- type: 'start',
412
- messageId,
413
- });
414
- const agentResponse = yield this.runAgentTurn({
415
- prompt,
416
- sessionId,
417
- turnId,
418
- modeName: body.mode,
419
- userTimeZone,
420
- currentPage,
421
- adminUser,
422
- httpExtra: {
423
- body,
424
- query,
425
- headers,
426
- cookies,
427
- requestUrl,
428
- response,
429
- },
430
- sequenceDebugCollector,
431
- emitToolCallEvent,
432
- emitReasoningDelta: (reasoningDelta) => {
433
- const reasoningId = startBlock('reasoning');
434
- send({
435
- type: 'reasoning-delta',
436
- id: reasoningId,
437
- delta: reasoningDelta,
438
- });
439
- },
440
- emitTextDelta: (textDelta) => {
441
- const textId = startBlock('text');
442
- fullResponse += textDelta;
443
- send({
444
- type: 'text-delta',
445
- id: textId,
446
- delta: textDelta,
447
- });
448
- },
449
- });
450
- fullResponse = agentResponse.text;
451
- }
452
- catch (error) {
453
- logger.error(`Agent response streaming failed:\n${formatAgentError(error)}`);
454
- sequenceDebugCollector.flush();
455
- fullResponse = formatAgentResponseError(error);
456
- const textId = startBlock('text');
457
- send({
458
- type: 'text-delta',
459
- id: textId,
460
- delta: fullResponse,
461
- });
462
- }
463
- sequenceDebugCollector.flush();
464
- const turnUpdates = {
465
- [this.options.turnResource.responseField]: fullResponse,
466
- };
467
- if (this.options.turnResource.debugField) {
468
- turnUpdates[this.options.turnResource.debugField] = sequenceDebugCollector.getHistory();
469
- }
470
- yield this.updateTurn(turnId, turnUpdates);
471
- endStream();
348
+ stream.end();
472
349
  return null;
473
350
  })
474
351
  });
475
352
  server.endpoint({
476
353
  method: 'POST',
477
354
  path: `/agent/speech-response`,
478
- handler: (_a) => __awaiter(this, [_a], void 0, function* ({ body, query, headers, cookies, adminUser, response, requestUrl }) {
355
+ target: 'upload',
356
+ handler: (_a) => __awaiter(this, [_a], void 0, function* ({ body, adminUser, response, _raw_express_req, _raw_express_res, abortSignal }) {
479
357
  var _b;
358
+ const req = _raw_express_req;
480
359
  const audioAdapter = this.options.audioAdapter;
481
360
  if (!audioAdapter) {
482
361
  response.setStatus(400, undefined);
483
- return {
484
- error: "Audio adapter is not configured for AdminForth Agent",
485
- };
362
+ return { error: "Audio adapter is not configured for AdminForth Agent" };
486
363
  }
487
- const speechBody = body;
364
+ const data = this.parseBody(agentSpeechResponseBodySchema, body, response);
365
+ if (!data)
366
+ return;
367
+ if (!req.file) {
368
+ response.setStatus(400, undefined);
369
+ return { error: "Audio file is required" };
370
+ }
371
+ const stream = createAgentEventStream(_raw_express_res);
488
372
  let transcription;
489
373
  try {
490
374
  transcription = yield audioAdapter.transcribe({
491
- buffer: Buffer.from(speechBody.audioBase64, "base64"),
492
- filename: speechBody.filename,
493
- mimeType: speechBody.mimeType,
375
+ buffer: req.file.buffer,
376
+ filename: req.file.originalname,
377
+ mimeType: req.file.mimetype,
494
378
  language: "auto",
495
- prompt: speechBody.prompt,
496
379
  });
497
380
  }
498
381
  catch (error) {
499
- logger.error(`Agent speech transcription failed:\n${formatAgentError(error)}`);
500
- response.setStatus(500, undefined);
501
- return {
502
- error: "Speech transcription failed. Check server logs for details.",
503
- };
382
+ logger.error(`Agent speech transcription failed:\n${error.message}`);
383
+ stream.error("Speech transcription failed. Check server logs for details.");
384
+ stream.end();
385
+ return null;
504
386
  }
505
387
  const prompt = transcription.text;
506
388
  if (!prompt) {
507
- response.setStatus(400, undefined);
508
- return {
509
- error: "Speech transcription is empty",
510
- };
389
+ stream.error("Speech transcription is empty");
390
+ stream.end();
391
+ return null;
392
+ }
393
+ stream.transcript(transcription.text, transcription.language);
394
+ const sessionId = data.sessionId;
395
+ const currentPage = data.currentPage;
396
+ const agentResponse = yield this.runAndPersistAgentResponse({
397
+ prompt,
398
+ sessionId,
399
+ modeName: data.mode,
400
+ userTimeZone: (_b = data.timeZone) !== null && _b !== void 0 ? _b : 'UTC',
401
+ currentPage,
402
+ abortSignal,
403
+ adminUser,
404
+ emitToolCallEvent: stream.toolCall,
405
+ failureLogMessage: "Agent speech response failed",
406
+ abortLogMessage: "Agent speech response aborted by the client",
407
+ });
408
+ if (agentResponse.aborted) {
409
+ stream.end();
410
+ return null;
411
+ }
412
+ if (agentResponse.failed) {
413
+ stream.error(agentResponse.text);
414
+ stream.end();
415
+ return null;
511
416
  }
512
- const sessionId = speechBody.sessionId || (adminUser === null || adminUser === void 0 ? void 0 : adminUser.pk) || (adminUser === null || adminUser === void 0 ? void 0 : adminUser.username) || 'default';
513
- const turnId = yield this.createNewTurn(sessionId, prompt);
514
- yield this.updateSessionDate(sessionId);
515
- const sequenceDebugCollector = createSequenceDebugCollector();
516
- let fullResponse = "";
517
417
  try {
518
- const agentResponse = yield this.runAgentTurn({
519
- prompt,
520
- sessionId,
521
- turnId,
522
- modeName: speechBody.mode,
523
- userTimeZone: (_b = speechBody.timeZone) !== null && _b !== void 0 ? _b : 'UTC',
524
- currentPage: speechBody.currentPage,
525
- adminUser,
526
- httpExtra: {
527
- body,
528
- query,
529
- headers,
530
- cookies,
531
- requestUrl,
532
- response,
533
- },
534
- sequenceDebugCollector,
535
- emitTextDelta: (textDelta) => {
536
- fullResponse += textDelta;
537
- },
418
+ stream.speechResponse({
419
+ text: transcription.text,
420
+ language: transcription.language,
421
+ }, {
422
+ text: agentResponse.text,
423
+ }, sessionId, agentResponse.turnId);
424
+ const speech = yield audioAdapter.synthesize({
425
+ text: agentResponse.text,
426
+ stream: true,
427
+ streamFormat: "audio",
428
+ format: "mp3",
538
429
  });
539
- fullResponse = agentResponse.text;
540
- const speech = yield audioAdapter.synthesize(Object.assign({ text: fullResponse }, speechBody.tts));
541
- sequenceDebugCollector.flush();
542
- const turnUpdates = {
543
- [this.options.turnResource.responseField]: fullResponse,
544
- };
545
- if (this.options.turnResource.debugField) {
546
- turnUpdates[this.options.turnResource.debugField] = sequenceDebugCollector.getHistory();
430
+ stream.audioStart(speech.mimeType, speech.format);
431
+ const reader = speech.audioStream.getReader();
432
+ try {
433
+ while (true) {
434
+ const { value, done } = yield reader.read();
435
+ if (done) {
436
+ break;
437
+ }
438
+ stream.audioDelta(value);
439
+ }
547
440
  }
548
- yield this.updateTurn(turnId, turnUpdates);
549
- return {
550
- transcript: {
551
- text: transcription.text,
552
- language: transcription.language,
553
- },
554
- response: {
555
- text: fullResponse,
556
- },
557
- audio: {
558
- base64: speech.audio.toString("base64"),
559
- mimeType: speech.mimeType,
560
- format: speech.format,
561
- },
562
- sessionId,
563
- turnId,
564
- };
441
+ finally {
442
+ reader.releaseLock();
443
+ }
444
+ stream.audioDone();
445
+ stream.end();
446
+ return null;
565
447
  }
566
448
  catch (error) {
567
- logger.error(`Agent speech response failed:\n${formatAgentError(error)}`);
568
- sequenceDebugCollector.flush();
569
- fullResponse = formatAgentResponseError(error);
570
- const turnUpdates = {
571
- [this.options.turnResource.responseField]: fullResponse,
572
- };
573
- if (this.options.turnResource.debugField) {
574
- turnUpdates[this.options.turnResource.debugField] = sequenceDebugCollector.getHistory();
449
+ if (abortSignal.aborted) {
450
+ logger.info("Agent speech audio streaming aborted by the client");
575
451
  }
576
- yield this.updateTurn(turnId, turnUpdates);
577
- response.setStatus(500, undefined);
578
- return {
579
- error: fullResponse,
580
- };
452
+ else {
453
+ logger.error(`Agent speech audio streaming failed:\n${error}`);
454
+ stream.error(error);
455
+ }
456
+ stream.end();
457
+ return null;
581
458
  }
582
459
  })
583
460
  });
584
461
  server.endpoint({
585
462
  method: 'POST',
586
463
  path: `/agent/get-sessions`,
587
- handler: (_a) => __awaiter(this, [_a], void 0, function* ({ body, adminUser }) {
464
+ handler: (_a) => __awaiter(this, [_a], void 0, function* ({ body, adminUser, response }) {
465
+ var _b;
466
+ const data = this.parseBody(getSessionsBodySchema, body, response);
467
+ if (!data)
468
+ return;
588
469
  const userId = adminUser.pk;
589
- const limit = typeof body.limit === 'number' ? body.limit : 20;
590
- const sessions = yield this.adminforth.resource(this.pluginOptions.sessionResource.resourceId).list([Filters.EQ(this.pluginOptions.sessionResource.askerIdField, userId)], limit, undefined, [Sorts.DESC(this.pluginOptions.sessionResource.createdAtField)]);
591
- const sessionsToReturn = [];
592
- for (const session of sessions) {
593
- sessionsToReturn.push({
594
- sessionId: session[this.pluginOptions.sessionResource.idField],
595
- title: session[this.pluginOptions.sessionResource.titleField],
596
- timestamp: session[this.pluginOptions.sessionResource.createdAtField],
597
- });
598
- }
470
+ const limit = (_b = data.limit) !== null && _b !== void 0 ? _b : 20;
471
+ const sessions = yield this.adminforth.resource(this.options.sessionResource.resourceId).list([Filters.EQ(this.options.sessionResource.askerIdField, userId)], limit, undefined, [Sorts.DESC(this.options.sessionResource.createdAtField)]);
599
472
  return {
600
- sessions: sessionsToReturn
473
+ sessions: sessions.map((session) => ({
474
+ sessionId: session[this.options.sessionResource.idField],
475
+ title: session[this.options.sessionResource.titleField],
476
+ timestamp: session[this.options.sessionResource.createdAtField],
477
+ })),
601
478
  };
602
479
  })
603
480
  });
604
481
  server.endpoint({
605
482
  method: 'POST',
606
483
  path: `/agent/get-session-info`,
607
- handler: (_a) => __awaiter(this, [_a], void 0, function* ({ body, adminUser }) {
484
+ handler: (_a) => __awaiter(this, [_a], void 0, function* ({ body, adminUser, response }) {
485
+ const parsedBody = sessionIdBodySchema.safeParse(body);
486
+ if (!parsedBody.success) {
487
+ response.setStatus(422, parsedBody.error.message);
488
+ return;
489
+ }
608
490
  const userId = adminUser.pk;
609
- const sessionId = body.sessionId;
610
- const session = yield this.adminforth.resource(this.pluginOptions.sessionResource.resourceId).get([Filters.EQ(this.pluginOptions.sessionResource.idField, sessionId)]);
491
+ const sessionId = parsedBody.data.sessionId;
492
+ const session = yield this.adminforth.resource(this.options.sessionResource.resourceId).get([Filters.EQ(this.options.sessionResource.idField, sessionId)]);
611
493
  if (!session) {
612
494
  return {
613
495
  error: 'Session not found'
614
496
  };
615
497
  }
616
- if (session[this.pluginOptions.sessionResource.askerIdField] !== userId) {
498
+ if (session[this.options.sessionResource.askerIdField] !== userId) {
617
499
  return {
618
500
  error: 'Unauthorized'
619
501
  };
620
502
  }
621
503
  const turns = yield this.getSessionTurns(sessionId);
622
- const messagesToReturn = [];
623
- for (const turn of turns) {
624
- messagesToReturn.push({
625
- text: turn.prompt,
626
- role: 'user',
627
- });
628
- if (turn.response !== "not_finished") {
629
- messagesToReturn.push({
630
- text: turn.response,
631
- role: 'assistant',
632
- });
633
- }
634
- }
635
- const sessionToReturn = {
636
- sessionId: session[this.pluginOptions.sessionResource.idField],
637
- title: session[this.pluginOptions.sessionResource.titleField],
638
- timestamp: session[this.pluginOptions.sessionResource.createdAtField],
639
- messages: messagesToReturn
640
- };
641
504
  return {
642
- session: sessionToReturn
505
+ session: {
506
+ sessionId,
507
+ title: session[this.options.sessionResource.titleField],
508
+ timestamp: session[this.options.sessionResource.createdAtField],
509
+ messages: turns.flatMap(turn => {
510
+ const messages = [];
511
+ if (turn.prompt) {
512
+ messages.push({
513
+ text: turn.prompt,
514
+ role: 'user',
515
+ });
516
+ }
517
+ if (turn.response && turn.response !== "not_finished") {
518
+ messages.push({
519
+ text: turn.response,
520
+ role: 'assistant',
521
+ });
522
+ }
523
+ return messages;
524
+ }),
525
+ },
643
526
  };
644
527
  })
645
528
  });
646
529
  server.endpoint({
647
530
  method: 'POST',
648
531
  path: `/agent/create-session`,
649
- handler: (_a) => __awaiter(this, [_a], void 0, function* ({ body, adminUser }) {
650
- const triggerMessage = body.triggerMessage;
532
+ handler: (_a) => __awaiter(this, [_a], void 0, function* ({ body, adminUser, response }) {
533
+ const data = this.parseBody(createSessionBodySchema, body, response);
534
+ if (!data)
535
+ return;
536
+ const triggerMessage = data.triggerMessage;
651
537
  const userId = adminUser.pk;
652
- const title = triggerMessage ? (triggerMessage.length > 40 ? triggerMessage.slice(0, 40) : triggerMessage) : 'New Session';
538
+ const title = (triggerMessage === null || triggerMessage === void 0 ? void 0 : triggerMessage.slice(0, 40)) || "New Session";
653
539
  const newSession = {
654
- [this.pluginOptions.sessionResource.idField]: randomUUID(),
655
- [this.pluginOptions.sessionResource.titleField]: title,
656
- [this.pluginOptions.sessionResource.askerIdField]: userId,
540
+ [this.options.sessionResource.idField]: randomUUID(),
541
+ [this.options.sessionResource.titleField]: title,
542
+ [this.options.sessionResource.askerIdField]: userId,
657
543
  };
658
- yield this.adminforth.resource(this.pluginOptions.sessionResource.resourceId).create(newSession);
544
+ yield this.adminforth.resource(this.options.sessionResource.resourceId).create(newSession);
659
545
  return {
660
- sessionId: newSession[this.pluginOptions.sessionResource.idField],
661
- title: newSession[this.pluginOptions.sessionResource.titleField],
662
- timestamp: newSession[this.pluginOptions.sessionResource.createdAtField],
546
+ sessionId: newSession[this.options.sessionResource.idField],
547
+ title: newSession[this.options.sessionResource.titleField],
548
+ timestamp: newSession[this.options.sessionResource.createdAtField],
663
549
  messages: []
664
550
  };
665
551
  })
@@ -667,24 +553,27 @@ export default class AdminForthAgentPlugin extends AdminForthPlugin {
667
553
  server.endpoint({
668
554
  method: 'POST',
669
555
  path: `/agent/delete-session`,
670
- handler: (_a) => __awaiter(this, [_a], void 0, function* ({ body, adminUser }) {
671
- const sessionId = body.sessionId;
556
+ handler: (_a) => __awaiter(this, [_a], void 0, function* ({ body, adminUser, response }) {
557
+ const data = this.parseBody(sessionIdBodySchema, body, response);
558
+ if (!data)
559
+ return;
560
+ const sessionId = data.sessionId;
672
561
  const userId = adminUser.pk;
673
- const session = yield this.adminforth.resource(this.pluginOptions.sessionResource.resourceId).get([Filters.EQ(this.pluginOptions.sessionResource.idField, sessionId)]);
562
+ const session = yield this.adminforth.resource(this.options.sessionResource.resourceId).get([Filters.EQ(this.options.sessionResource.idField, sessionId)]);
674
563
  if (!session) {
675
564
  return {
676
565
  error: 'Session not found'
677
566
  };
678
567
  }
679
- if (session[this.pluginOptions.sessionResource.askerIdField] !== userId) {
568
+ if (session[this.options.sessionResource.askerIdField] !== userId) {
680
569
  return {
681
570
  error: 'Unauthorized'
682
571
  };
683
572
  }
684
- yield this.adminforth.resource(this.pluginOptions.sessionResource.resourceId).delete(sessionId);
685
- const turns = yield this.adminforth.resource(this.pluginOptions.turnResource.resourceId).list([Filters.EQ(this.pluginOptions.turnResource.sessionIdField, sessionId)]);
573
+ yield this.adminforth.resource(this.options.sessionResource.resourceId).delete(sessionId);
574
+ const turns = yield this.adminforth.resource(this.options.turnResource.resourceId).list([Filters.EQ(this.options.turnResource.sessionIdField, sessionId)]);
686
575
  for (const turn of turns) {
687
- yield this.adminforth.resource(this.pluginOptions.turnResource.resourceId).delete(turn[this.pluginOptions.turnResource.idField]);
576
+ yield this.adminforth.resource(this.options.turnResource.resourceId).delete(turn[this.options.turnResource.idField]);
688
577
  }
689
578
  return {
690
579
  ok: true
@@ -694,10 +583,11 @@ export default class AdminForthAgentPlugin extends AdminForthPlugin {
694
583
  server.endpoint({
695
584
  method: 'POST',
696
585
  path: `/agent/add-system-message-to-turns`,
697
- handler: (_a) => __awaiter(this, [_a], void 0, function* ({ body, adminUser, _raw_express_req }) {
698
- const sessionId = body.sessionId;
699
- const systemMessage = body.systemMessage;
700
- yield this.createNewTurn(sessionId, systemMessage);
586
+ handler: (_a) => __awaiter(this, [_a], void 0, function* ({ body, response }) {
587
+ const data = this.parseBody(addSystemMessageBodySchema, body, response);
588
+ if (!data)
589
+ return;
590
+ yield this.createNewTurn(data.sessionId, data.systemMessage);
701
591
  return {
702
592
  ok: true
703
593
  };