Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
3bd503c
chore: seech recognirion progress
yaroslav8765 May 1, 2026
ef1a3ac
Merge branch 'main' of https://github.com/devforth/adminforth-agent i…
yaroslav8765 May 1, 2026
07aa3bc
chore: progress voice recognition[2]
yaroslav8765 May 4, 2026
37d1310
Merge branch 'main' of https://github.com/devforth/adminforth-agent i…
yaroslav8765 May 4, 2026
9f46aa0
fix: rebuild
yaroslav8765 May 4, 2026
225f095
fix: rebuild
yaroslav8765 May 4, 2026
4da4af9
fix: rebuild
yaroslav8765 May 4, 2026
1bf16f0
chore: add audio upload handling in speech response endpoint
yaroslav8765 May 4, 2026
ac6e582
chore: sync agent store with main branch
yaroslav8765 May 4, 2026
3547889
Merge branch 'main' of https://github.com/devforth/adminforth-agent i…
yaroslav8765 May 4, 2026
19855c8
chore: finish merge with main branch
yaroslav8765 May 4, 2026
6da5490
feat: add optional abortSignal parameter to contextSchema and callAge…
NoOne7135 May 4, 2026
98cca01
refactor: remove audio properties from SpeechResponseRequestBody and …
NoOne7135 May 4, 2026
f737d24
feat: add abortSignal handling to API tool calls and agent response s…
NoOne7135 May 4, 2026
0625d7e
feat: add emitToolCallEvent function to handle tool call events in ru…
NoOne7135 May 4, 2026
1080946
chore: progress voice recognition[3]
yaroslav8765 May 4, 2026
efb0fdf
fix: update sessionId handling in sendAudioToServerAndHandleResponse …
yaroslav8765 May 4, 2026
7563b3a
feat: enhance audio handling by adding currentPage context and improv…
NoOne7135 May 4, 2026
acd27f8
chore: progress voice recognition[4]
yaroslav8765 May 4, 2026
80c06fe
Merge branch 'add-frontend-speech-recognition' of https://github.com/…
yaroslav8765 May 4, 2026
ae175fd
feat: implement agent event stream for real-time response handling
NoOne7135 May 4, 2026
6d0de23
refactor: streamline audio handling and sessionId management in agent…
NoOne7135 May 4, 2026
2ac664f
chore: progress voice recognition[5]
yaroslav8765 May 4, 2026
45dab19
chore: progress voice recognition[6]
yaroslav8765 May 4, 2026
4f2d413
Merge branch 'add-frontend-speech-recognition' of https://github.com/…
yaroslav8765 May 4, 2026
a960954
fix: correct stop of generation when user click on microphone second …
yaroslav8765 May 5, 2026
3cc00ab
fix: update button width for audio chat mode in MicrophoneButon.vue
yaroslav8765 May 5, 2026
a60a4bd
chore: update button positioning and visibility based on audio chat m…
yaroslav8765 May 5, 2026
471f931
fix: add ability to start audio chat from the voice message
yaroslav8765 May 5, 2026
a49a4da
chore: add beep sound on start/stop recording
yaroslav8765 May 5, 2026
88d9586
refactor: remove unused language formatting function and streamline s…
NoOne7135 May 5, 2026
98b7dc5
Merge branch 'add-frontend-speech-recognition' of https://github.com/…
NoOne7135 May 5, 2026
eb1d9b3
refactor: streamline import statements and enhance type safety for Ex…
NoOne7135 May 5, 2026
8536a09
chore: add template for the addDataToolCallMessage
yaroslav8765 May 5, 2026
c97ac06
Merge branch 'add-frontend-speech-recognition' of https://github.com/…
yaroslav8765 May 5, 2026
25092ee
fix: display data tool calls for the audio chat
yaroslav8765 May 5, 2026
962bc28
fix: play audio just after chat response
yaroslav8765 May 5, 2026
34d224b
refactor: update httpExtra type to use Pick<HttpExtra, "headers" | "c…
NoOne7135 May 5, 2026
4ae0b7b
Merge branch 'add-frontend-speech-recognition' of https://github.com/…
NoOne7135 May 5, 2026
5c30a00
chore: improve voice chat button ui
yaroslav8765 May 5, 2026
968d61f
Merge branch 'add-frontend-speech-recognition' of https://github.com/…
yaroslav8765 May 5, 2026
55e85e4
refactor: remove httpExtra from various function signatures and conte…
NoOne7135 May 5, 2026
97dad3c
Merge branch 'add-frontend-speech-recognition' of https://github.com/…
NoOne7135 May 5, 2026
9e03032
chore: refactor agent microphone button
yaroslav8765 May 6, 2026
366afc6
fix: reset microphone button to default state on chat close
yaroslav8765 May 6, 2026
8695e21
fix: update adminforth version to latest
yaroslav8765 May 6, 2026
f7f8923
Merge branch 'main' into add-frontend-speech-recognition
yaroslav8765 May 6, 2026
04dcb1c
fix: regenerate pnpm-lock file
yaroslav8765 May 6, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 0 additions & 8 deletions agent/languageDetect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,6 @@ const USER_LANGUAGE_OUTPUT_SCHEMA = {
},
} as const;

