From 8aa6fd7c8e38df16cde03a9fbfb126ec4833d314 Mon Sep 17 00:00:00 2001 From: Enrico Ros Date: Thu, 26 Jun 2025 10:31:51 -0700 Subject: [PATCH] Zod-4: for JSON schema --- .../agiAttachmentPrompts.ts | 4 +- src/modules/aifn/agicodefixup/agiFixupCode.ts | 6 ++- .../auto-chat-follow-ups/autoChatFollowUps.ts | 10 ++-- .../client/aix.client.fromSimpleFunction.ts | 50 +++++++------------ 4 files changed, 30 insertions(+), 40 deletions(-) diff --git a/src/modules/aifn/agiattachmentprompts/agiAttachmentPrompts.ts b/src/modules/aifn/agiattachmentprompts/agiAttachmentPrompts.ts index 6380af10f..91457be8f 100644 --- a/src/modules/aifn/agiattachmentprompts/agiAttachmentPrompts.ts +++ b/src/modules/aifn/agiattachmentprompts/agiAttachmentPrompts.ts @@ -1,4 +1,4 @@ -import { z } from 'zod'; +import { z } from 'zod/v4'; import type { AixAPIChatGenerate_Request } from '~/modules/aix/server/api/aix.wiretypes'; import { aixCGR_ChatSequence_FromDMessagesOrThrow, aixCGR_SystemMessageText } from '~/modules/aix/client/aix.client.chatGenerateRequest'; @@ -24,7 +24,7 @@ export async function agiAttachmentPrompts(attachmentFragments: DMessageAttachme const num_suggestions = 3; - const inputSchema = z.object({ + const inputSchema = z.object({ // zod-4 attachments_analysis: z.array( z.object({ name: z.string().describe('Identifier of the file.'), diff --git a/src/modules/aifn/agicodefixup/agiFixupCode.ts b/src/modules/aifn/agicodefixup/agiFixupCode.ts index 893dc6d63..dde0ac133 100644 --- a/src/modules/aifn/agicodefixup/agiFixupCode.ts +++ b/src/modules/aifn/agicodefixup/agiFixupCode.ts @@ -1,4 +1,4 @@ -import type { ZodObject } from 'zod'; +import type { ZodObject, ZodString } from 'zod/v4'; import type { AixAPIChatGenerate_Request } from '~/modules/aix/server/api/aix.wiretypes'; import { aixChatGenerateContent_DMessage, aixCreateChatGenerateContext } from '~/modules/aix/client/aix.client'; @@ -17,7 +17,9 @@ interface CodeFix { userInstructionTemplate: string; // Template with placeholders for `codeToFix` and `errorString` functionName: string; functionPolicy: 'invoke' | 'think-then-invoke'; - outputSchema: ZodObject; + outputSchema: ZodObject<{ + corrected_code: ZodString; + }>; } const CodeFixes: Record = {}; diff --git a/src/modules/aifn/auto-chat-follow-ups/autoChatFollowUps.ts b/src/modules/aifn/auto-chat-follow-ups/autoChatFollowUps.ts index 803c0119c..897571269 100644 --- a/src/modules/aifn/auto-chat-follow-ups/autoChatFollowUps.ts +++ b/src/modules/aifn/auto-chat-follow-ups/autoChatFollowUps.ts @@ -1,4 +1,4 @@ -import { z } from 'zod'; +import { z } from 'zod/v4'; import type { AixAPIChatGenerate_Request } from '~/modules/aix/server/api/aix.wiretypes'; import { AixClientFunctionCallToolDefinition, aixFunctionCallTool, aixRequireSingleFunctionCallInvocation } from '~/modules/aix/client/aix.client.fromSimpleFunction'; @@ -70,9 +70,9 @@ Analyze the following short exchange and call the function {{functionName}} with fun: { name: 'draw_plantuml_diagram', description: 'Generates a PlantUML diagram or mindmap from the last message, if applicable, very useful to the user, and no other diagrams are present.', - inputSchema: z.object({ + inputSchema: z.object({ // zod-4 rating_short_reason: z.string().describe('A 4-10 words reason on whether the diagram would be desired by the user or not.'), - rating_number: z.number().int().describe('The relevance of the diagram to the conversation, on a scale of 1 to 5 . If lower than 4, STOP.'), + rating_number: z.number().describe('The relevance of the diagram to the conversation, on a scale of 1 to 5 . If lower than 4, STOP.'), type: z.string().describe('The most suitable PlantUML diagram type: sequence, usecase, class, activity, component, state, object, deployment, timing, network, wireframe, gantt, wbs or mindmap.').optional(), code: z.string().describe('A valid PlantUML string (@startuml...@enduml) to be rendered as a diagram or mindmap (@startmindmap...@endmindmap), or empty. No external references allowed. Use one or more asterisks to indent and separate with spaces.').optional(), }), @@ -113,10 +113,10 @@ Please follow closely the following requirements: fun: { name: suggestUIFunctionName, description: 'Renders a web UI when provided with a single concise HTML5 string (can include CSS and JS), if applicable and relevant.', - inputSchema: z.object({ + inputSchema: z.object({ // zod-4 possible_ui_requirements: z.string().describe('Brief (10 words) to medium length (40 words) requirements for the UI. Include main features, looks, and layout.'), rating_short_reason: z.string().describe('A 4-10 word reason on whether the UI would be desired by the user or not.'), - rating_number: z.number().int().describe('The relevance of the UI to the conversation, on a scale of 1 (does not add much value), 2 (superfluous), 3 (helps a lot in understanding), 4 (essential) to 5 (fundamental to the understanding). If 1 or 2, do not proceed and STOP.'), + rating_number: z.number().describe('The relevance of the UI to the conversation, on a scale of 1 (does not add much value), 2 (superfluous), 3 (helps a lot in understanding), 4 (essential) to 5 (fundamental to the understanding). If 1 or 2, do not proceed and STOP.'), html: z.string().describe('A valid HTML string containing the user interface code. The code should be complete, with no dependencies, lower case, and include minimal inline CSS if needed. The UI should be visual and interactive.').optional(), file_name: z.string().describe('Short letters-and-dashes file name of the HTML without the .html extension.').optional(), }), diff --git a/src/modules/aix/client/aix.client.fromSimpleFunction.ts b/src/modules/aix/client/aix.client.fromSimpleFunction.ts index 3be3c37aa..16433fcfe 100644 --- a/src/modules/aix/client/aix.client.fromSimpleFunction.ts +++ b/src/modules/aix/client/aix.client.fromSimpleFunction.ts @@ -1,5 +1,4 @@ -import { ZodSchema } from 'zod'; -import { JsonSchema7ObjectType, zodToJsonSchema } from 'zod-to-json-schema'; +import { z } from 'zod/v4'; import type { AixTools_FunctionCallDefinition } from '../server/api/aix.wiretypes'; import { DMessageContentFragment, DMessageToolInvocationPart, DMessageVoidFragment, isContentFragment } from '~/common/stores/chat/chat.fragments'; @@ -17,7 +16,7 @@ export type AixClientFunctionCallToolDefinition = { * We only accept objects, not arrays - as downstream APIs have spotty implementation for non-object. * If the function does not take any inputs, use `Zod.object({})` or Zod.void(). */ - inputSchema: ZodSchema; + inputSchema: z.ZodObject; // zod-4 object } @@ -26,7 +25,19 @@ export type AixClientFunctionCallToolDefinition = { * @param functionCall */ export function aixFunctionCallTool(functionCall: AixClientFunctionCallToolDefinition): AixTools_FunctionCallDefinition { - const { properties, required } = zodToJsonSchema(functionCall.inputSchema, { $refStrategy: 'none' }) as JsonSchema7ObjectType; + + // convert a Zod schema to JSON Schema + const { properties, required } = z.toJSONSchema(functionCall.inputSchema, { + // config + io: 'input', // avoids AdditionalProperties by looking at the Zod schema from the input perspective + target: 'draft-2020-12', // (default) newest standard + reused: 'inline', // (default) inline reused schemas + + // [DEV] makes sure we specify good tool definitions + cycles: 'throw', + unrepresentable: 'throw', + }); + const takesNoInputs = !Object.keys(properties || {}).length; return { type: 'function_call', @@ -35,8 +46,10 @@ export function aixFunctionCallTool(functionCall: AixClientFunctionCallToolDefin description: functionCall.description, ...(!takesNoInputs && { input_schema: { - properties: _recursiveObjectSchemaCleanup(properties), - ...(required && { required }), + properties: properties as any, // FIXME: remove the 'as any' after the full migration to zod-4 + ...(!!required?.length && { + required: required, + }), }, }), }, @@ -44,31 +57,6 @@ export function aixFunctionCallTool(functionCall: AixClientFunctionCallToolDefin } -/* Recursive function to clean up the Schema object, to: - * - remove extra 'additionalProperties' keys - * - reorder the keys of object/array description objects to be: ['type', 'description', ..., 'required'] - */ -function _recursiveObjectSchemaCleanup(obj: Record, thisKey?: string): Record { - if (typeof obj !== 'object' || obj === null) - return obj; // leaf node - - const { additionalProperties: _, ...rest } = obj; - - // 'properties' are ordered and we don't want to re-sort them - if (thisKey === 'properties') { - return Object.fromEntries(Object.entries(rest).map(([key, value]) => [key, _recursiveObjectSchemaCleanup(value, key)])); - } - - const { type, description, required, ...others } = rest; - return { - ...(type && { type }), - ...(description && { description }), - ...Object.fromEntries(Object.entries(others).map(([key, value]) => [key, _recursiveObjectSchemaCleanup(value, key)])), - ...(required && { required }), - }; -} - - /** Extract the function name from the Aix FunctionCall Tool Definition */ export function aixRequireSingleFunctionCallInvocation(fragments: (DMessageContentFragment | DMessageVoidFragment)[], expectedFunctionName: string, allowThinkPart: boolean, debugLabel: string): { invocation: Extract;