@adminforth/agent 1.34.2 → 1.35.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.
@@ -19,12 +19,14 @@ import {
19
19
  } from "./middleware/sequenceDebug.js";
20
20
  import type { ApiBasedTool } from "../apiBasedTools.js";
21
21
  import type { ToolCallEventSink } from "./toolCallEvents.js";
22
+ import type { CurrentPageContext } from "./tools/getUserLocation.js";
22
23
 
23
24
  export const contextSchema = z.object({
24
25
  adminUser: z.custom<AdminUser>(),
25
26
  userTimeZone: z.string(),
26
27
  sessionId: z.string(),
27
28
  turnId: z.string(),
29
+ currentPage: z.custom<CurrentPageContext>().optional(),
28
30
  httpExtra: z.custom<Partial<HttpExtra>>().optional(),
29
31
  emitToolCallEvent: z.custom<ToolCallEventSink>(),
30
32
  });
@@ -231,6 +233,7 @@ export async function callAgent(params: {
231
233
  customComponentsDir: string;
232
234
  sessionId: string;
233
235
  turnId: string;
236
+ currentPage?: CurrentPageContext;
234
237
  httpExtra?: Partial<HttpExtra>;
235
238
  userTimeZone: string;
236
239
  emitToolCallEvent: ToolCallEventSink;
@@ -249,6 +252,7 @@ export async function callAgent(params: {
249
252
  customComponentsDir,
250
253
  sessionId,
251
254
  turnId,
255
+ currentPage,
252
256
  httpExtra,
253
257
  userTimeZone,
254
258
  emitToolCallEvent,
@@ -293,6 +297,7 @@ export async function callAgent(params: {
293
297
  userTimeZone,
294
298
  sessionId,
295
299
  turnId,
300
+ currentPage,
296
301
  httpExtra,
297
302
  emitToolCallEvent,
298
303
  },
@@ -0,0 +1,45 @@
1
+ import { tool } from "langchain";
2
+ import { z } from "zod";
3
+
4
+ export type CurrentPageContext = {
5
+ path: string;
6
+ fullPath: string;
7
+ title: string;
8
+ url: string;
9
+ };
10
+
11
+ const getUserLocationSchema = z.object({});
12
+
13
+ export function createGetUserLocationTool() {
14
+ return tool(
15
+ async (_input, runtime) => {
16
+ const currentPage = (runtime.context as { currentPage?: CurrentPageContext }).currentPage;
17
+
18
+ if (!currentPage) {
19
+ return JSON.stringify(
20
+ {
21
+ status: 404,
22
+ message: "Current user location is not available.",
23
+ },
24
+ null,
25
+ 2,
26
+ );
27
+ }
28
+
29
+ return JSON.stringify(
30
+ {
31
+ status: 200,
32
+ location: currentPage,
33
+ },
34
+ null,
35
+ 2,
36
+ );
37
+ },
38
+ {
39
+ name: "get_user_location",
40
+ description:
41
+ "Get the user's current location in the AdminForth UI, including current page path, full path, title, and URL. Call this tool when you do not understand what the user is referring to, especially when they say here, this page, current page, opened page, or otherwise rely on page context.",
42
+ schema: getUserLocationSchema,
43
+ },
44
+ );
45
+ }
@@ -3,6 +3,7 @@ import { createFetchSkillTool } from "./fetchSkill.js";
3
3
  import { createFetchToolSchemaTool } from "./fetchToolSchema.js";
4
4
  import type { ApiBasedTool } from "../../apiBasedTools.js";
5
5
  import { createApiTool } from "./apiTool.js";
6
+ import { createGetUserLocationTool } from "./getUserLocation.js";
6
7
 
7
8
  export const ALWAYS_AVAILABLE_API_TOOL_NAMES = ["get_resource"] as const;
8
9
 
@@ -20,6 +21,7 @@ export async function createAgentTools(
20
21
 
21
22
  return createApiTool(toolName, apiBasedTool);
22
23
  }),
24
+ createGetUserLocationTool(),
23
25
  await createFetchSkillTool(customComponentsDir),
24
26
  await createFetchToolSchemaTool(apiBasedTools),
25
27
  ];
package/build.log CHANGED
@@ -40,5 +40,5 @@ custom/skills/fetch_data/SKILL.md
40
40
  custom/skills/mutate_data/
41
41
  custom/skills/mutate_data/SKILL.md
42
42
 
43
- sent 210,653 bytes received 585 bytes 422,476.00 bytes/sec
43
+ sent 210,681 bytes received 581 bytes 422,524.00 bytes/sec
44
44
  total size is 208,279 speedup is 0.99
@@ -19,6 +19,7 @@ export const contextSchema = z.object({
19
19
  userTimeZone: z.string(),
20
20
  sessionId: z.string(),
21
21
  turnId: z.string(),
22
+ currentPage: z.custom().optional(),
22
23
  httpExtra: z.custom().optional(),
23
24
  emitToolCallEvent: z.custom(),
24
25
  });
@@ -131,7 +132,7 @@ export function createAgentChatModel(params) {
131
132
  }
132
133
  export function callAgent(params) {
133
134
  return __awaiter(this, void 0, void 0, function* () {
134
- const { name, model, summaryModel, modelMiddleware = [], checkpointer, messages, adminUser, adminforth, apiBasedTools, customComponentsDir, sessionId, turnId, httpExtra, userTimeZone, emitToolCallEvent, sequenceDebugSink, } = params;
135
+ const { name, model, summaryModel, modelMiddleware = [], checkpointer, messages, adminUser, adminforth, apiBasedTools, customComponentsDir, sessionId, turnId, currentPage, httpExtra, userTimeZone, emitToolCallEvent, sequenceDebugSink, } = params;
135
136
  const tools = yield createAgentTools(customComponentsDir, apiBasedTools);
136
137
  const apiBasedToolsMiddleware = createApiBasedToolsMiddleware(apiBasedTools, adminforth);
137
138
  const sequenceDebugMiddleware = createSequenceDebugMiddleware(sequenceDebugSink);
@@ -165,6 +166,7 @@ export function callAgent(params) {
165
166
  userTimeZone,
166
167
  sessionId,
167
168
  turnId,
169
+ currentPage,
168
170
  httpExtra,
169
171
  emitToolCallEvent,
170
172
  },
@@ -0,0 +1,31 @@
1
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
2
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
3
+ return new (P || (P = Promise))(function (resolve, reject) {
4
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
5
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
6
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
7
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
8
+ });
9
+ };
10
+ import { tool } from "langchain";
11
+ import { z } from "zod";
12
+ const getUserLocationSchema = z.object({});
13
+ export function createGetUserLocationTool() {
14
+ return tool((_input, runtime) => __awaiter(this, void 0, void 0, function* () {
15
+ const currentPage = runtime.context.currentPage;
16
+ if (!currentPage) {
17
+ return JSON.stringify({
18
+ status: 404,
19
+ message: "Current user location is not available.",
20
+ }, null, 2);
21
+ }
22
+ return JSON.stringify({
23
+ status: 200,
24
+ location: currentPage,
25
+ }, null, 2);
26
+ }), {
27
+ name: "get_user_location",
28
+ description: "Get the user's current location in the AdminForth UI, including current page path, full path, title, and URL. Call this tool when you do not understand what the user is referring to, especially when they say here, this page, current page, opened page, or otherwise rely on page context.",
29
+ schema: getUserLocationSchema,
30
+ });
31
+ }
@@ -10,6 +10,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
10
10
  import { createFetchSkillTool } from "./fetchSkill.js";
11
11
  import { createFetchToolSchemaTool } from "./fetchToolSchema.js";
12
12
  import { createApiTool } from "./apiTool.js";
13
+ import { createGetUserLocationTool } from "./getUserLocation.js";
13
14
  export const ALWAYS_AVAILABLE_API_TOOL_NAMES = ["get_resource"];
14
15
  export function createAgentTools(customComponentsDir, apiBasedTools) {
15
16
  return __awaiter(this, void 0, void 0, function* () {
@@ -21,6 +22,7 @@ export function createAgentTools(customComponentsDir, apiBasedTools) {
21
22
  }
22
23
  return createApiTool(toolName, apiBasedTool);
23
24
  }),
25
+ createGetUserLocationTool(),
24
26
  yield createFetchSkillTool(customComponentsDir),
25
27
  yield createFetchToolSchemaTool(apiBasedTools),
26
28
  ];
package/dist/index.js CHANGED
@@ -47,6 +47,19 @@ function formatAgentError(error) {
47
47
  }
48
48
  return String(error);
49
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");
55
+ }
56
+ return error.message || "Agent response failed";
57
+ }
58
+ if (error instanceof Error) {
59
+ return error.toString();
60
+ }
61
+ return String(error);
62
+ }
50
63
  function formatAdminUserPrompt(adminUser, usernameField) {
51
64
  const dbUser = adminUser.dbUser;
52
65
  const adminUserContext = {
@@ -59,16 +72,6 @@ function formatAdminUserPrompt(adminUser, usernameField) {
59
72
  "Use this admin user email when the user asks to send information to themselves, the current admin, or the logged-in user.",
60
73
  ].join("\n");
61
74
  }
62
- function formatCurrentPagePrompt(currentPage) {
63
- if (!currentPage) {
64
- return null;
65
- }
66
- return [
67
- "Current user page context for the latest message:",
68
- JSON.stringify(currentPage, null, 2),
69
- "When the user says here, this page, current page, or opened page, treat it as this page.",
70
- ].join("\n");
71
- }
72
75
  function assertRequiredApiTool(apiBasedTools, toolName) {
73
76
  if (toolName in apiBasedTools) {
74
77
  return;
@@ -191,6 +194,7 @@ export default class AdminForthAgentPlugin extends AdminForthPlugin {
191
194
  modes: this.options.modes.map((mode) => ({ name: mode.name })),
192
195
  defaultModeName: this.options.modes[0].name,
193
196
  stickByDefault: (_b = this.options.stickByDefault) !== null && _b !== void 0 ? _b : false,
197
+ hasAudioAdapter: Boolean(this.options.audioAdapter),
194
198
  }
195
199
  });
196
200
  if (!this.pluginOptions.sessionResource) {
@@ -199,12 +203,110 @@ export default class AdminForthAgentPlugin extends AdminForthPlugin {
199
203
  });
200
204
  }
201
205
  validateConfigAfterDiscover(adminforth, resourceConfig) {
206
+ var _a;
207
+ (_a = this.options.audioAdapter) === null || _a === void 0 ? void 0 : _a.validate();
202
208
  this.agentSystemPromptPromise = buildAgentSystemPrompt(adminforth, this.getInternalAgentResourceIds())
203
209
  .then((systemPrompt) => appendCustomSystemPrompt(systemPrompt, this.options.systemPrompt));
204
210
  }
205
211
  instanceUniqueRepresentation(pluginOptions) {
206
212
  return `single`;
207
213
  }
214
+ runAgentTurn(input) {
215
+ return __awaiter(this, void 0, void 0, function* () {
216
+ var _a, e_1, _b, _c;
217
+ var _d, _e, _f, _g;
218
+ let fullResponse = "";
219
+ const maxTokens = (_d = this.options.maxTokens) !== null && _d !== void 0 ? _d : 10000;
220
+ 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);
222
+ const userLanguage = yield detectUserLanguage(selectedMode.completionAdapter, input.prompt)
223
+ .catch((error) => {
224
+ logger.warn(`Failed to detect user language: ${error instanceof Error ? error.message : String(error)}`);
225
+ return null;
226
+ });
227
+ const systemPrompt = [
228
+ yield this.agentSystemPromptPromise,
229
+ formatAdminUserPrompt(input.adminUser, this.adminforth.config.auth.usernameField),
230
+ formatLanguagePrompt(userLanguage),
231
+ ].join("\n\n");
232
+ 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
+ const stream = yield callAgent({
239
+ name: `adminforth-agent-${this.pluginInstanceId}`,
240
+ model,
241
+ summaryModel,
242
+ modelMiddleware,
243
+ checkpointer: this.getCheckpointer(),
244
+ messages: [
245
+ new SystemMessage(systemPrompt),
246
+ new HumanMessage(input.prompt),
247
+ ],
248
+ adminUser: input.adminUser,
249
+ adminforth: this.adminforth,
250
+ apiBasedTools,
251
+ customComponentsDir: this.adminforth.config.customization.customComponentsDir,
252
+ sessionId: input.sessionId,
253
+ turnId: input.turnId,
254
+ currentPage: input.currentPage,
255
+ httpExtra: input.httpExtra,
256
+ userTimeZone: input.userTimeZone,
257
+ emitToolCallEvent: (event) => {
258
+ var _a;
259
+ input.sequenceDebugCollector.handleToolCallEvent(event);
260
+ (_a = input.emitToolCallEvent) === null || _a === void 0 ? void 0 : _a.call(input, event);
261
+ },
262
+ sequenceDebugSink: input.sequenceDebugCollector,
263
+ });
264
+ try {
265
+ for (var _h = true, _j = __asyncValues(stream), _k; _k = yield _j.next(), _a = _k.done, !_a; _h = true) {
266
+ _c = _k.value;
267
+ _h = false;
268
+ const rawChunk = _c;
269
+ const [token, metadata] = rawChunk;
270
+ const nodeName = typeof (metadata === null || metadata === void 0 ? void 0 : metadata.langgraph_node) === "string"
271
+ ? metadata.langgraph_node
272
+ : "";
273
+ if (nodeName && !["model", "model_request"].includes(nodeName)) {
274
+ continue;
275
+ }
276
+ const blocks = Array.isArray(token === null || token === void 0 ? void 0 : token.contentBlocks)
277
+ ? token.contentBlocks
278
+ : Array.isArray(token === null || token === void 0 ? void 0 : token.content)
279
+ ? token.content
280
+ : [];
281
+ const reasoningDelta = blocks
282
+ .filter((b) => (b === null || b === void 0 ? void 0 : b.type) === "reasoning")
283
+ .map((b) => { var _a; return String((_a = b.reasoning) !== null && _a !== void 0 ? _a : ""); })
284
+ .join("");
285
+ const textDelta = blocks
286
+ .filter((b) => (b === null || b === void 0 ? void 0 : b.type) === "text")
287
+ .map((b) => { var _a; return String((_a = b.text) !== null && _a !== void 0 ? _a : ""); })
288
+ .join("");
289
+ if (reasoningDelta) {
290
+ (_f = input.emitReasoningDelta) === null || _f === void 0 ? void 0 : _f.call(input, reasoningDelta);
291
+ }
292
+ if (textDelta) {
293
+ fullResponse += textDelta;
294
+ (_g = input.emitTextDelta) === null || _g === void 0 ? void 0 : _g.call(input, textDelta);
295
+ }
296
+ }
297
+ }
298
+ catch (e_1_1) { e_1 = { error: e_1_1 }; }
299
+ finally {
300
+ try {
301
+ if (!_h && !_a && (_b = _j.return)) yield _b.call(_j);
302
+ }
303
+ finally { if (e_1) throw e_1.error; }
304
+ }
305
+ return {
306
+ text: fullResponse,
307
+ };
308
+ });
309
+ }
208
310
  setupEndpoints(server) {
209
311
  server.endpoint({
210
312
  method: 'POST',
@@ -235,12 +337,11 @@ export default class AdminForthAgentPlugin extends AdminForthPlugin {
235
337
  method: 'POST',
236
338
  path: `/agent/response`,
237
339
  handler: (_a) => __awaiter(this, [_a], void 0, function* ({ body, query, headers, cookies, adminUser, response, requestUrl, _raw_express_res }) {
238
- var _b, e_1, _c, _d;
239
- var _e, _f, _g;
340
+ var _b;
240
341
  const res = _raw_express_res;
241
342
  const messageId = randomUUID();
242
343
  const prompt = body.message;
243
- const userTimeZone = (_e = body.timeZone) !== null && _e !== void 0 ? _e : 'UTC';
344
+ const userTimeZone = (_b = body.timeZone) !== null && _b !== void 0 ? _b : 'UTC';
244
345
  const currentPage = body.currentPage;
245
346
  const sessionId = body.sessionId || (adminUser === null || adminUser === void 0 ? void 0 : adminUser.pk) || (adminUser === null || adminUser === void 0 ? void 0 : adminUser.username) || 'default';
246
347
  const turnId = yield this.createNewTurn(sessionId, prompt);
@@ -264,7 +365,6 @@ export default class AdminForthAgentPlugin extends AdminForthPlugin {
264
365
  if (event.phase === "start") {
265
366
  endActiveBlock();
266
367
  }
267
- sequenceDebugCollector.handleToolCallEvent(event);
268
368
  send({
269
369
  type: "data-tool-call",
270
370
  data: event,
@@ -311,43 +411,14 @@ export default class AdminForthAgentPlugin extends AdminForthPlugin {
311
411
  type: 'start',
312
412
  messageId,
313
413
  });
314
- const maxTokens = (_f = this.options.maxTokens) !== null && _f !== void 0 ? _f : 10000;
315
- const selectedMode = (_g = this.options.modes.find((mode) => mode.name === body.mode)) !== null && _g !== void 0 ? _g : this.options.modes[0];
316
- const { model, summaryModel, modelMiddleware } = yield this.getModeModels(selectedMode, maxTokens);
317
- const userLanguage = yield detectUserLanguage(selectedMode.completionAdapter, prompt)
318
- .catch((error) => {
319
- logger.warn(`Failed to detect user language: ${error instanceof Error ? error.message : String(error)}`);
320
- return null;
321
- });
322
- const systemPrompt = [
323
- yield this.agentSystemPromptPromise,
324
- formatAdminUserPrompt(adminUser, this.adminforth.config.auth.usernameField),
325
- formatLanguagePrompt(userLanguage),
326
- ].join("\n\n");
327
- const apiBasedTools = buildApiBasedTools(this.adminforth, this.getInternalAgentResourceIds());
328
- for (const toolName of ALWAYS_AVAILABLE_API_TOOL_NAMES) {
329
- assertRequiredApiTool(apiBasedTools, toolName);
330
- }
331
- assertRequiredApiTool(apiBasedTools, "update_record");
332
- this.apiBasedTools = apiBasedTools;
333
- const currentPagePrompt = formatCurrentPagePrompt(currentPage);
334
- const stream = yield callAgent({
335
- name: `adminforth-agent-${this.pluginInstanceId}`,
336
- model,
337
- summaryModel,
338
- modelMiddleware,
339
- checkpointer: this.getCheckpointer(),
340
- messages: [
341
- new SystemMessage(systemPrompt),
342
- ...(currentPagePrompt ? [new SystemMessage(currentPagePrompt)] : []),
343
- new HumanMessage(prompt),
344
- ],
345
- adminUser,
346
- adminforth: this.adminforth,
347
- apiBasedTools,
348
- customComponentsDir: this.adminforth.config.customization.customComponentsDir,
414
+ const agentResponse = yield this.runAgentTurn({
415
+ prompt,
349
416
  sessionId,
350
417
  turnId,
418
+ modeName: body.mode,
419
+ userTimeZone,
420
+ currentPage,
421
+ adminUser,
351
422
  httpExtra: {
352
423
  body,
353
424
  query,
@@ -356,70 +427,37 @@ export default class AdminForthAgentPlugin extends AdminForthPlugin {
356
427
  requestUrl,
357
428
  response,
358
429
  },
359
- userTimeZone,
430
+ sequenceDebugCollector,
360
431
  emitToolCallEvent,
361
- sequenceDebugSink: sequenceDebugCollector,
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
+ },
362
449
  });
363
- try {
364
- for (var _h = true, _j = __asyncValues(stream), _k; _k = yield _j.next(), _b = _k.done, !_b; _h = true) {
365
- _d = _k.value;
366
- _h = false;
367
- const rawChunk = _d;
368
- const [token, metadata] = rawChunk;
369
- const nodeName = typeof (metadata === null || metadata === void 0 ? void 0 : metadata.langgraph_node) === "string"
370
- ? metadata.langgraph_node
371
- : "";
372
- if (nodeName && !["model", "model_request"].includes(nodeName)) {
373
- continue;
374
- }
375
- const blocks = Array.isArray(token === null || token === void 0 ? void 0 : token.contentBlocks)
376
- ? token.contentBlocks
377
- : Array.isArray(token === null || token === void 0 ? void 0 : token.content)
378
- ? token.content
379
- : [];
380
- const reasoningDelta = blocks
381
- .filter((b) => (b === null || b === void 0 ? void 0 : b.type) === "reasoning")
382
- .map((b) => { var _a; return String((_a = b.reasoning) !== null && _a !== void 0 ? _a : ""); })
383
- .join("");
384
- const textDelta = blocks
385
- .filter((b) => (b === null || b === void 0 ? void 0 : b.type) === "text")
386
- .map((b) => { var _a; return String((_a = b.text) !== null && _a !== void 0 ? _a : ""); })
387
- .join("");
388
- if (reasoningDelta) {
389
- const reasoningId = startBlock('reasoning');
390
- send({
391
- type: 'reasoning-delta',
392
- id: reasoningId,
393
- delta: reasoningDelta,
394
- });
395
- }
396
- if (textDelta) {
397
- const textId = startBlock('text');
398
- fullResponse += textDelta;
399
- send({
400
- type: 'text-delta',
401
- id: textId,
402
- delta: textDelta,
403
- });
404
- }
405
- }
406
- }
407
- catch (e_1_1) { e_1 = { error: e_1_1 }; }
408
- finally {
409
- try {
410
- if (!_h && !_b && (_c = _j.return)) yield _c.call(_j);
411
- }
412
- finally { if (e_1) throw e_1.error; }
413
- }
450
+ fullResponse = agentResponse.text;
414
451
  }
415
452
  catch (error) {
416
453
  logger.error(`Agent response streaming failed:\n${formatAgentError(error)}`);
417
454
  sequenceDebugCollector.flush();
455
+ fullResponse = formatAgentResponseError(error);
418
456
  const textId = startBlock('text');
419
457
  send({
420
458
  type: 'text-delta',
421
459
  id: textId,
422
- delta: 'Agent response failed. Check server logs for details.',
460
+ delta: fullResponse,
423
461
  });
424
462
  }
425
463
  sequenceDebugCollector.flush();
@@ -434,6 +472,115 @@ export default class AdminForthAgentPlugin extends AdminForthPlugin {
434
472
  return null;
435
473
  })
436
474
  });
475
+ server.endpoint({
476
+ method: 'POST',
477
+ path: `/agent/speech-response`,
478
+ handler: (_a) => __awaiter(this, [_a], void 0, function* ({ body, query, headers, cookies, adminUser, response, requestUrl }) {
479
+ var _b;
480
+ const audioAdapter = this.options.audioAdapter;
481
+ if (!audioAdapter) {
482
+ response.setStatus(400, undefined);
483
+ return {
484
+ error: "Audio adapter is not configured for AdminForth Agent",
485
+ };
486
+ }
487
+ const speechBody = body;
488
+ let transcription;
489
+ try {
490
+ transcription = yield audioAdapter.transcribe({
491
+ buffer: Buffer.from(speechBody.audioBase64, "base64"),
492
+ filename: speechBody.filename,
493
+ mimeType: speechBody.mimeType,
494
+ language: "auto",
495
+ prompt: speechBody.prompt,
496
+ });
497
+ }
498
+ 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
+ };
504
+ }
505
+ const prompt = transcription.text;
506
+ if (!prompt) {
507
+ response.setStatus(400, undefined);
508
+ return {
509
+ error: "Speech transcription is empty",
510
+ };
511
+ }
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
+ 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
+ },
538
+ });
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();
547
+ }
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
+ };
565
+ }
566
+ 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();
575
+ }
576
+ yield this.updateTurn(turnId, turnUpdates);
577
+ response.setStatus(500, undefined);
578
+ return {
579
+ error: fullResponse,
580
+ };
581
+ }
582
+ })
583
+ });
437
584
  server.endpoint({
438
585
  method: 'POST',
439
586
  path: `/agent/get-sessions`,
package/index.ts CHANGED
@@ -1,8 +1,10 @@
1
1
  import type {
2
2
  AdminUser,
3
3
  AdminForthResource,
4
+ HttpExtra,
4
5
  IAdminForth,
5
- IHttpServer
6
+ IHttpServer,
7
+ TextToSpeechInput,
6
8
  } from "adminforth";
7
9
 
8
10
  import { AdminForthPlugin, logger, Filters, Sorts } from "adminforth";
@@ -33,16 +35,36 @@ import {
33
35
  } from "./agent/systemPrompt.js";
34
36
  import { ALWAYS_AVAILABLE_API_TOOL_NAMES } from "./agent/tools/index.js";
35
37
  import type { ToolCallEvent } from "./agent/toolCallEvents.js";
38
+ import type { CurrentPageContext } from "./agent/tools/getUserLocation.js";
36
39
 
37
40
  type CurrentPageRequestBody = {
38
41
  currentPage?: CurrentPageContext;
39
42
  };
40
43
 
41
- type CurrentPageContext = {
42
- path: string;
43
- fullPath: string;
44
- title: string;
45
- url: string;
44
+ type SpeechResponseRequestBody = CurrentPageRequestBody & {
45
+ audioBase64: string;
46
+ filename: string;
47
+ mimeType: string;
48
+ prompt?: string;
49
+ sessionId?: string | null;
50
+ mode?: string | null;
51
+ timeZone?: string;
52
+ tts?: Omit<TextToSpeechInput, "text">;
53
+ };
54
+
55
+ type AgentTurnRunInput = {
56
+ prompt: string;
57
+ sessionId: string;
58
+ turnId: string;
59
+ modeName?: string | null;
60
+ userTimeZone: string;
61
+ currentPage?: CurrentPageContext;
62
+ adminUser: AdminUser;
63
+ httpExtra: HttpExtra;
64
+ sequenceDebugCollector: ReturnType<typeof createSequenceDebugCollector>;
65
+ emitReasoningDelta?: (delta: string) => void;
66
+ emitTextDelta?: (delta: string) => void;
67
+ emitToolCallEvent?: (event: ToolCallEvent) => void;
46
68
  };
47
69
 
48
70
  function isAggregateErrorLike(
@@ -73,6 +95,24 @@ function formatAgentError(error: unknown) {
73
95
  return String(error);
74
96
  }
75
97
 
98
+ function formatAgentResponseError(error: unknown): string {
99
+ if (isAggregateErrorLike(error)) {
100
+ const nestedErrors = error.errors.map(formatAgentResponseError);
101
+
102
+ if (nestedErrors.length) {
103
+ return nestedErrors.join("\n");
104
+ }
105
+
106
+ return error.message || "Agent response failed";
107
+ }
108
+
109
+ if (error instanceof Error) {
110
+ return error.toString();
111
+ }
112
+
113
+ return String(error);
114
+ }
115
+
76
116
  function formatAdminUserPrompt(adminUser: AdminUser, usernameField: string) {
77
117
  const dbUser = adminUser.dbUser as Record<string, unknown>;
78
118
  const adminUserContext = {
@@ -87,18 +127,6 @@ function formatAdminUserPrompt(adminUser: AdminUser, usernameField: string) {
87
127
  ].join("\n");
88
128
  }
89
129
 
90
- function formatCurrentPagePrompt(currentPage: CurrentPageContext | undefined) {
91
- if (!currentPage) {
92
- return null;
93
- }
94
-
95
- return [
96
- "Current user page context for the latest message:",
97
- JSON.stringify(currentPage, null, 2),
98
- "When the user says here, this page, current page, or opened page, treat it as this page.",
99
- ].join("\n");
100
- }
101
-
102
130
  function assertRequiredApiTool(
103
131
  apiBasedTools: Record<string, ApiBasedTool>,
104
132
  toolName: string,
@@ -245,6 +273,7 @@ export default class AdminForthAgentPlugin extends AdminForthPlugin {
245
273
  modes: this.options.modes.map((mode) => ({ name: mode.name })),
246
274
  defaultModeName: this.options.modes[0].name,
247
275
  stickByDefault: this.options.stickByDefault ?? false,
276
+ hasAudioAdapter: Boolean(this.options.audioAdapter),
248
277
  }
249
278
  });
250
279
  if (!this.pluginOptions.sessionResource) {
@@ -253,6 +282,7 @@ export default class AdminForthAgentPlugin extends AdminForthPlugin {
253
282
  }
254
283
 
255
284
  validateConfigAfterDiscover(adminforth: IAdminForth, resourceConfig: AdminForthResource) {
285
+ this.options.audioAdapter?.validate();
256
286
  this.agentSystemPromptPromise = buildAgentSystemPrompt(
257
287
  adminforth,
258
288
  this.getInternalAgentResourceIds(),
@@ -264,6 +294,101 @@ export default class AdminForthAgentPlugin extends AdminForthPlugin {
264
294
  return `single`;
265
295
  }
266
296
 
297
+ private async runAgentTurn(input: AgentTurnRunInput) {
298
+ let fullResponse = "";
299
+ const maxTokens = this.options.maxTokens ?? 10000;
300
+ const selectedMode =
301
+ this.options.modes.find((mode) => mode.name === input.modeName) ??
302
+ this.options.modes[0];
303
+ const { model, summaryModel, modelMiddleware } =
304
+ await this.getModeModels(selectedMode, maxTokens);
305
+ const userLanguage = await detectUserLanguage(selectedMode.completionAdapter, input.prompt)
306
+ .catch((error) => {
307
+ logger.warn(`Failed to detect user language: ${error instanceof Error ? error.message : String(error)}`);
308
+ return null;
309
+ });
310
+ const systemPrompt = [
311
+ await this.agentSystemPromptPromise,
312
+ formatAdminUserPrompt(input.adminUser, this.adminforth.config.auth.usernameField),
313
+ formatLanguagePrompt(userLanguage),
314
+ ].join("\n\n");
315
+ const apiBasedTools = buildApiBasedTools(
316
+ this.adminforth,
317
+ this.getInternalAgentResourceIds(),
318
+ );
319
+ for (const toolName of ALWAYS_AVAILABLE_API_TOOL_NAMES) {
320
+ assertRequiredApiTool(apiBasedTools, toolName);
321
+ }
322
+ assertRequiredApiTool(apiBasedTools, "update_record");
323
+ this.apiBasedTools = apiBasedTools;
324
+ const stream = await callAgent({
325
+ name: `adminforth-agent-${this.pluginInstanceId}`,
326
+ model,
327
+ summaryModel,
328
+ modelMiddleware,
329
+ checkpointer: this.getCheckpointer(),
330
+ messages: [
331
+ new SystemMessage(systemPrompt),
332
+ new HumanMessage(input.prompt),
333
+ ],
334
+ adminUser: input.adminUser,
335
+ adminforth: this.adminforth,
336
+ apiBasedTools,
337
+ customComponentsDir: this.adminforth.config.customization.customComponentsDir,
338
+ sessionId: input.sessionId,
339
+ turnId: input.turnId,
340
+ currentPage: input.currentPage,
341
+ httpExtra: input.httpExtra,
342
+ userTimeZone: input.userTimeZone,
343
+ emitToolCallEvent: (event) => {
344
+ input.sequenceDebugCollector.handleToolCallEvent(event);
345
+ input.emitToolCallEvent?.(event);
346
+ },
347
+ sequenceDebugSink: input.sequenceDebugCollector,
348
+ });
349
+
350
+ for await (const rawChunk of stream as AsyncIterable<[any, any]>) {
351
+ const [token, metadata] = rawChunk;
352
+
353
+ const nodeName =
354
+ typeof metadata?.langgraph_node === "string"
355
+ ? metadata.langgraph_node
356
+ : "";
357
+
358
+ if (nodeName && !["model", "model_request"].includes(nodeName)) {
359
+ continue;
360
+ }
361
+
362
+ const blocks = Array.isArray(token?.contentBlocks)
363
+ ? token.contentBlocks
364
+ : Array.isArray(token?.content)
365
+ ? token.content
366
+ : [];
367
+ const reasoningDelta = blocks
368
+ .filter((b: any) => b?.type === "reasoning")
369
+ .map((b: any) => String(b.reasoning ?? ""))
370
+ .join("");
371
+
372
+ const textDelta = blocks
373
+ .filter((b: any) => b?.type === "text")
374
+ .map((b: any) => String(b.text ?? ""))
375
+ .join("");
376
+
377
+ if (reasoningDelta) {
378
+ input.emitReasoningDelta?.(reasoningDelta);
379
+ }
380
+
381
+ if (textDelta) {
382
+ fullResponse += textDelta;
383
+ input.emitTextDelta?.(textDelta);
384
+ }
385
+ }
386
+
387
+ return {
388
+ text: fullResponse,
389
+ };
390
+ }
391
+
267
392
  setupEndpoints(server: IHttpServer) {
268
393
  server.endpoint({
269
394
  method: 'POST',
@@ -327,8 +452,6 @@ export default class AdminForthAgentPlugin extends AdminForthPlugin {
327
452
  endActiveBlock();
328
453
  }
329
454
 
330
- sequenceDebugCollector.handleToolCallEvent(event);
331
-
332
455
  send({
333
456
  type: "data-tool-call",
334
457
  data: event,
@@ -389,47 +512,14 @@ export default class AdminForthAgentPlugin extends AdminForthPlugin {
389
512
  messageId,
390
513
  });
391
514
 
392
- const maxTokens = this.options.maxTokens ?? 10000;
393
- const selectedMode = this.options.modes.find((mode) => mode.name === body.mode) ?? this.options.modes[0];
394
- const { model, summaryModel, modelMiddleware } =
395
- await this.getModeModels(selectedMode, maxTokens);
396
- const userLanguage = await detectUserLanguage(selectedMode.completionAdapter, prompt)
397
- .catch((error) => {
398
- logger.warn(`Failed to detect user language: ${error instanceof Error ? error.message : String(error)}`);
399
- return null;
400
- });
401
- const systemPrompt = [
402
- await this.agentSystemPromptPromise,
403
- formatAdminUserPrompt(adminUser, this.adminforth.config.auth.usernameField),
404
- formatLanguagePrompt(userLanguage),
405
- ].join("\n\n");
406
- const apiBasedTools = buildApiBasedTools(
407
- this.adminforth,
408
- this.getInternalAgentResourceIds(),
409
- );
410
- for (const toolName of ALWAYS_AVAILABLE_API_TOOL_NAMES) {
411
- assertRequiredApiTool(apiBasedTools, toolName);
412
- }
413
- assertRequiredApiTool(apiBasedTools, "update_record");
414
- this.apiBasedTools = apiBasedTools;
415
- const currentPagePrompt = formatCurrentPagePrompt(currentPage);
416
- const stream = await callAgent({
417
- name: `adminforth-agent-${this.pluginInstanceId}`,
418
- model,
419
- summaryModel,
420
- modelMiddleware,
421
- checkpointer: this.getCheckpointer(),
422
- messages: [
423
- new SystemMessage(systemPrompt),
424
- ...(currentPagePrompt ? [new SystemMessage(currentPagePrompt)] : []),
425
- new HumanMessage(prompt),
426
- ],
427
- adminUser,
428
- adminforth: this.adminforth,
429
- apiBasedTools,
430
- customComponentsDir: this.adminforth.config.customization.customComponentsDir,
515
+ const agentResponse = await this.runAgentTurn({
516
+ prompt,
431
517
  sessionId,
432
518
  turnId,
519
+ modeName: body.mode,
520
+ userTimeZone,
521
+ currentPage,
522
+ adminUser,
433
523
  httpExtra: {
434
524
  body,
435
525
  query,
@@ -438,48 +528,17 @@ export default class AdminForthAgentPlugin extends AdminForthPlugin {
438
528
  requestUrl,
439
529
  response,
440
530
  },
441
- userTimeZone,
531
+ sequenceDebugCollector,
442
532
  emitToolCallEvent,
443
- sequenceDebugSink: sequenceDebugCollector,
444
- });
445
-
446
- for await (const rawChunk of stream as AsyncIterable<[any, any]>) {
447
- const [token, metadata] = rawChunk;
448
-
449
- const nodeName =
450
- typeof metadata?.langgraph_node === "string"
451
- ? metadata.langgraph_node
452
- : "";
453
-
454
- if (nodeName && !["model", "model_request"].includes(nodeName)) {
455
- continue;
456
- }
457
-
458
- const blocks = Array.isArray(token?.contentBlocks)
459
- ? token.contentBlocks
460
- : Array.isArray(token?.content)
461
- ? token.content
462
- : [];
463
- const reasoningDelta = blocks
464
- .filter((b: any) => b?.type === "reasoning")
465
- .map((b: any) => String(b.reasoning ?? ""))
466
- .join("");
467
-
468
- const textDelta = blocks
469
- .filter((b: any) => b?.type === "text")
470
- .map((b: any) => String(b.text ?? ""))
471
- .join("");
472
-
473
- if (reasoningDelta) {
533
+ emitReasoningDelta: (reasoningDelta) => {
474
534
  const reasoningId = startBlock('reasoning');
475
- send({
476
- type: 'reasoning-delta',
477
- id: reasoningId,
478
- delta: reasoningDelta,
479
- });
480
- }
481
-
482
- if (textDelta) {
535
+ send({
536
+ type: 'reasoning-delta',
537
+ id: reasoningId,
538
+ delta: reasoningDelta,
539
+ });
540
+ },
541
+ emitTextDelta: (textDelta) => {
483
542
  const textId = startBlock('text');
484
543
  fullResponse += textDelta;
485
544
  send({
@@ -487,16 +546,18 @@ export default class AdminForthAgentPlugin extends AdminForthPlugin {
487
546
  id: textId,
488
547
  delta: textDelta,
489
548
  });
490
- }
491
- }
549
+ },
550
+ });
551
+ fullResponse = agentResponse.text;
492
552
  } catch (error) {
493
553
  logger.error(`Agent response streaming failed:\n${formatAgentError(error)}`);
494
554
  sequenceDebugCollector.flush();
555
+ fullResponse = formatAgentResponseError(error);
495
556
  const textId = startBlock('text');
496
557
  send({
497
558
  type: 'text-delta',
498
559
  id: textId,
499
- delta: 'Agent response failed. Check server logs for details.',
560
+ delta: fullResponse,
500
561
  });
501
562
  }
502
563
  sequenceDebugCollector.flush();
@@ -513,6 +574,125 @@ export default class AdminForthAgentPlugin extends AdminForthPlugin {
513
574
  return null;
514
575
  }
515
576
  });
577
+ server.endpoint({
578
+ method: 'POST',
579
+ path: `/agent/speech-response`,
580
+ handler: async ({ body, query, headers, cookies, adminUser, response, requestUrl }) => {
581
+ const audioAdapter = this.options.audioAdapter;
582
+ if (!audioAdapter) {
583
+ response.setStatus(400, undefined);
584
+ return {
585
+ error: "Audio adapter is not configured for AdminForth Agent",
586
+ };
587
+ }
588
+
589
+ const speechBody = body as SpeechResponseRequestBody;
590
+ let transcription;
591
+
592
+ try {
593
+ transcription = await audioAdapter.transcribe({
594
+ buffer: Buffer.from(speechBody.audioBase64, "base64"),
595
+ filename: speechBody.filename,
596
+ mimeType: speechBody.mimeType,
597
+ language: "auto",
598
+ prompt: speechBody.prompt,
599
+ });
600
+ } catch (error) {
601
+ logger.error(`Agent speech transcription failed:\n${formatAgentError(error)}`);
602
+ response.setStatus(500, undefined);
603
+ return {
604
+ error: "Speech transcription failed. Check server logs for details.",
605
+ };
606
+ }
607
+
608
+ const prompt = transcription.text;
609
+ if (!prompt) {
610
+ response.setStatus(400, undefined);
611
+ return {
612
+ error: "Speech transcription is empty",
613
+ };
614
+ }
615
+
616
+ const sessionId = speechBody.sessionId || adminUser?.pk || adminUser?.username || 'default';
617
+ const turnId = await this.createNewTurn(sessionId, prompt);
618
+ await this.updateSessionDate(sessionId);
619
+ const sequenceDebugCollector = createSequenceDebugCollector();
620
+ let fullResponse = "";
621
+
622
+ try {
623
+ const agentResponse = await this.runAgentTurn({
624
+ prompt,
625
+ sessionId,
626
+ turnId,
627
+ modeName: speechBody.mode,
628
+ userTimeZone: speechBody.timeZone ?? 'UTC',
629
+ currentPage: speechBody.currentPage,
630
+ adminUser,
631
+ httpExtra: {
632
+ body,
633
+ query,
634
+ headers,
635
+ cookies,
636
+ requestUrl,
637
+ response,
638
+ },
639
+ sequenceDebugCollector,
640
+ emitTextDelta: (textDelta) => {
641
+ fullResponse += textDelta;
642
+ },
643
+ });
644
+ fullResponse = agentResponse.text;
645
+ const speech = await audioAdapter.synthesize({
646
+ text: fullResponse,
647
+ ...speechBody.tts,
648
+ });
649
+ sequenceDebugCollector.flush();
650
+ const turnUpdates: Record<string, unknown> = {
651
+ [this.options.turnResource.responseField]: fullResponse,
652
+ };
653
+
654
+ if (this.options.turnResource.debugField) {
655
+ turnUpdates[this.options.turnResource.debugField] = sequenceDebugCollector.getHistory();
656
+ }
657
+
658
+ await this.updateTurn(turnId, turnUpdates);
659
+
660
+ return {
661
+ transcript: {
662
+ text: transcription.text,
663
+ language: transcription.language,
664
+ },
665
+ response: {
666
+ text: fullResponse,
667
+ },
668
+ audio: {
669
+ base64: speech.audio.toString("base64"),
670
+ mimeType: speech.mimeType,
671
+ format: speech.format,
672
+ },
673
+ sessionId,
674
+ turnId,
675
+ };
676
+ } catch (error) {
677
+ logger.error(`Agent speech response failed:\n${formatAgentError(error)}`);
678
+ sequenceDebugCollector.flush();
679
+ fullResponse = formatAgentResponseError(error);
680
+ const turnUpdates: Record<string, unknown> = {
681
+ [this.options.turnResource.responseField]: fullResponse,
682
+ };
683
+
684
+ if (this.options.turnResource.debugField) {
685
+ turnUpdates[this.options.turnResource.debugField] = sequenceDebugCollector.getHistory();
686
+ }
687
+
688
+ await this.updateTurn(turnId, turnUpdates);
689
+ response.setStatus(500, undefined);
690
+ return {
691
+ error: fullResponse,
692
+ };
693
+ }
694
+ }
695
+ });
516
696
  server.endpoint({
517
697
  method: 'POST',
518
698
  path: `/agent/get-sessions`,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adminforth/agent",
3
- "version": "1.34.2",
3
+ "version": "1.35.0",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "type": "module",
@@ -33,7 +33,7 @@
33
33
  "@langchain/core": "^1.1.40",
34
34
  "@langchain/langgraph": "^1.2.8",
35
35
  "@langchain/langgraph-checkpoint": "^1.0.1",
36
- "adminforth": "2.42.0",
36
+ "adminforth": "2.51.0",
37
37
  "dayjs": "^1.11.20",
38
38
  "langchain": "^1.3.3",
39
39
  "yaml": "^2.8.3",
package/types.ts CHANGED
@@ -2,6 +2,7 @@ import {
2
2
  type PluginsCommonOptions,
3
3
  type AdminUser,
4
4
  type HttpExtra,
5
+ type AudioAdapter,
5
6
  } from "adminforth";
6
7
  import type { AgentModeCompletionAdapter } from "./agent/simpleAgent.js";
7
8
 
@@ -61,6 +62,11 @@ export interface PluginOptions extends PluginsCommonOptions {
61
62
  completionAdapter: AgentModeCompletionAdapter;
62
63
  }[];
63
64
 
65
+ /**
66
+ * Optional audio adapter for speech-to-text and text-to-speech flows.
67
+ */
68
+ audioAdapter?: AudioAdapter;
69
+
64
70
  /**
65
71
  * Max tokens for the generation.
66
72
  * Default is 1000