export function formatLanguagePrompt(language: UserLanguage | null) {
if (!language) {
return "Respond in the user's language.";
}

return `Respond in ${language.language} (${language.code}).`;
}

function parseUserLanguage(content: string | undefined): UserLanguage | null {
if (!content) {
return null;
Expand Down
10 changes: 5 additions & 5 deletions agent/simpleAgent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import {
logger,
type AdminUser,
type CompletionAdapter,
type HttpExtra,
type IAdminForth,
} from "adminforth";
import { BaseCallbackHandler } from "@langchain/core/callbacks/base";
Expand All @@ -26,8 +25,8 @@ export const contextSchema = z.object({
userTimeZone: z.string(),
sessionId: z.string(),
turnId: z.string(),
abortSignal: z.custom<AbortSignal>().optional(),
currentPage: z.custom<CurrentPageContext>().optional(),
httpExtra: z.custom<Partial<HttpExtra>>().optional(),
emitToolCallEvent: z.custom<ToolCallEventSink>(),
});

Expand Down Expand Up @@ -234,8 +233,8 @@ export async function callAgent(params: {
sessionId: string;
turnId: string;
currentPage?: CurrentPageContext;
httpExtra?: Partial<HttpExtra>;
userTimeZone: string;
abortSignal?: AbortSignal;
emitToolCallEvent: ToolCallEventSink;
sequenceDebugSink: SequenceDebugModelCallSink;
}) {
Expand All @@ -253,8 +252,8 @@ export async function callAgent(params: {
sessionId,
turnId,
currentPage,
httpExtra,
userTimeZone,
abortSignal,
emitToolCallEvent,
sequenceDebugSink,
} = params;
Expand Down Expand Up @@ -289,6 +288,7 @@ export async function callAgent(params: {
streamMode: "messages",
recursionLimit: 100,
callbacks: [createAgentLlmMetricsLogger()],
signal: abortSignal,
configurable: {
thread_id: sessionId,
},
Expand All @@ -297,8 +297,8 @@ export async function callAgent(params: {
userTimeZone,
sessionId,
turnId,
abortSignal,
currentPage,
httpExtra,
emitToolCallEvent,
},
});
Expand Down
39 changes: 35 additions & 4 deletions agent/systemPrompt.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import type { AdminForthResource, IAdminForth } from "adminforth";
import type { AdminForthResource, AdminUser, IAdminForth } from "adminforth";
import type { UserLanguage } from "./languageDetect.js";
import {
listBundledSkillManifests,
listProjectSkillManifests,
type AgentSkillManifest,
} from "./skills/registry.js";
import { ALWAYS_AVAILABLE_API_TOOL_NAMES } from "./tools/index.js";

export const DEFAULT_AGENT_SYSTEM_PROMPT = [
"You are helpful AI Assistant for Admin Panel.",
Expand Down Expand Up @@ -46,6 +46,39 @@ export function appendCustomSystemPrompt(
return `${systemPrompt}\n\n${normalizedCustomSystemPrompt}`;
}

function formatLanguagePrompt(language: UserLanguage | null) {
if (!language) {
return "Respond in the user's language.";
}

return `Respond in ${language.language} (${language.code}).`;
}

function formatAdminUserPrompt(adminUser: AdminUser, usernameField: string) {
const adminUserContext = {
id: adminUser.pk,
email: adminUser.dbUser[usernameField],
};
return [
"Current admin user context:",
JSON.stringify(adminUserContext, null, 2),
"Use this admin user email when the user asks to send information to themselves, the current admin, or the logged-in user.",
].join("\n");
}

export function buildAgentTurnSystemPrompt(input: {
agentSystemPrompt: string;
adminUser: AdminUser;
usernameField: string;
userLanguage: UserLanguage | null;
}) {
return [
input.agentSystemPrompt,
formatAdminUserPrompt(input.adminUser, input.usernameField),
formatLanguagePrompt(input.userLanguage),
].join("\n\n");
}

function formatResources(resources: AdminForthResource[]) {
return resources
.map((resource) => `- resourceId: ${resource.resourceId}\n label: ${resource.label}`)
Expand All @@ -66,7 +99,6 @@ export async function buildAgentSystemPrompt(
listProjectSkillManifests(adminforth.config.customization.customComponentsDir),
listBundledSkillManifests(),
]);
const alwaysAvailableTools = ALWAYS_AVAILABLE_API_TOOL_NAMES.join(", ");
const adminBasePath = adminforth.config.baseUrlSlashed;
const hiddenResourceIdSet = new Set(hiddenResourceIds);
const visibleResources = adminforth.config.resources.filter(
Expand All @@ -76,7 +108,6 @@ export async function buildAgentSystemPrompt(
DEFAULT_AGENT_SYSTEM_PROMPT,
`ADMIN_BASE_PATH: ${adminBasePath}`,
`List of resources:\n${formatResources(visibleResources)}`,
`You have always-available base tools: ${alwaysAvailableTools}.`,
primarySkills.length > 0
? `You have primary skills set:\n${formatSkills(primarySkills, "skill_name")}`
: "",
Expand Down
33 changes: 31 additions & 2 deletions agent/toolCallEvents.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { randomUUID } from "crypto";
import YAML from "yaml";
import { serializeUnknownError } from "../apiBasedTools.js";

export type ToolCallEvent =
| {
Expand Down Expand Up @@ -61,6 +60,36 @@ function sanitizeToolCallOutputForDebug(output: unknown) {
);
}

function serializeErrorForDebug(error: unknown): unknown {
if (!(error instanceof Error)) {
return error;
}

const errorWithCause = error as Error & { cause?: unknown };
const serialized: Record<string, unknown> = {
name: error.name,
message: error.message,
};

if (error.stack) {
serialized.stack = error.stack;
}

if (errorWithCause.cause !== undefined) {
serialized.cause = serializeErrorForDebug(errorWithCause.cause);
}

for (const key of Object.getOwnPropertyNames(error)) {
if (key in serialized) {
continue;
}

serialized[key] = (error as unknown as Record<string, unknown>)[key];
}

return serialized;
}

export function createToolCallTracker(params: {
emit: ToolCallEventSink;
toolCallId?: string;
Expand Down Expand Up @@ -99,7 +128,7 @@ export function createToolCallTracker(params: {
phase: "end",
durationMs: Date.now() - startedAt,
output: null,
error: YAML.stringify(serializeUnknownError(error)).trimEnd(),
error: YAML.stringify(serializeErrorForDebug(error)).trimEnd(),
});
},
};
Expand Down
2 changes: 1 addition & 1 deletion agent/tools/apiTool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ export function createApiTool(toolName: string, apiBasedTool: ApiBasedTool) {
const normalizedInput = (input ?? {}) as Record<string, unknown>;
return apiBasedTool.call({
adminUser: runtime.context.adminUser,
httpExtra: runtime.context.httpExtra,
abortSignal: runtime.context.abortSignal,
inputs: normalizedInput,
userTimeZone: runtime.context.userTimeZone,
});
Expand Down
197 changes: 197 additions & 0 deletions agentResponseEvents.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
import { randomUUID } from "crypto";

import type { ToolCallEvent } from "./agent/toolCallEvents.js";

type AgentEventStreamResponse = {
writeHead: (statusCode: number, headers: Record<string, string>) => void;
write: (chunk: string) => unknown;
end: () => unknown;
writableEnded: boolean;
destroyed: boolean;
};

type AgentEventStreamOptions = {
vercelAiUiMessageStream?: boolean;
closeActiveBlockOnToolStart?: boolean;
};

export function createAgentEventStream(
res: AgentEventStreamResponse,
options: AgentEventStreamOptions = {},
) {
let isStreamClosed = false;
let activeBlock: { type: "text" | "reasoning"; id: string } | null = null;

res.writeHead(200, {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
"Connection": "keep-alive",
...(options.vercelAiUiMessageStream
? { "x-vercel-ai-ui-message-stream": "v1" }
: {}),
});

const stream = {
send(obj: unknown) {
if (isStreamClosed || res.writableEnded || res.destroyed) {
return;
}

res.write(`data: ${JSON.stringify(obj)}\n\n`);
},

endActiveBlock() {
if (!activeBlock) {
return;
}

stream.send({
type: `${activeBlock.type}-end`,
id: activeBlock.id,
});

activeBlock = null;
},

startBlock(type: "text" | "reasoning") {
if (activeBlock?.type === type) {
return activeBlock.id;
}

stream.endActiveBlock();

const id = randomUUID();
activeBlock = { type, id };

stream.send({
type: `${type}-start`,
id,
});

return id;
},

start(messageId: string) {
stream.send({
type: "start",
messageId,
});
},

textDelta(delta: string) {
const textId = stream.startBlock("text");
stream.send({
type: "text-delta",
id: textId,
delta,
});
},

reasoningDelta(delta: string) {
const reasoningId = stream.startBlock("reasoning");
stream.send({
type: "reasoning-delta",
id: reasoningId,
delta,
});
},

toolCall(event: ToolCallEvent) {
if (options.closeActiveBlockOnToolStart && event.phase === "start") {
stream.endActiveBlock();
}

stream.send({
type: "data-tool-call",
data: event,
});
},

transcript(text: string, language?: string) {
stream.send({
type: "transcript",
data: {
text,
language,
},
});
},

response(text: string, sessionId: string, turnId: string) {
stream.send({
type: "response",
data: {
text,
sessionId,
turnId,
},
});
},

speechResponse(
transcript: { text: string; language?: string },
response: { text: string },
sessionId: string,
turnId: string,
) {
stream.send({
type: "speech-response",
data: {
transcript,
response,
sessionId,
turnId,
},
});
},

audioStart(mimeType: string, format: string) {
stream.send({
type: "audio-start",
data: {
mimeType,
format,
},
});
},

audioDelta(value: Uint8Array) {
stream.send({
type: "audio-delta",
data: {
base64: Buffer.from(value).toString("base64"),
},
});
},

audioDone() {
stream.send({
type: "audio-done",
});
},

error(error: string) {
stream.send({
type: "error",
error,
});
},

end() {
if (isStreamClosed || res.writableEnded || res.destroyed) {
return;
}

stream.endActiveBlock();
stream.send({
type: "finish",
});

res.write("data: [DONE]\n\n");
isStreamClosed = true;
res.end();
},
};

return stream;
}
Loading