Skip to content

Commit 449bf15

Browse files
committed
fix: remove unused types and simplify request body parsing in endpoints
1 parent efbd93c commit 449bf15

6 files changed

Lines changed: 81 additions & 56 deletions

File tree

endpoints/context.ts

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import type {
44
ChatSurfaceIncomingMessage,
55
IAdminForth,
66
} from "adminforth";
7-
import type { ZodType } from "zod";
87
import type {
98
HandleSpeechTurnInput,
109
HandleTurnInput,
@@ -13,10 +12,6 @@ import type {
1312
} from "../agentTurnService.js";
1413
import type { PluginOptions } from "../types.js";
1514

16-
export type EndpointResponse = {
17-
setStatus: (code: number, message: string) => void;
18-
};
19-
2015
export type SessionTurn = {
2116
prompt: string;
2217
response: string;
@@ -25,7 +20,6 @@ export type SessionTurn = {
2520
export type AgentEndpointsContext = {
2621
adminforth: IAdminForth;
2722
options: PluginOptions;
28-
parseBody<T>(schema: ZodType<T>, body: unknown, response: EndpointResponse): T | null;
2923
handleTurn(input: HandleTurnInput): Promise<RunAndPersistAgentResponseResult>;
3024
handleSpeechTurn(input: HandleSpeechTurnInput): Promise<RunAndPersistAgentResponseResult | null>;
3125
runAndPersistAgentResponse(input: RunAndPersistAgentResponseInput): Promise<RunAndPersistAgentResponseResult>;
@@ -41,12 +35,12 @@ export type AgentEndpointsContext = {
4135

4236
export type CoreEndpointsContext = Pick<
4337
AgentEndpointsContext,
44-
"options" | "parseBody" | "handleTurn" | "handleSpeechTurn"
38+
"options" | "handleTurn" | "handleSpeechTurn"
4539
>;
4640

4741
export type SessionEndpointsContext = Pick<
4842
AgentEndpointsContext,
49-
"adminforth" | "options" | "parseBody" | "getSessionTurns" | "createNewTurn"
43+
"adminforth" | "options" | "getSessionTurns" | "createNewTurn"
5044
| "createSystemTurn"
5145
>;
5246

endpoints/core.ts

Lines changed: 46 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,20 +12,53 @@ type MulterFile = {
1212

1313
type ExpressMulterRequest = { file?: MulterFile };
1414

15+
const currentPageContextSchema = z.object({
16+
path: z.string(),
17+
fullPath: z.string(),
18+
title: z.string(),
19+
url: z.string(),
20+
}).strict() satisfies z.ZodType<CurrentPageContext>;
21+
1522
const agentResponseBodySchema = z.object({
1623
message: z.string(),
1724
sessionId: z.string(),
1825
mode: z.string().nullish(),
1926
timeZone: z.string().optional(),
20-
currentPage: z.custom<CurrentPageContext>().optional(),
27+
currentPage: currentPageContextSchema.optional(),
2128
}).strict();
2229

2330
const agentApprovalBodySchema = z.object({
2431
sessionId: z.string(),
2532
decision: z.enum(["approve", "reject"]),
2633
}).strict();
2734

28-
const agentSpeechResponseBodySchema = agentResponseBodySchema.omit({ message: true });
35+
// Sent as multipart/form-data (via multer), so every field arrives as a plain string on the wire.
36+
// `currentPage` is JSON.stringify'd by the client into a form field and must be parsed back into
37+
// an object. Zod -> JSON-Schema conversion cannot represent `.transform()` at all (it throws), so
38+
// `agentSpeechResponseShapeSchema` (plain strings, no transform) is what's passed as request_schema
39+
// for the AJV pre-check, while `agentSpeechResponseBodySchema` (with the transform) is used via an
40+
// inline safeParse in the handler to actually parse `currentPage` into an object.
41+
const agentSpeechResponseShapeSchema = z.object({
42+
sessionId: z.string(),
43+
mode: z.string().nullish(),
44+
timeZone: z.string().optional(),
45+
currentPage: z.string().optional(),
46+
}).strict();
47+
48+
const agentSpeechResponseBodySchema = agentSpeechResponseShapeSchema.extend({
49+
currentPage: z.string().optional().transform((value, ctx) => {
50+
if (value === undefined) return undefined;
51+
try {
52+
return currentPageContextSchema.parse(JSON.parse(value));
53+
} catch (err) {
54+
ctx.addIssue({
55+
code: z.ZodIssueCode.custom,
56+
message: `currentPage must be a JSON-encoded object matching CurrentPageContext: ${err instanceof Error ? err.message : String(err)}`,
57+
});
58+
return z.NEVER;
59+
}
60+
}),
61+
});
2962

3063
export function setupCoreEndpoints(ctx: CoreEndpointsContext, server: IHttpServer) {
3164
server.endpoint({
@@ -52,9 +85,9 @@ export function setupCoreEndpoints(ctx: CoreEndpointsContext, server: IHttpServe
5285
server.endpoint({
5386
method: 'POST',
5487
path: `/agent/response`,
88+
request_schema: agentResponseBodySchema,
5589
handler: async ({ body, adminUser, response, _raw_express_res, abortSignal }) => {
56-
const data = ctx.parseBody(agentResponseBodySchema, body, response);
57-
if (!data) return;
90+
const data = body as z.infer<typeof agentResponseBodySchema>;
5891
const emit = createSseEventEmitter(_raw_express_res, {
5992
vercelAiUiMessageStream: true,
6093
closeActiveBlockOnToolStart: true,
@@ -79,9 +112,9 @@ export function setupCoreEndpoints(ctx: CoreEndpointsContext, server: IHttpServe
79112
server.endpoint({
80113
method: 'POST',
81114
path: `/agent/approval`,
115+
request_schema: agentApprovalBodySchema,
82116
handler: async ({ body, adminUser, response, _raw_express_res, abortSignal }) => {
83-
const data = ctx.parseBody(agentApprovalBodySchema, body, response);
84-
if (!data) return;
117+
const data = body as z.infer<typeof agentApprovalBodySchema>;
85118
const emit = createSseEventEmitter(_raw_express_res, {
86119
vercelAiUiMessageStream: true,
87120
closeActiveBlockOnToolStart: true,
@@ -105,15 +138,20 @@ export function setupCoreEndpoints(ctx: CoreEndpointsContext, server: IHttpServe
105138
method: 'POST',
106139
path: `/agent/speech-response`,
107140
target: 'upload',
141+
request_schema: agentSpeechResponseShapeSchema,
108142
handler: async ({ body, adminUser, response, _raw_express_req, _raw_express_res, abortSignal }) => {
109143
const req = _raw_express_req as ExpressMulterRequest;
110144
const audioAdapter = ctx.options.audioAdapter;
111145
if (!audioAdapter) {
112146
response.setStatus(400, "Audio adapter is not configured for AdminForth Agent");
113147
return { error: "Audio adapter is not configured for AdminForth Agent" };
114148
}
115-
const data = ctx.parseBody(agentSpeechResponseBodySchema, body, response);
116-
if (!data) return;
149+
const parsed = agentSpeechResponseBodySchema.safeParse(body);
150+
if (!parsed.success) {
151+
response.setStatus(400, 'Request body validation failed');
152+
return { error: 'Request body validation failed', details: parsed.error.issues };
153+
}
154+
const data = parsed.data;
117155
if (!req.file) {
118156
response.setStatus(400, "Audio file is required");
119157
return { error: "Audio file is required" };

endpoints/sessions.ts

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,9 @@ export function setupSessionEndpoints(ctx: SessionEndpointsContext, server: IHtt
2626
server.endpoint({
2727
method: 'POST',
2828
path: `/agent/get-sessions`,
29+
request_schema: getSessionsBodySchema,
2930
handler: async ({body, adminUser, response }) => {
30-
const data = ctx.parseBody(getSessionsBodySchema, body, response);
31-
if (!data) return;
31+
const data = body as z.infer<typeof getSessionsBodySchema>;
3232
const userId = adminUser!.pk;
3333
const limit = data.limit ?? 20;
3434
const sessions = await ctx.adminforth.resource(ctx.options.sessionResource.resourceId).list(
@@ -47,9 +47,9 @@ export function setupSessionEndpoints(ctx: SessionEndpointsContext, server: IHtt
4747
server.endpoint({
4848
method: 'POST',
4949
path: `/agent/get-session-info`,
50+
request_schema: sessionIdBodySchema,
5051
handler: async ({body, adminUser, response }) => {
51-
const data = ctx.parseBody(sessionIdBodySchema, body, response);
52-
if (!data) return;
52+
const data = body as z.infer<typeof sessionIdBodySchema>;
5353
const userId = adminUser!.pk;
5454
const sessionId = data.sessionId;
5555
const session = await ctx.adminforth.resource(ctx.options.sessionResource.resourceId).get(
@@ -102,9 +102,9 @@ export function setupSessionEndpoints(ctx: SessionEndpointsContext, server: IHtt
102102
server.endpoint({
103103
method: 'POST',
104104
path: `/agent/create-session`,
105+
request_schema: createSessionBodySchema,
105106
handler: async ({body, adminUser, response }) => {
106-
const data = ctx.parseBody(createSessionBodySchema, body, response);
107-
if (!data) return;
107+
const data = body as z.infer<typeof createSessionBodySchema>;
108108
const triggerMessage = data.triggerMessage;
109109
const userId = adminUser!.pk;
110110
const title = triggerMessage?.slice(0, 40) || "New Session";
@@ -126,9 +126,9 @@ export function setupSessionEndpoints(ctx: SessionEndpointsContext, server: IHtt
126126
server.endpoint({
127127
method: 'POST',
128128
path: `/agent/delete-session`,
129+
request_schema: sessionIdBodySchema,
129130
handler: async ({body, adminUser, response }) => {
130-
const data = ctx.parseBody(sessionIdBodySchema, body, response);
131-
if (!data) return;
131+
const data = body as z.infer<typeof sessionIdBodySchema>;
132132
const sessionId = data.sessionId;
133133
const userId = adminUser!.pk;
134134
const session = await ctx.adminforth.resource(ctx.options.sessionResource.resourceId).get(
@@ -158,9 +158,9 @@ export function setupSessionEndpoints(ctx: SessionEndpointsContext, server: IHtt
158158
server.endpoint({
159159
method: 'POST',
160160
path: `/agent/add-system-message-to-turns`,
161+
request_schema: addSystemMessageBodySchema,
161162
handler: async ({body, adminUser, response }) => {
162-
const data = ctx.parseBody(addSystemMessageBodySchema, body, response);
163-
if (!data) return;
163+
const data = body as z.infer<typeof addSystemMessageBodySchema>;
164164
const session = await ctx.adminforth.resource(ctx.options.sessionResource.resourceId).get(
165165
[Filters.EQ(ctx.options.sessionResource.idField, data.sessionId)]
166166
);

index.ts

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import { AdminForthPlugin } from "adminforth";
88

99
import type { PluginOptions } from './types.js';
1010
import { MemorySaver, type BaseCheckpointSaver } from "@langchain/langgraph";
11-
import { z } from "zod";
1211
import { AdminForthCheckpointSaver } from "./agent/checkpointer.js";
1312
import { appendCustomSystemPrompt, buildAgentSystemPrompt, DEFAULT_AGENT_SYSTEM_PROMPT} from "./agent/systemPrompt.js";
1413
import { setupCoreEndpoints } from "./endpoints/core.js";
@@ -41,18 +40,6 @@ export default class AdminForthAgentPlugin extends AdminForthPlugin {
4140
private agentTurnService: AgentTurnService;
4241
private speechTurnService: SpeechTurnService;
4342
private chatSurfaceService: ChatSurfaceService;
44-
private parseBody<T>(
45-
schema: z.ZodType<T>,
46-
body: unknown,
47-
response: { setStatus: (code: number, message: string) => void },
48-
): T | null {
49-
const parsed = schema.safeParse(body ?? {});
50-
if (!parsed.success) {
51-
response.setStatus(422, parsed.error.message);
52-
return null;
53-
}
54-
return parsed.data;
55-
}
5643
private getCheckpointer() {
5744
if (this.checkpointer) return this.checkpointer;
5845

@@ -189,7 +176,6 @@ export default class AdminForthAgentPlugin extends AdminForthPlugin {
189176
const endpointContext = {
190177
adminforth: this.adminforth,
191178
options: this.options,
192-
parseBody: this.parseBody.bind(this),
193179
handleTurn: this.agentTurnService.handleTurn.bind(this.agentTurnService),
194180
handleSpeechTurn: this.speechTurnService.handle.bind(this.speechTurnService),
195181
runAndPersistAgentResponse: this.agentTurnService.runAndPersistAgentResponse.bind(this.agentTurnService),

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
"description": "AI agent plugin for AdminForth with tool-based workflows and persistent chat sessions",
2626
"devDependencies": {
2727
"@types/node": "latest",
28-
"adminforth": "^3.7.1",
28+
"adminforth": "^3.8.2",
2929
"semantic-release": "^24.2.1",
3030
"semantic-release-slack-bot": "^4.0.2",
3131
"typescript": "^5.7.3"
@@ -67,6 +67,6 @@
6767
}
6868
],
6969
"peerDependencies": {
70-
"adminforth": "^3.7.1"
70+
"adminforth": "^3.8.2"
7171
}
7272
}

pnpm-lock.yaml

Lines changed: 21 additions & 14 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)