@adminforth/agent 1.49.3 → 1.50.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/agent/middleware/sequenceDebug.ts +42 -7
- package/agent/simpleAgent.ts +15 -0
- package/agent/systemPrompt.ts +6 -1
- package/agent/tools/index.ts +2 -0
- package/agent/tools/navigateUser.ts +210 -0
- package/agentEvents.ts +4 -0
- package/agentTurnService.ts +14 -0
- package/build.log +2 -2
- package/chatSurfaceService.ts +7 -0
- package/custom/composables/agentStore/useAgentChat.ts +8 -1
- package/custom/composables/useAgentAudio.ts +5 -0
- package/custom/composables/useAgentStore.ts +43 -0
- package/custom/skills/fetch_data/SKILL.md +2 -0
- package/custom/skills/mutate_data/SKILL.md +4 -0
- package/custom/tsconfig.json +0 -1
- package/custom/types.ts +6 -0
- package/dist/agent/middleware/sequenceDebug.d.ts +6 -0
- package/dist/agent/middleware/sequenceDebug.js +33 -6
- package/dist/agent/simpleAgent.d.ts +8 -0
- package/dist/agent/simpleAgent.js +9 -1
- package/dist/agent/systemPrompt.d.ts +1 -0
- package/dist/agent/systemPrompt.js +5 -1
- package/dist/agent/tools/index.js +2 -0
- package/dist/agent/tools/navigateUser.d.ts +55 -0
- package/dist/agent/tools/navigateUser.js +163 -0
- package/dist/agentEvents.d.ts +3 -0
- package/dist/agentTurnService.d.ts +2 -0
- package/dist/agentTurnService.js +10 -0
- package/dist/chatSurfaceService.js +10 -3
- package/dist/custom/composables/agentStore/useAgentChat.ts +8 -1
- package/dist/custom/composables/useAgentAudio.ts +5 -0
- package/dist/custom/composables/useAgentStore.ts +43 -0
- package/dist/custom/skills/fetch_data/SKILL.md +2 -0
- package/dist/custom/skills/mutate_data/SKILL.md +4 -0
- package/dist/custom/tsconfig.json +0 -1
- package/dist/custom/types.ts +6 -0
- package/dist/endpoints/chatSurfaces.js +20 -0
- package/dist/surfaces/web-sse/createSseEventEmitter.js +11 -0
- package/endpoints/chatSurfaces.ts +29 -0
- package/package.json +1 -1
- package/surfaces/web-sse/createSseEventEmitter.ts +12 -0
|
@@ -26,6 +26,9 @@ export type SequenceDebug = {
|
|
|
26
26
|
reasoningTokens: number;
|
|
27
27
|
text: string;
|
|
28
28
|
textTokens: number;
|
|
29
|
+
uncachedInputTokens: number;
|
|
30
|
+
cachedInputTokens: number;
|
|
31
|
+
outputTokens: number;
|
|
29
32
|
cachedTokens: number;
|
|
30
33
|
responseId: string | null;
|
|
31
34
|
toolCalls: SequenceDebugToolCall[];
|
|
@@ -45,6 +48,9 @@ type SequenceDebugModelCall = {
|
|
|
45
48
|
reasoningTokens: number;
|
|
46
49
|
text: string;
|
|
47
50
|
textTokens: number;
|
|
51
|
+
uncachedInputTokens: number;
|
|
52
|
+
cachedInputTokens: number;
|
|
53
|
+
outputTokens: number;
|
|
48
54
|
cachedTokens: number;
|
|
49
55
|
responseId: string | null;
|
|
50
56
|
resultType: SequenceDebugResultType;
|
|
@@ -52,6 +58,7 @@ type SequenceDebugModelCall = {
|
|
|
52
58
|
|
|
53
59
|
type OpenAiUsageMetadata = {
|
|
54
60
|
input_tokens?: number;
|
|
61
|
+
output_tokens?: number;
|
|
55
62
|
input_token_details?: {
|
|
56
63
|
cache_read?: number;
|
|
57
64
|
};
|
|
@@ -82,6 +89,9 @@ function createPendingSequenceDebug(sequenceId: number): PendingSequenceDebug {
|
|
|
82
89
|
reasoningTokens: 0,
|
|
83
90
|
text: "",
|
|
84
91
|
textTokens: 0,
|
|
92
|
+
uncachedInputTokens: 0,
|
|
93
|
+
cachedInputTokens: 0,
|
|
94
|
+
outputTokens: 0,
|
|
85
95
|
cachedTokens: 0,
|
|
86
96
|
responseId: null,
|
|
87
97
|
toolCalls: [],
|
|
@@ -112,6 +122,9 @@ function finalizeSequenceDebug(sequence: PendingSequenceDebug): SequenceDebug {
|
|
|
112
122
|
reasoningTokens: sequence.reasoningTokens,
|
|
113
123
|
text: sequence.text,
|
|
114
124
|
textTokens: sequence.textTokens,
|
|
125
|
+
uncachedInputTokens: sequence.uncachedInputTokens,
|
|
126
|
+
cachedInputTokens: sequence.cachedInputTokens,
|
|
127
|
+
outputTokens: sequence.outputTokens,
|
|
115
128
|
cachedTokens: sequence.cachedTokens,
|
|
116
129
|
responseId: sequence.responseId,
|
|
117
130
|
toolCalls: sequence.toolCalls.map(({ completed: _completed, ...toolCall }) => toolCall),
|
|
@@ -133,6 +146,21 @@ function getDebugModelName(model: SequenceDebugPromptModel) {
|
|
|
133
146
|
return typeof model.getName === "function" ? model.getName() : undefined;
|
|
134
147
|
}
|
|
135
148
|
|
|
149
|
+
function getDebugToolName(tool: unknown) {
|
|
150
|
+
if (!tool || typeof tool !== "object") {
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const name = (tool as { name?: unknown }).name;
|
|
155
|
+
return typeof name === "string" ? name : null;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function formatToolsForDebug(tools: unknown[]) {
|
|
159
|
+
return tools.map((tool) => ({
|
|
160
|
+
name: getDebugToolName(tool),
|
|
161
|
+
}));
|
|
162
|
+
}
|
|
163
|
+
|
|
136
164
|
function stringifyPromptForDebug(params: {
|
|
137
165
|
model: SequenceDebugPromptModel;
|
|
138
166
|
systemMessage: { text: string };
|
|
@@ -160,7 +188,7 @@ function stringifyPromptForDebug(params: {
|
|
|
160
188
|
},
|
|
161
189
|
systemMessage,
|
|
162
190
|
messages,
|
|
163
|
-
...(tools.length > 0 ? { tools } : {}),
|
|
191
|
+
...(tools.length > 0 ? { tools: formatToolsForDebug(tools) } : {}),
|
|
164
192
|
...(toolChoice !== undefined ? { toolChoice } : {}),
|
|
165
193
|
...(modelSettings ? { modelSettings } : {}),
|
|
166
194
|
...(invocationParams ? { invocationParams } : {}),
|
|
@@ -219,6 +247,9 @@ async function countTokens(model: unknown, content: string) {
|
|
|
219
247
|
}
|
|
220
248
|
|
|
221
249
|
function extractSequenceResponseDebug(message: AIMessage): SequenceDebugModelCall {
|
|
250
|
+
const usageMetadata = message.usage_metadata as OpenAiUsageMetadata | undefined;
|
|
251
|
+
const promptTokens = usageMetadata?.input_tokens ?? 0;
|
|
252
|
+
const cachedInputTokens = usageMetadata?.input_token_details?.cache_read ?? 0;
|
|
222
253
|
const blocks = getMessageBlocks(message);
|
|
223
254
|
const reasoning = blocks
|
|
224
255
|
.filter((block: any) => block?.type === "reasoning")
|
|
@@ -230,16 +261,15 @@ function extractSequenceResponseDebug(message: AIMessage): SequenceDebugModelCal
|
|
|
230
261
|
.join("");
|
|
231
262
|
|
|
232
263
|
return {
|
|
233
|
-
promptTokens
|
|
234
|
-
(message.usage_metadata as OpenAiUsageMetadata | undefined)?.input_tokens ??
|
|
235
|
-
0,
|
|
264
|
+
promptTokens,
|
|
236
265
|
reasoning,
|
|
237
266
|
reasoningTokens: 0,
|
|
238
267
|
text: textFromBlocks || (typeof message.content === "string" ? message.content : ""),
|
|
239
268
|
textTokens: 0,
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
269
|
+
uncachedInputTokens: Math.max(promptTokens - cachedInputTokens, 0),
|
|
270
|
+
cachedInputTokens,
|
|
271
|
+
outputTokens: usageMetadata?.output_tokens ?? 0,
|
|
272
|
+
cachedTokens: cachedInputTokens,
|
|
243
273
|
responseId:
|
|
244
274
|
(message.response_metadata as OpenAiResponseMetadata | undefined)?.id ??
|
|
245
275
|
null,
|
|
@@ -290,6 +320,9 @@ export function createSequenceDebugCollector(): SequenceDebugCollector {
|
|
|
290
320
|
sequenceDebug.reasoningTokens = params.reasoningTokens;
|
|
291
321
|
sequenceDebug.text = params.text;
|
|
292
322
|
sequenceDebug.textTokens = params.textTokens;
|
|
323
|
+
sequenceDebug.uncachedInputTokens = params.uncachedInputTokens;
|
|
324
|
+
sequenceDebug.cachedInputTokens = params.cachedInputTokens;
|
|
325
|
+
sequenceDebug.outputTokens = params.outputTokens;
|
|
293
326
|
sequenceDebug.cachedTokens = params.cachedTokens;
|
|
294
327
|
sequenceDebug.responseId = params.responseId;
|
|
295
328
|
sequenceDebug.resultType = params.resultType;
|
|
@@ -387,6 +420,8 @@ export function createSequenceDebugMiddleware(
|
|
|
387
420
|
promptTokens,
|
|
388
421
|
reasoningTokens,
|
|
389
422
|
textTokens,
|
|
423
|
+
uncachedInputTokens: debug.promptTokens ? debug.uncachedInputTokens : promptTokens,
|
|
424
|
+
outputTokens: debug.outputTokens || reasoningTokens + textTokens,
|
|
390
425
|
});
|
|
391
426
|
return response;
|
|
392
427
|
},
|
package/agent/simpleAgent.ts
CHANGED
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
import type { ApiBasedTool } from "../apiBasedTools.js";
|
|
20
20
|
import type { ToolCallEventSink } from "./toolCallEvents.js";
|
|
21
21
|
import type { CurrentPageContext } from "./tools/getUserLocation.js";
|
|
22
|
+
import type { AgentEventEmitter } from "../agentEvents.js";
|
|
22
23
|
|
|
23
24
|
export const contextSchema = z.object({
|
|
24
25
|
adminUser: z.custom<AdminUser>(),
|
|
@@ -27,7 +28,11 @@ export const contextSchema = z.object({
|
|
|
27
28
|
turnId: z.string(),
|
|
28
29
|
abortSignal: z.custom<AbortSignal>().optional(),
|
|
29
30
|
currentPage: z.custom<CurrentPageContext>().optional(),
|
|
31
|
+
chatSurface: z.string().optional(),
|
|
32
|
+
adminBaseUrl: z.string().optional(),
|
|
33
|
+
adminPublicOrigin: z.string().optional(),
|
|
30
34
|
emitToolCallEvent: z.custom<ToolCallEventSink>(),
|
|
35
|
+
emitAgentEvent: z.custom<AgentEventEmitter>().optional(),
|
|
31
36
|
});
|
|
32
37
|
|
|
33
38
|
export type AgentChatModel = BaseChatModel<any, any>;
|
|
@@ -234,9 +239,12 @@ export async function callAgent(params: {
|
|
|
234
239
|
sessionId: string;
|
|
235
240
|
turnId: string;
|
|
236
241
|
currentPage?: CurrentPageContext;
|
|
242
|
+
chatSurface?: string;
|
|
243
|
+
adminPublicOrigin?: string;
|
|
237
244
|
userTimeZone: string;
|
|
238
245
|
abortSignal?: AbortSignal;
|
|
239
246
|
emitToolCallEvent: ToolCallEventSink;
|
|
247
|
+
emitAgentEvent?: AgentEventEmitter;
|
|
240
248
|
sequenceDebugSink: SequenceDebugModelCallSink;
|
|
241
249
|
}) {
|
|
242
250
|
const {
|
|
@@ -254,9 +262,12 @@ export async function callAgent(params: {
|
|
|
254
262
|
sessionId,
|
|
255
263
|
turnId,
|
|
256
264
|
currentPage,
|
|
265
|
+
chatSurface,
|
|
266
|
+
adminPublicOrigin,
|
|
257
267
|
userTimeZone,
|
|
258
268
|
abortSignal,
|
|
259
269
|
emitToolCallEvent,
|
|
270
|
+
emitAgentEvent,
|
|
260
271
|
sequenceDebugSink,
|
|
261
272
|
} = params;
|
|
262
273
|
|
|
@@ -305,7 +316,11 @@ export async function callAgent(params: {
|
|
|
305
316
|
turnId,
|
|
306
317
|
abortSignal,
|
|
307
318
|
currentPage,
|
|
319
|
+
chatSurface,
|
|
320
|
+
adminBaseUrl: adminforth.config.baseUrlSlashed,
|
|
321
|
+
adminPublicOrigin,
|
|
308
322
|
emitToolCallEvent,
|
|
323
|
+
emitAgentEvent,
|
|
309
324
|
},
|
|
310
325
|
});
|
|
311
326
|
}
|
package/agent/systemPrompt.ts
CHANGED
|
@@ -70,12 +70,16 @@ export function buildAgentTurnSystemPrompt(input: {
|
|
|
70
70
|
adminUser: AdminUser;
|
|
71
71
|
usernameField: string;
|
|
72
72
|
userLanguage: DetectedLanguage | null;
|
|
73
|
+
chatSurface?: string;
|
|
73
74
|
}) {
|
|
74
75
|
return [
|
|
75
76
|
input.agentSystemPrompt,
|
|
76
77
|
formatAdminUserPrompt(input.adminUser, input.usernameField),
|
|
78
|
+
input.chatSurface
|
|
79
|
+
? `Current chat surface: ${input.chatSurface}. The user is not in the AdminForth web UI, so tools cannot move their browser. When navigate_user returns a link, send that link to the user.`
|
|
80
|
+
: "",
|
|
77
81
|
formatLanguagePrompt(input.userLanguage),
|
|
78
|
-
].join("\n\n");
|
|
82
|
+
].filter(Boolean).join("\n\n");
|
|
79
83
|
}
|
|
80
84
|
|
|
81
85
|
function formatResources(resources: AdminForthResource[]) {
|
|
@@ -124,6 +128,7 @@ export async function buildAgentSystemPrompt(
|
|
|
124
128
|
"When fetch_tool_schema succeeds, that tool becomes available on the next step.",
|
|
125
129
|
"All admin links must be root-relative and start with '/'.",
|
|
126
130
|
"Build record links as '/resource/{resourceId}/show/{primary key}'. Never use bare 'resource/{resourceId}/show/{primary key}' without the leading slash.",
|
|
131
|
+
"When the user asks to open or show a page in the AdminForth UI, call navigate_user instead of only sending a link.",
|
|
127
132
|
"Try to call as many tools as possible in parallel in one step.",
|
|
128
133
|
];
|
|
129
134
|
|
package/agent/tools/index.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { createFetchToolSchemaTool } from "./fetchToolSchema.js";
|
|
|
4
4
|
import type { ApiBasedTool } from "../../apiBasedTools.js";
|
|
5
5
|
import { createApiTool } from "./apiTool.js";
|
|
6
6
|
import { createGetUserLocationTool } from "./getUserLocation.js";
|
|
7
|
+
import { createNavigateUserTool } from "./navigateUser.js";
|
|
7
8
|
|
|
8
9
|
export const ALWAYS_AVAILABLE_API_TOOL_NAMES = ["get_resource"] as const;
|
|
9
10
|
|
|
@@ -23,6 +24,7 @@ export async function createAgentTools(
|
|
|
23
24
|
return createApiTool(toolName, apiBasedTool);
|
|
24
25
|
}),
|
|
25
26
|
createGetUserLocationTool(),
|
|
27
|
+
createNavigateUserTool(),
|
|
26
28
|
await createFetchSkillTool(customComponentsDir, pluginCustomFolderPaths),
|
|
27
29
|
await createFetchToolSchemaTool(apiBasedTools),
|
|
28
30
|
];
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { tool } from "langchain";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import type { CurrentPageContext } from "./getUserLocation.js";
|
|
4
|
+
import type { AgentEventEmitter } from "../../agentEvents.js";
|
|
5
|
+
|
|
6
|
+
const filterSchema = z.object({
|
|
7
|
+
column: z.string().min(1).describe("Resource column name."),
|
|
8
|
+
operator: z
|
|
9
|
+
.string()
|
|
10
|
+
.min(1)
|
|
11
|
+
.describe("Filter operator suffix, for example eq, gte, lte, like, in."),
|
|
12
|
+
value: z.unknown().describe("Filter value. Dates should be ISO strings."),
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
const navigateUserSchema = z
|
|
16
|
+
.object({
|
|
17
|
+
targetPath: z
|
|
18
|
+
.string()
|
|
19
|
+
.optional()
|
|
20
|
+
.describe(
|
|
21
|
+
"Root-relative AdminForth path to open, with optional query string and hash, for example /resource/adminuser?sort=created_at__desc.",
|
|
22
|
+
),
|
|
23
|
+
resourceId: z
|
|
24
|
+
.string()
|
|
25
|
+
.optional()
|
|
26
|
+
.describe("Resource id to build an AdminForth resource route for."),
|
|
27
|
+
mode: z
|
|
28
|
+
.enum(["list", "show", "edit", "create"])
|
|
29
|
+
.optional()
|
|
30
|
+
.default("list")
|
|
31
|
+
.describe("Resource page mode. Defaults to list."),
|
|
32
|
+
recordId: z
|
|
33
|
+
.union([z.string(), z.number()])
|
|
34
|
+
.optional()
|
|
35
|
+
.describe("Record primary key for show or edit resource pages."),
|
|
36
|
+
filters: z
|
|
37
|
+
.array(filterSchema)
|
|
38
|
+
.optional()
|
|
39
|
+
.describe(
|
|
40
|
+
"List page filters. Each item becomes filter__{column}__{operator}=JSON.stringify(value).",
|
|
41
|
+
),
|
|
42
|
+
sort: z
|
|
43
|
+
.object({
|
|
44
|
+
column: z.string().min(1),
|
|
45
|
+
direction: z.enum(["asc", "desc"]),
|
|
46
|
+
})
|
|
47
|
+
.optional()
|
|
48
|
+
.describe("List page sort. Becomes sort={column}__{direction}."),
|
|
49
|
+
query: z
|
|
50
|
+
.record(z.string(), z.union([z.string(), z.number(), z.boolean()]))
|
|
51
|
+
.optional()
|
|
52
|
+
.describe("Additional query parameters to append to the target URL."),
|
|
53
|
+
})
|
|
54
|
+
.refine((input) => input.targetPath || input.resourceId, {
|
|
55
|
+
message: "Either targetPath or resourceId is required.",
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
function normalizeTargetPath(targetPath: string, currentPage?: CurrentPageContext) {
|
|
59
|
+
const trimmed = targetPath.trim();
|
|
60
|
+
|
|
61
|
+
if (!trimmed) {
|
|
62
|
+
throw new Error("targetPath cannot be empty.");
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const currentOrigin = currentPage?.url ? new URL(currentPage.url).origin : undefined;
|
|
66
|
+
|
|
67
|
+
if (currentOrigin) {
|
|
68
|
+
const targetUrl = new URL(trimmed, currentOrigin);
|
|
69
|
+
|
|
70
|
+
if (targetUrl.origin !== currentOrigin) {
|
|
71
|
+
throw new Error("Only same-origin navigation targets are allowed.");
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return `${targetUrl.pathname}${targetUrl.search}${targetUrl.hash}`;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const fallbackOrigin = "http://adminforth.local";
|
|
78
|
+
const targetUrl = new URL(trimmed, fallbackOrigin);
|
|
79
|
+
|
|
80
|
+
if (targetUrl.origin !== fallbackOrigin) {
|
|
81
|
+
throw new Error("Only relative AdminForth paths are allowed when current origin is unavailable.");
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return `${targetUrl.pathname}${targetUrl.search}${targetUrl.hash}`;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function appendQueryParams(path: string, params: URLSearchParams) {
|
|
88
|
+
const queryString = params.toString();
|
|
89
|
+
|
|
90
|
+
if (!queryString) {
|
|
91
|
+
return path;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const hashIndex = path.indexOf("#");
|
|
95
|
+
const pathWithoutHash = hashIndex === -1 ? path : path.slice(0, hashIndex);
|
|
96
|
+
const hash = hashIndex === -1 ? "" : path.slice(hashIndex);
|
|
97
|
+
const separator = pathWithoutHash.includes("?") ? "&" : "?";
|
|
98
|
+
|
|
99
|
+
return `${pathWithoutHash}${separator}${queryString}${hash}`;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function buildResourcePath(input: z.infer<typeof navigateUserSchema>) {
|
|
103
|
+
if (!input.resourceId) {
|
|
104
|
+
throw new Error("resourceId is required to build a resource route.");
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const resourceId = encodeURIComponent(input.resourceId);
|
|
108
|
+
const mode = input.mode ?? "list";
|
|
109
|
+
|
|
110
|
+
if (mode === "show" || mode === "edit") {
|
|
111
|
+
if (input.recordId === undefined || input.recordId === null) {
|
|
112
|
+
throw new Error(`recordId is required for ${mode} resource pages.`);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return `/resource/${resourceId}/${mode}/${encodeURIComponent(String(input.recordId))}`;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (mode === "create") {
|
|
119
|
+
return `/resource/${resourceId}/create`;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return `/resource/${resourceId}`;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function buildQueryParams(input: z.infer<typeof navigateUserSchema>) {
|
|
126
|
+
const params = new URLSearchParams();
|
|
127
|
+
|
|
128
|
+
for (const filter of input.filters ?? []) {
|
|
129
|
+
params.set(
|
|
130
|
+
`filter__${filter.column}__${filter.operator}`,
|
|
131
|
+
JSON.stringify(filter.value),
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (input.sort) {
|
|
136
|
+
params.set("sort", `${input.sort.column}__${input.sort.direction}`);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
for (const [key, value] of Object.entries(input.query ?? {})) {
|
|
140
|
+
params.set(key, String(value));
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return params;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function buildSurfaceUrl(targetPath: string, adminBaseUrl?: string, adminPublicOrigin?: string) {
|
|
147
|
+
const normalizedBasePath = adminBaseUrl?.replace(/\/+$/, "") ?? "";
|
|
148
|
+
const normalizedTargetPath = targetPath.replace(/^\/+/, "");
|
|
149
|
+
const path = `${normalizedBasePath}/${normalizedTargetPath}`;
|
|
150
|
+
|
|
151
|
+
return adminPublicOrigin ? new URL(path, adminPublicOrigin).toString() : path;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function createNavigateUserTool() {
|
|
155
|
+
return tool(
|
|
156
|
+
async (input, runtime) => {
|
|
157
|
+
const context = runtime.context as {
|
|
158
|
+
currentPage?: CurrentPageContext;
|
|
159
|
+
chatSurface?: string;
|
|
160
|
+
adminBaseUrl?: string;
|
|
161
|
+
adminPublicOrigin?: string;
|
|
162
|
+
emitAgentEvent?: AgentEventEmitter;
|
|
163
|
+
};
|
|
164
|
+
const currentPage = context.currentPage;
|
|
165
|
+
const basePath = input.targetPath
|
|
166
|
+
? normalizeTargetPath(input.targetPath, currentPage)
|
|
167
|
+
: buildResourcePath(input);
|
|
168
|
+
const targetPath = appendQueryParams(basePath, buildQueryParams(input));
|
|
169
|
+
|
|
170
|
+
if (context.chatSurface) {
|
|
171
|
+
const url = buildSurfaceUrl(targetPath, context.adminBaseUrl, context.adminPublicOrigin);
|
|
172
|
+
|
|
173
|
+
return JSON.stringify(
|
|
174
|
+
{
|
|
175
|
+
status: 200,
|
|
176
|
+
action: "link",
|
|
177
|
+
surface: context.chatSurface,
|
|
178
|
+
targetPath,
|
|
179
|
+
url,
|
|
180
|
+
message: `Send this link to the user: ${url}`,
|
|
181
|
+
},
|
|
182
|
+
null,
|
|
183
|
+
2,
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
await context.emitAgentEvent?.({
|
|
188
|
+
type: "open-page",
|
|
189
|
+
targetPath,
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
return JSON.stringify(
|
|
193
|
+
{
|
|
194
|
+
status: 200,
|
|
195
|
+
action: "navigate",
|
|
196
|
+
targetPath,
|
|
197
|
+
message: `Navigation requested to ${targetPath}.`,
|
|
198
|
+
},
|
|
199
|
+
null,
|
|
200
|
+
2,
|
|
201
|
+
);
|
|
202
|
+
},
|
|
203
|
+
{
|
|
204
|
+
name: "navigate_user",
|
|
205
|
+
description:
|
|
206
|
+
"Navigate the user to another AdminForth page. Use this only when the user asks to open, show, go to, or switch to a resource list/detail page, including filtered or sorted resource lists. Or if the user is asked to open something on the left. Do not use this tool in any other case.",
|
|
207
|
+
schema: navigateUserSchema,
|
|
208
|
+
},
|
|
209
|
+
);
|
|
210
|
+
}
|
package/agentEvents.ts
CHANGED
package/agentTurnService.ts
CHANGED
|
@@ -23,6 +23,8 @@ type AgentTurnRunInput = {
|
|
|
23
23
|
modeName?: string | null;
|
|
24
24
|
userTimeZone: string;
|
|
25
25
|
currentPage?: CurrentPageContext;
|
|
26
|
+
chatSurface?: string;
|
|
27
|
+
adminPublicOrigin?: string;
|
|
26
28
|
abortSignal?: AbortSignal;
|
|
27
29
|
adminUser: AdminUser;
|
|
28
30
|
sequenceDebugCollector: ReturnType<typeof createSequenceDebugCollector>;
|
|
@@ -35,6 +37,8 @@ export type RunAndPersistAgentResponseInput = {
|
|
|
35
37
|
modeName?: string | null;
|
|
36
38
|
userTimeZone: string;
|
|
37
39
|
currentPage?: CurrentPageContext;
|
|
40
|
+
chatSurface?: string;
|
|
41
|
+
adminPublicOrigin?: string;
|
|
38
42
|
abortSignal?: AbortSignal;
|
|
39
43
|
adminUser: AdminUser;
|
|
40
44
|
emit?: AgentEventEmitter;
|
|
@@ -118,6 +122,7 @@ export class AgentTurnService {
|
|
|
118
122
|
adminUser: input.adminUser,
|
|
119
123
|
usernameField: adminforth.config.auth!.usernameField,
|
|
120
124
|
userLanguage,
|
|
125
|
+
chatSurface: input.chatSurface,
|
|
121
126
|
});
|
|
122
127
|
const apiBasedTools = buildApiBasedTools(
|
|
123
128
|
adminforth,
|
|
@@ -142,6 +147,8 @@ export class AgentTurnService {
|
|
|
142
147
|
sessionId: input.sessionId,
|
|
143
148
|
turnId: input.turnId,
|
|
144
149
|
currentPage: input.currentPage,
|
|
150
|
+
chatSurface: input.chatSurface,
|
|
151
|
+
adminPublicOrigin: input.adminPublicOrigin,
|
|
145
152
|
userTimeZone: input.userTimeZone,
|
|
146
153
|
abortSignal: input.abortSignal,
|
|
147
154
|
emitToolCallEvent: (event) => {
|
|
@@ -151,6 +158,7 @@ export class AgentTurnService {
|
|
|
151
158
|
data: event,
|
|
152
159
|
});
|
|
153
160
|
},
|
|
161
|
+
emitAgentEvent: input.emit,
|
|
154
162
|
sequenceDebugSink: input.sequenceDebugCollector,
|
|
155
163
|
});
|
|
156
164
|
|
|
@@ -278,6 +286,8 @@ export class AgentTurnService {
|
|
|
278
286
|
modeName: input.modeName,
|
|
279
287
|
userTimeZone: input.userTimeZone,
|
|
280
288
|
currentPage: input.currentPage,
|
|
289
|
+
chatSurface: input.chatSurface,
|
|
290
|
+
adminPublicOrigin: input.adminPublicOrigin,
|
|
281
291
|
abortSignal: input.abortSignal,
|
|
282
292
|
adminUser: input.adminUser,
|
|
283
293
|
sequenceDebugCollector,
|
|
@@ -326,6 +336,8 @@ export class AgentTurnService {
|
|
|
326
336
|
modeName: input.modeName,
|
|
327
337
|
userTimeZone: input.userTimeZone,
|
|
328
338
|
currentPage: input.currentPage,
|
|
339
|
+
chatSurface: input.chatSurface,
|
|
340
|
+
adminPublicOrigin: input.adminPublicOrigin,
|
|
329
341
|
abortSignal: input.abortSignal,
|
|
330
342
|
adminUser: input.adminUser,
|
|
331
343
|
emit: input.emit,
|
|
@@ -408,6 +420,8 @@ export class AgentTurnService {
|
|
|
408
420
|
modeName: input.modeName,
|
|
409
421
|
userTimeZone: input.userTimeZone,
|
|
410
422
|
currentPage: input.currentPage,
|
|
423
|
+
chatSurface: input.chatSurface,
|
|
424
|
+
adminPublicOrigin: input.adminPublicOrigin,
|
|
411
425
|
abortSignal: input.abortSignal,
|
|
412
426
|
adminUser: input.adminUser,
|
|
413
427
|
emit: async (event) => {
|
package/build.log
CHANGED
|
@@ -62,5 +62,5 @@ custom/speech_recognition_frontend/voiceActivityDetection.ts
|
|
|
62
62
|
custom/speech_recognition_frontend/types/
|
|
63
63
|
custom/speech_recognition_frontend/types/voice-activity-detection.d.ts
|
|
64
64
|
|
|
65
|
-
sent 1,
|
|
66
|
-
total size is 1,
|
|
65
|
+
sent 1,670,943 bytes received 921 bytes 3,343,728.00 bytes/sec
|
|
66
|
+
total size is 1,666,790 speedup is 1.00
|
package/chatSurfaceService.ts
CHANGED
|
@@ -184,6 +184,9 @@ export class ChatSurfaceService {
|
|
|
184
184
|
options?: { emitDone?: boolean },
|
|
185
185
|
) {
|
|
186
186
|
const emitDone = options?.emitDone ?? true;
|
|
187
|
+
const adminPublicOrigin = typeof incoming.metadata?.adminPublicOrigin === "string"
|
|
188
|
+
? incoming.metadata.adminPublicOrigin
|
|
189
|
+
: undefined;
|
|
187
190
|
const sessionId = await this.sessionStore.getOrCreateChatSurfaceSession(
|
|
188
191
|
{ ...incoming, prompt },
|
|
189
192
|
adminUser,
|
|
@@ -195,6 +198,8 @@ export class ChatSurfaceService {
|
|
|
195
198
|
sessionId,
|
|
196
199
|
modeName: incoming.modeName,
|
|
197
200
|
userTimeZone: incoming.userTimeZone ?? "UTC",
|
|
201
|
+
chatSurface: incoming.surface,
|
|
202
|
+
adminPublicOrigin,
|
|
198
203
|
adminUser,
|
|
199
204
|
emit: this.createEventEmitter(sink),
|
|
200
205
|
failureLogMessage: `Agent ${incoming.surface} surface response failed`,
|
|
@@ -208,6 +213,8 @@ export class ChatSurfaceService {
|
|
|
208
213
|
sessionId,
|
|
209
214
|
modeName: incoming.modeName,
|
|
210
215
|
userTimeZone: incoming.userTimeZone ?? "UTC",
|
|
216
|
+
chatSurface: incoming.surface,
|
|
217
|
+
adminPublicOrigin,
|
|
211
218
|
adminUser,
|
|
212
219
|
emit: this.createEventEmitter(sink),
|
|
213
220
|
failureLogMessage: `Agent ${incoming.surface} surface response failed`,
|
|
@@ -13,11 +13,13 @@ type AgentImportMeta = ImportMeta & {
|
|
|
13
13
|
type CreateAgentChatManagerOptions = {
|
|
14
14
|
lastMessage: Ref<string>;
|
|
15
15
|
activeModeName: Ref<string | null>;
|
|
16
|
+
onOpenPage: (targetPath: string) => void;
|
|
16
17
|
};
|
|
17
18
|
|
|
18
19
|
export function createAgentChatManager({
|
|
19
20
|
lastMessage,
|
|
20
21
|
activeModeName,
|
|
22
|
+
onOpenPage,
|
|
21
23
|
}: CreateAgentChatManagerOptions) {
|
|
22
24
|
const chats = new Map<string, Chat<any>>();
|
|
23
25
|
const currentChat = shallowRef<Chat<any> | null>();
|
|
@@ -52,6 +54,11 @@ export function createAgentChatManager({
|
|
|
52
54
|
onError(error: unknown) {
|
|
53
55
|
console.error('Chat error:', error);
|
|
54
56
|
},
|
|
57
|
+
onData(dataPart: any) {
|
|
58
|
+
if (dataPart?.type === 'data-open-page' && typeof dataPart.data?.targetPath === 'string') {
|
|
59
|
+
onOpenPage(dataPart.data.targetPath);
|
|
60
|
+
}
|
|
61
|
+
},
|
|
55
62
|
});
|
|
56
63
|
chats.set(sessionId, newChat);
|
|
57
64
|
currentChat.value = newChat;
|
|
@@ -67,4 +74,4 @@ export function createAgentChatManager({
|
|
|
67
74
|
setCurrentChat,
|
|
68
75
|
abortCurrentChatRequest,
|
|
69
76
|
};
|
|
70
|
-
}
|
|
77
|
+
}
|
|
@@ -187,6 +187,11 @@ export const useAgentAudio = defineStore('agentAudio', () => {
|
|
|
187
187
|
playStandByAudio();
|
|
188
188
|
}
|
|
189
189
|
agentStore.addDataToolCallMessage(event.data);
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (event.type === 'open-page') {
|
|
194
|
+
agentStore.openAgentPage(event.data.targetPath);
|
|
190
195
|
}
|
|
191
196
|
}
|
|
192
197
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { defineStore } from 'pinia';
|
|
2
2
|
import { IAgentSession, ISessionsListItem } from '../types';
|
|
3
3
|
import { ref, nextTick, computed, watch, onMounted } from 'vue';
|
|
4
|
+
import { useRouter } from 'vue-router';
|
|
4
5
|
import { useAdminforth } from '@/adminforth';
|
|
5
6
|
import { useCoreStore } from '@/stores/core';
|
|
6
7
|
import { useAgentTransitions } from './useAgentTransitions';
|
|
@@ -28,6 +29,7 @@ export const useAgentStore = defineStore('agent', () => {
|
|
|
28
29
|
const adminforth = useAdminforth();
|
|
29
30
|
const isChatOpen = ref(false);
|
|
30
31
|
const isSessionHistoryOpen = ref(false);
|
|
32
|
+
const router = useRouter();
|
|
31
33
|
const textInput = ref<HTMLTextAreaElement | null>(null);
|
|
32
34
|
const userMessageInput = ref();
|
|
33
35
|
const trimmedUserMessage = computed(() => userMessageInput.value ? userMessageInput.value.trim() : '');
|
|
@@ -54,6 +56,7 @@ export const useAgentStore = defineStore('agent', () => {
|
|
|
54
56
|
} = createAgentChatManager({
|
|
55
57
|
lastMessage,
|
|
56
58
|
activeModeName,
|
|
59
|
+
onOpenPage: openAgentPage,
|
|
57
60
|
});
|
|
58
61
|
const {
|
|
59
62
|
userMessagePlaceholder,
|
|
@@ -324,6 +327,45 @@ export const useAgentStore = defineStore('agent', () => {
|
|
|
324
327
|
addSystemMessage(RESERVED_SYSTEM_MESSAGE_CONTENT.AGENT_RESPONSE_ABORTED);
|
|
325
328
|
}
|
|
326
329
|
|
|
330
|
+
function resolveInternalRoute(href: string): string | null {
|
|
331
|
+
if (href.startsWith('#')) {
|
|
332
|
+
return `${window.location.pathname}${window.location.search}${href}`;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
if (href.startsWith('//')) {
|
|
336
|
+
return null;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const isAbsoluteWithScheme = /^[a-zA-Z][a-zA-Z\d+\-.]*:/.test(href);
|
|
340
|
+
const baseUrl = isAbsoluteWithScheme
|
|
341
|
+
? undefined
|
|
342
|
+
: `${window.location.origin}/`;
|
|
343
|
+
const resolvedUrl = new URL(href, baseUrl ?? window.location.href);
|
|
344
|
+
|
|
345
|
+
if (resolvedUrl.origin !== window.location.origin) {
|
|
346
|
+
return null;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
return `${resolvedUrl.pathname}${resolvedUrl.search}${resolvedUrl.hash}`;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function openAgentPage(targetPath: string) {
|
|
353
|
+
const internalRoute = resolveInternalRoute(targetPath);
|
|
354
|
+
|
|
355
|
+
if (internalRoute === null) {
|
|
356
|
+
console.warn('Ignoring external agent navigation target:', targetPath);
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
if (isFullScreen.value && !coreStore.isMobile) {
|
|
361
|
+
setFullScreen(false);
|
|
362
|
+
} else if (coreStore.isMobile) {
|
|
363
|
+
setIsChatOpen(false);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
void router.push(internalRoute);
|
|
367
|
+
}
|
|
368
|
+
|
|
327
369
|
return {
|
|
328
370
|
//_________-Sessions management-_____________
|
|
329
371
|
activeSessionId,
|
|
@@ -374,6 +416,7 @@ export const useAgentStore = defineStore('agent', () => {
|
|
|
374
416
|
addAgentMessage,
|
|
375
417
|
addUserMessage,
|
|
376
418
|
addDataToolCallMessage,
|
|
419
|
+
openAgentPage,
|
|
377
420
|
setCurrentChatStatus,
|
|
378
421
|
updateLastAgentMessage
|
|
379
422
|
}
|
|
@@ -24,6 +24,8 @@ If several rows look similar, do not guess which one is "the same" record. Show
|
|
|
24
24
|
|
|
25
25
|
For long texts show only several first words and add "..." at the end (only if user did not request this field specifically).
|
|
26
26
|
|
|
27
|
+
Never show information about more than 10 records in one message. If a tool returns more than 10 relevant records, summarize the total count if known, show only the 10 most relevant records, and offer to narrow filters, sort differently, or continue with the next 10.
|
|
28
|
+
|
|
27
29
|
Also when you communicate with user about record, add related link to this record. Build it as `{ADMIN_BASE_PATH}resource/{resourceId}/show/{primary key}`. Use _label from `get_resource_data` as anchor text for link (use markdown link). Links should always be relative paths and must start with `ADMIN_BASE_PATH`. Do not add an extra slash after `ADMIN_BASE_PATH`.
|
|
28
30
|
|
|
29
31
|
Before sending the link, verify that the `resourceId`, `{primary key}`, `_label`, and shown fields come from the same exact returned row.
|