@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.
Files changed (55) hide show
  1. package/agent/languageDetect.ts +0 -8
  2. package/agent/simpleAgent.ts +5 -5
  3. package/agent/systemPrompt.ts +35 -4
  4. package/agent/toolCallEvents.ts +31 -2
  5. package/agent/tools/apiTool.ts +1 -1
  6. package/agentResponseEvents.ts +197 -0
  7. package/apiBasedTools.ts +118 -284
  8. package/build.log +12 -2
  9. package/custom/ChatSurface.vue +31 -21
  10. package/custom/composables/agentAudio/agent-processing.mp3 +0 -0
  11. package/custom/composables/agentStore/constants.ts +8 -1
  12. package/custom/composables/agentStore/useAgentSessions.ts +88 -13
  13. package/custom/composables/useAgentAudio.ts +392 -0
  14. package/custom/composables/useAgentStore.ts +52 -5
  15. package/custom/conversation_area/ConversationArea.vue +1 -1
  16. package/custom/conversation_area/MessageRenderer.vue +12 -1
  17. package/custom/conversation_area/SystemMessageRenderer.vue +28 -0
  18. package/custom/conversation_area/TextRenderer.vue +4 -3
  19. package/custom/conversation_area/ToolRenderer.vue +1 -1
  20. package/custom/package.json +2 -1
  21. package/custom/pnpm-lock.yaml +29 -0
  22. package/custom/speech_recognition_frontend/AudioLines.vue +97 -0
  23. package/custom/speech_recognition_frontend/MicrophoneButon.vue +157 -0
  24. package/custom/speech_recognition_frontend/types/voice-activity-detection.d.ts +22 -0
  25. package/custom/speech_recognition_frontend/voiceActivityDetection.ts +151 -0
  26. package/custom/types.ts +52 -2
  27. package/dist/agent/languageDetect.js +0 -6
  28. package/dist/agent/simpleAgent.js +4 -3
  29. package/dist/agent/systemPrompt.js +24 -3
  30. package/dist/agent/toolCallEvents.js +24 -2
  31. package/dist/agent/tools/apiTool.js +1 -1
  32. package/dist/agentResponseEvents.js +141 -0
  33. package/dist/apiBasedTools.js +95 -211
  34. package/dist/custom/ChatSurface.vue +31 -21
  35. package/dist/custom/composables/agentAudio/agent-processing.mp3 +0 -0
  36. package/dist/custom/composables/agentStore/constants.ts +8 -1
  37. package/dist/custom/composables/agentStore/useAgentSessions.ts +88 -13
  38. package/dist/custom/composables/useAgentAudio.ts +392 -0
  39. package/dist/custom/composables/useAgentStore.ts +52 -5
  40. package/dist/custom/conversation_area/ConversationArea.vue +1 -1
  41. package/dist/custom/conversation_area/MessageRenderer.vue +12 -1
  42. package/dist/custom/conversation_area/SystemMessageRenderer.vue +28 -0
  43. package/dist/custom/conversation_area/TextRenderer.vue +4 -3
  44. package/dist/custom/conversation_area/ToolRenderer.vue +1 -1
  45. package/dist/custom/package.json +2 -1
  46. package/dist/custom/pnpm-lock.yaml +29 -0
  47. package/dist/custom/speech_recognition_frontend/AudioLines.vue +97 -0
  48. package/dist/custom/speech_recognition_frontend/MicrophoneButon.vue +157 -0
  49. package/dist/custom/speech_recognition_frontend/types/voice-activity-detection.d.ts +22 -0
  50. package/dist/custom/speech_recognition_frontend/voiceActivityDetection.ts +151 -0
  51. package/dist/custom/types.ts +52 -2
  52. package/dist/index.js +290 -400
  53. package/index.ts +318 -492
  54. package/package.json +4 -2
  55. package/types.ts +1 -1
@@ -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;
@@ -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
  });
@@ -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
  : "",
@@ -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(serializeUnknownError(error)).trimEnd(),
131
+ error: YAML.stringify(serializeErrorForDebug(error)).trimEnd(),
103
132
  });
104
133
  },
105
134
  };
@@ -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
- httpExtra: runtime.context.httpExtra,
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
+ }