@adminforth/agent 1.34.1 → 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.
- package/agent/simpleAgent.ts +5 -0
- package/agent/tools/getUserLocation.ts +45 -0
- package/agent/tools/index.ts +2 -0
- package/build.log +2 -2
- package/custom/ChatSurface.vue +1 -1
- package/dist/agent/simpleAgent.js +3 -1
- package/dist/agent/tools/getUserLocation.js +31 -0
- package/dist/agent/tools/index.js +2 -0
- package/dist/custom/ChatSurface.vue +1 -1
- package/dist/index.js +250 -103
- package/index.ts +282 -102
- package/package.json +2 -2
- package/types.ts +6 -0
package/agent/simpleAgent.ts
CHANGED
|
@@ -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
|
+
}
|
package/agent/tools/index.ts
CHANGED
|
@@ -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,
|
|
44
|
-
total size is 208,
|
|
43
|
+
sent 210,681 bytes received 581 bytes 422,524.00 bytes/sec
|
|
44
|
+
total size is 208,279 speedup is 0.99
|
package/custom/ChatSurface.vue
CHANGED
|
@@ -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
|
|
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 = (
|
|
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
|
|
315
|
-
|
|
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
|
-
|
|
430
|
+
sequenceDebugCollector,
|
|
360
431
|
emitToolCallEvent,
|
|
361
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
|
393
|
-
|
|
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
|
-
|
|
531
|
+
sequenceDebugCollector,
|
|
442
532
|
emitToolCallEvent,
|
|
443
|
-
|
|
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
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
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:
|
|
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.
|
|
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.
|
|
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
|