@adminforth/agent 1.36.0 → 1.38.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/agent/languageDetect.ts +0 -8
- package/agent/simpleAgent.ts +5 -5
- package/agent/systemPrompt.ts +35 -4
- package/agent/toolCallEvents.ts +31 -2
- package/agent/tools/apiTool.ts +1 -1
- package/agentResponseEvents.ts +197 -0
- package/apiBasedTools.ts +118 -284
- package/build.log +12 -2
- package/custom/ChatSurface.vue +31 -21
- package/custom/composables/agentAudio/agent-processing.mp3 +0 -0
- package/custom/composables/agentStore/constants.ts +8 -1
- package/custom/composables/agentStore/useAgentSessions.ts +88 -13
- package/custom/composables/useAgentAudio.ts +392 -0
- package/custom/composables/useAgentStore.ts +52 -5
- package/custom/conversation_area/ConversationArea.vue +1 -1
- package/custom/conversation_area/MessageRenderer.vue +12 -1
- package/custom/conversation_area/SystemMessageRenderer.vue +28 -0
- package/custom/conversation_area/TextRenderer.vue +4 -3
- package/custom/conversation_area/ToolRenderer.vue +1 -1
- package/custom/package.json +2 -1
- package/custom/pnpm-lock.yaml +29 -0
- package/custom/speech_recognition_frontend/AudioLines.vue +97 -0
- package/custom/speech_recognition_frontend/MicrophoneButon.vue +157 -0
- package/custom/speech_recognition_frontend/types/voice-activity-detection.d.ts +22 -0
- package/custom/speech_recognition_frontend/voiceActivityDetection.ts +151 -0
- package/custom/types.ts +52 -2
- package/dist/agent/languageDetect.js +0 -6
- package/dist/agent/simpleAgent.js +4 -3
- package/dist/agent/systemPrompt.js +24 -3
- package/dist/agent/toolCallEvents.js +24 -2
- package/dist/agent/tools/apiTool.js +1 -1
- package/dist/agentResponseEvents.js +141 -0
- package/dist/apiBasedTools.js +95 -211
- package/dist/custom/ChatSurface.vue +31 -21
- package/dist/custom/composables/agentAudio/agent-processing.mp3 +0 -0
- package/dist/custom/composables/agentStore/constants.ts +8 -1
- package/dist/custom/composables/agentStore/useAgentSessions.ts +88 -13
- package/dist/custom/composables/useAgentAudio.ts +392 -0
- package/dist/custom/composables/useAgentStore.ts +52 -5
- package/dist/custom/conversation_area/ConversationArea.vue +1 -1
- package/dist/custom/conversation_area/MessageRenderer.vue +12 -1
- package/dist/custom/conversation_area/SystemMessageRenderer.vue +28 -0
- package/dist/custom/conversation_area/TextRenderer.vue +4 -3
- package/dist/custom/conversation_area/ToolRenderer.vue +1 -1
- package/dist/custom/package.json +2 -1
- package/dist/custom/pnpm-lock.yaml +29 -0
- package/dist/custom/speech_recognition_frontend/AudioLines.vue +97 -0
- package/dist/custom/speech_recognition_frontend/MicrophoneButon.vue +157 -0
- package/dist/custom/speech_recognition_frontend/types/voice-activity-detection.d.ts +22 -0
- package/dist/custom/speech_recognition_frontend/voiceActivityDetection.ts +151 -0
- package/dist/custom/types.ts +52 -2
- package/dist/index.js +290 -400
- package/index.ts +318 -492
- package/package.json +4 -2
- package/types.ts +1 -1
package/agent/languageDetect.ts
CHANGED
|
@@ -26,14 +26,6 @@ const USER_LANGUAGE_OUTPUT_SCHEMA = {
|
|
|
26
26
|
},
|
|
27
27
|
} as const;
|
|
28
28
|
|
|
29
|
-
export function formatLanguagePrompt(language: UserLanguage | null) {
|
|
30
|
-
if (!language) {
|
|
31
|
-
return "Respond in the user's language.";
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
return `Respond in ${language.language} (${language.code}).`;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
29
|
function parseUserLanguage(content: string | undefined): UserLanguage | null {
|
|
38
30
|
if (!content) {
|
|
39
31
|
return null;
|
package/agent/simpleAgent.ts
CHANGED
|
@@ -4,7 +4,6 @@ import {
|
|
|
4
4
|
logger,
|
|
5
5
|
type AdminUser,
|
|
6
6
|
type CompletionAdapter,
|
|
7
|
-
type HttpExtra,
|
|
8
7
|
type IAdminForth,
|
|
9
8
|
} from "adminforth";
|
|
10
9
|
import { BaseCallbackHandler } from "@langchain/core/callbacks/base";
|
|
@@ -26,8 +25,8 @@ export const contextSchema = z.object({
|
|
|
26
25
|
userTimeZone: z.string(),
|
|
27
26
|
sessionId: z.string(),
|
|
28
27
|
turnId: z.string(),
|
|
28
|
+
abortSignal: z.custom<AbortSignal>().optional(),
|
|
29
29
|
currentPage: z.custom<CurrentPageContext>().optional(),
|
|
30
|
-
httpExtra: z.custom<Partial<HttpExtra>>().optional(),
|
|
31
30
|
emitToolCallEvent: z.custom<ToolCallEventSink>(),
|
|
32
31
|
});
|
|
33
32
|
|
|
@@ -234,8 +233,8 @@ export async function callAgent(params: {
|
|
|
234
233
|
sessionId: string;
|
|
235
234
|
turnId: string;
|
|
236
235
|
currentPage?: CurrentPageContext;
|
|
237
|
-
httpExtra?: Partial<HttpExtra>;
|
|
238
236
|
userTimeZone: string;
|
|
237
|
+
abortSignal?: AbortSignal;
|
|
239
238
|
emitToolCallEvent: ToolCallEventSink;
|
|
240
239
|
sequenceDebugSink: SequenceDebugModelCallSink;
|
|
241
240
|
}) {
|
|
@@ -253,8 +252,8 @@ export async function callAgent(params: {
|
|
|
253
252
|
sessionId,
|
|
254
253
|
turnId,
|
|
255
254
|
currentPage,
|
|
256
|
-
httpExtra,
|
|
257
255
|
userTimeZone,
|
|
256
|
+
abortSignal,
|
|
258
257
|
emitToolCallEvent,
|
|
259
258
|
sequenceDebugSink,
|
|
260
259
|
} = params;
|
|
@@ -289,6 +288,7 @@ export async function callAgent(params: {
|
|
|
289
288
|
streamMode: "messages",
|
|
290
289
|
recursionLimit: 100,
|
|
291
290
|
callbacks: [createAgentLlmMetricsLogger()],
|
|
291
|
+
signal: abortSignal,
|
|
292
292
|
configurable: {
|
|
293
293
|
thread_id: sessionId,
|
|
294
294
|
},
|
|
@@ -297,8 +297,8 @@ export async function callAgent(params: {
|
|
|
297
297
|
userTimeZone,
|
|
298
298
|
sessionId,
|
|
299
299
|
turnId,
|
|
300
|
+
abortSignal,
|
|
300
301
|
currentPage,
|
|
301
|
-
httpExtra,
|
|
302
302
|
emitToolCallEvent,
|
|
303
303
|
},
|
|
304
304
|
});
|
package/agent/systemPrompt.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import type { AdminForthResource, IAdminForth } from "adminforth";
|
|
1
|
+
import type { AdminForthResource, AdminUser, IAdminForth } from "adminforth";
|
|
2
|
+
import type { UserLanguage } from "./languageDetect.js";
|
|
2
3
|
import {
|
|
3
4
|
listBundledSkillManifests,
|
|
4
5
|
listProjectSkillManifests,
|
|
5
6
|
type AgentSkillManifest,
|
|
6
7
|
} from "./skills/registry.js";
|
|
7
|
-
import { ALWAYS_AVAILABLE_API_TOOL_NAMES } from "./tools/index.js";
|
|
8
8
|
|
|
9
9
|
export const DEFAULT_AGENT_SYSTEM_PROMPT = [
|
|
10
10
|
"You are helpful AI Assistant for Admin Panel.",
|
|
@@ -46,6 +46,39 @@ export function appendCustomSystemPrompt(
|
|
|
46
46
|
return `${systemPrompt}\n\n${normalizedCustomSystemPrompt}`;
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
function formatLanguagePrompt(language: UserLanguage | null) {
|
|
50
|
+
if (!language) {
|
|
51
|
+
return "Respond in the user's language.";
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return `Respond in ${language.language} (${language.code}).`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function formatAdminUserPrompt(adminUser: AdminUser, usernameField: string) {
|
|
58
|
+
const adminUserContext = {
|
|
59
|
+
id: adminUser.pk,
|
|
60
|
+
email: adminUser.dbUser[usernameField],
|
|
61
|
+
};
|
|
62
|
+
return [
|
|
63
|
+
"Current admin user context:",
|
|
64
|
+
JSON.stringify(adminUserContext, null, 2),
|
|
65
|
+
"Use this admin user email when the user asks to send information to themselves, the current admin, or the logged-in user.",
|
|
66
|
+
].join("\n");
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function buildAgentTurnSystemPrompt(input: {
|
|
70
|
+
agentSystemPrompt: string;
|
|
71
|
+
adminUser: AdminUser;
|
|
72
|
+
usernameField: string;
|
|
73
|
+
userLanguage: UserLanguage | null;
|
|
74
|
+
}) {
|
|
75
|
+
return [
|
|
76
|
+
input.agentSystemPrompt,
|
|
77
|
+
formatAdminUserPrompt(input.adminUser, input.usernameField),
|
|
78
|
+
formatLanguagePrompt(input.userLanguage),
|
|
79
|
+
].join("\n\n");
|
|
80
|
+
}
|
|
81
|
+
|
|
49
82
|
function formatResources(resources: AdminForthResource[]) {
|
|
50
83
|
return resources
|
|
51
84
|
.map((resource) => `- resourceId: ${resource.resourceId}\n label: ${resource.label}`)
|
|
@@ -66,7 +99,6 @@ export async function buildAgentSystemPrompt(
|
|
|
66
99
|
listProjectSkillManifests(adminforth.config.customization.customComponentsDir),
|
|
67
100
|
listBundledSkillManifests(),
|
|
68
101
|
]);
|
|
69
|
-
const alwaysAvailableTools = ALWAYS_AVAILABLE_API_TOOL_NAMES.join(", ");
|
|
70
102
|
const adminBasePath = adminforth.config.baseUrlSlashed;
|
|
71
103
|
const hiddenResourceIdSet = new Set(hiddenResourceIds);
|
|
72
104
|
const visibleResources = adminforth.config.resources.filter(
|
|
@@ -76,7 +108,6 @@ export async function buildAgentSystemPrompt(
|
|
|
76
108
|
DEFAULT_AGENT_SYSTEM_PROMPT,
|
|
77
109
|
`ADMIN_BASE_PATH: ${adminBasePath}`,
|
|
78
110
|
`List of resources:\n${formatResources(visibleResources)}`,
|
|
79
|
-
`You have always-available base tools: ${alwaysAvailableTools}.`,
|
|
80
111
|
primarySkills.length > 0
|
|
81
112
|
? `You have primary skills set:\n${formatSkills(primarySkills, "skill_name")}`
|
|
82
113
|
: "",
|
package/agent/toolCallEvents.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { randomUUID } from "crypto";
|
|
2
2
|
import YAML from "yaml";
|
|
3
|
-
import { serializeUnknownError } from "../apiBasedTools.js";
|
|
4
3
|
|
|
5
4
|
export type ToolCallEvent =
|
|
6
5
|
| {
|
|
@@ -61,6 +60,36 @@ function sanitizeToolCallOutputForDebug(output: unknown) {
|
|
|
61
60
|
);
|
|
62
61
|
}
|
|
63
62
|
|
|
63
|
+
function serializeErrorForDebug(error: unknown): unknown {
|
|
64
|
+
if (!(error instanceof Error)) {
|
|
65
|
+
return error;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const errorWithCause = error as Error & { cause?: unknown };
|
|
69
|
+
const serialized: Record<string, unknown> = {
|
|
70
|
+
name: error.name,
|
|
71
|
+
message: error.message,
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
if (error.stack) {
|
|
75
|
+
serialized.stack = error.stack;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (errorWithCause.cause !== undefined) {
|
|
79
|
+
serialized.cause = serializeErrorForDebug(errorWithCause.cause);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
for (const key of Object.getOwnPropertyNames(error)) {
|
|
83
|
+
if (key in serialized) {
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
serialized[key] = (error as unknown as Record<string, unknown>)[key];
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return serialized;
|
|
91
|
+
}
|
|
92
|
+
|
|
64
93
|
export function createToolCallTracker(params: {
|
|
65
94
|
emit: ToolCallEventSink;
|
|
66
95
|
toolCallId?: string;
|
|
@@ -99,7 +128,7 @@ export function createToolCallTracker(params: {
|
|
|
99
128
|
phase: "end",
|
|
100
129
|
durationMs: Date.now() - startedAt,
|
|
101
130
|
output: null,
|
|
102
|
-
error: YAML.stringify(
|
|
131
|
+
error: YAML.stringify(serializeErrorForDebug(error)).trimEnd(),
|
|
103
132
|
});
|
|
104
133
|
},
|
|
105
134
|
};
|
package/agent/tools/apiTool.ts
CHANGED
|
@@ -51,7 +51,7 @@ export function createApiTool(toolName: string, apiBasedTool: ApiBasedTool) {
|
|
|
51
51
|
const normalizedInput = (input ?? {}) as Record<string, unknown>;
|
|
52
52
|
return apiBasedTool.call({
|
|
53
53
|
adminUser: runtime.context.adminUser,
|
|
54
|
-
|
|
54
|
+
abortSignal: runtime.context.abortSignal,
|
|
55
55
|
inputs: normalizedInput,
|
|
56
56
|
userTimeZone: runtime.context.userTimeZone,
|
|
57
57
|
});
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { randomUUID } from "crypto";
|
|
2
|
+
|
|
3
|
+
import type { ToolCallEvent } from "./agent/toolCallEvents.js";
|
|
4
|
+
|
|
5
|
+
type AgentEventStreamResponse = {
|
|
6
|
+
writeHead: (statusCode: number, headers: Record<string, string>) => void;
|
|
7
|
+
write: (chunk: string) => unknown;
|
|
8
|
+
end: () => unknown;
|
|
9
|
+
writableEnded: boolean;
|
|
10
|
+
destroyed: boolean;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
type AgentEventStreamOptions = {
|
|
14
|
+
vercelAiUiMessageStream?: boolean;
|
|
15
|
+
closeActiveBlockOnToolStart?: boolean;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export function createAgentEventStream(
|
|
19
|
+
res: AgentEventStreamResponse,
|
|
20
|
+
options: AgentEventStreamOptions = {},
|
|
21
|
+
) {
|
|
22
|
+
let isStreamClosed = false;
|
|
23
|
+
let activeBlock: { type: "text" | "reasoning"; id: string } | null = null;
|
|
24
|
+
|
|
25
|
+
res.writeHead(200, {
|
|
26
|
+
"Content-Type": "text/event-stream",
|
|
27
|
+
"Cache-Control": "no-cache",
|
|
28
|
+
"Connection": "keep-alive",
|
|
29
|
+
...(options.vercelAiUiMessageStream
|
|
30
|
+
? { "x-vercel-ai-ui-message-stream": "v1" }
|
|
31
|
+
: {}),
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
const stream = {
|
|
35
|
+
send(obj: unknown) {
|
|
36
|
+
if (isStreamClosed || res.writableEnded || res.destroyed) {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
res.write(`data: ${JSON.stringify(obj)}\n\n`);
|
|
41
|
+
},
|
|
42
|
+
|
|
43
|
+
endActiveBlock() {
|
|
44
|
+
if (!activeBlock) {
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
stream.send({
|
|
49
|
+
type: `${activeBlock.type}-end`,
|
|
50
|
+
id: activeBlock.id,
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
activeBlock = null;
|
|
54
|
+
},
|
|
55
|
+
|
|
56
|
+
startBlock(type: "text" | "reasoning") {
|
|
57
|
+
if (activeBlock?.type === type) {
|
|
58
|
+
return activeBlock.id;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
stream.endActiveBlock();
|
|
62
|
+
|
|
63
|
+
const id = randomUUID();
|
|
64
|
+
activeBlock = { type, id };
|
|
65
|
+
|
|
66
|
+
stream.send({
|
|
67
|
+
type: `${type}-start`,
|
|
68
|
+
id,
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
return id;
|
|
72
|
+
},
|
|
73
|
+
|
|
74
|
+
start(messageId: string) {
|
|
75
|
+
stream.send({
|
|
76
|
+
type: "start",
|
|
77
|
+
messageId,
|
|
78
|
+
});
|
|
79
|
+
},
|
|
80
|
+
|
|
81
|
+
textDelta(delta: string) {
|
|
82
|
+
const textId = stream.startBlock("text");
|
|
83
|
+
stream.send({
|
|
84
|
+
type: "text-delta",
|
|
85
|
+
id: textId,
|
|
86
|
+
delta,
|
|
87
|
+
});
|
|
88
|
+
},
|
|
89
|
+
|
|
90
|
+
reasoningDelta(delta: string) {
|
|
91
|
+
const reasoningId = stream.startBlock("reasoning");
|
|
92
|
+
stream.send({
|
|
93
|
+
type: "reasoning-delta",
|
|
94
|
+
id: reasoningId,
|
|
95
|
+
delta,
|
|
96
|
+
});
|
|
97
|
+
},
|
|
98
|
+
|
|
99
|
+
toolCall(event: ToolCallEvent) {
|
|
100
|
+
if (options.closeActiveBlockOnToolStart && event.phase === "start") {
|
|
101
|
+
stream.endActiveBlock();
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
stream.send({
|
|
105
|
+
type: "data-tool-call",
|
|
106
|
+
data: event,
|
|
107
|
+
});
|
|
108
|
+
},
|
|
109
|
+
|
|
110
|
+
transcript(text: string, language?: string) {
|
|
111
|
+
stream.send({
|
|
112
|
+
type: "transcript",
|
|
113
|
+
data: {
|
|
114
|
+
text,
|
|
115
|
+
language,
|
|
116
|
+
},
|
|
117
|
+
});
|
|
118
|
+
},
|
|
119
|
+
|
|
120
|
+
response(text: string, sessionId: string, turnId: string) {
|
|
121
|
+
stream.send({
|
|
122
|
+
type: "response",
|
|
123
|
+
data: {
|
|
124
|
+
text,
|
|
125
|
+
sessionId,
|
|
126
|
+
turnId,
|
|
127
|
+
},
|
|
128
|
+
});
|
|
129
|
+
},
|
|
130
|
+
|
|
131
|
+
speechResponse(
|
|
132
|
+
transcript: { text: string; language?: string },
|
|
133
|
+
response: { text: string },
|
|
134
|
+
sessionId: string,
|
|
135
|
+
turnId: string,
|
|
136
|
+
) {
|
|
137
|
+
stream.send({
|
|
138
|
+
type: "speech-response",
|
|
139
|
+
data: {
|
|
140
|
+
transcript,
|
|
141
|
+
response,
|
|
142
|
+
sessionId,
|
|
143
|
+
turnId,
|
|
144
|
+
},
|
|
145
|
+
});
|
|
146
|
+
},
|
|
147
|
+
|
|
148
|
+
audioStart(mimeType: string, format: string) {
|
|
149
|
+
stream.send({
|
|
150
|
+
type: "audio-start",
|
|
151
|
+
data: {
|
|
152
|
+
mimeType,
|
|
153
|
+
format,
|
|
154
|
+
},
|
|
155
|
+
});
|
|
156
|
+
},
|
|
157
|
+
|
|
158
|
+
audioDelta(value: Uint8Array) {
|
|
159
|
+
stream.send({
|
|
160
|
+
type: "audio-delta",
|
|
161
|
+
data: {
|
|
162
|
+
base64: Buffer.from(value).toString("base64"),
|
|
163
|
+
},
|
|
164
|
+
});
|
|
165
|
+
},
|
|
166
|
+
|
|
167
|
+
audioDone() {
|
|
168
|
+
stream.send({
|
|
169
|
+
type: "audio-done",
|
|
170
|
+
});
|
|
171
|
+
},
|
|
172
|
+
|
|
173
|
+
error(error: string) {
|
|
174
|
+
stream.send({
|
|
175
|
+
type: "error",
|
|
176
|
+
error,
|
|
177
|
+
});
|
|
178
|
+
},
|
|
179
|
+
|
|
180
|
+
end() {
|
|
181
|
+
if (isStreamClosed || res.writableEnded || res.destroyed) {
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
stream.endActiveBlock();
|
|
186
|
+
stream.send({
|
|
187
|
+
type: "finish",
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
res.write("data: [DONE]\n\n");
|
|
191
|
+
isStreamClosed = true;
|
|
192
|
+
res.end();
|
|
193
|
+
},
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
return stream;
|
|
197
|
+
}
|