Zod-4: for JSON schema

This commit is contained in:
Enrico Ros
2025-06-26 10:31:51 -07:00
parent e2e6e6d641
commit 8aa6fd7c8e
4 changed files with 30 additions and 40 deletions
@@ -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.'),
@@ -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<any>;
outputSchema: ZodObject<{
corrected_code: ZodString;
}>;
}
const CodeFixes: Record<string, CodeFix> = {};
@@ -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(),
}),
@@ -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<object /*| void*/>;
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<string, any>, thisKey?: string): Record<string, any> {
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<DMessageToolInvocationPart['invocation'], { type: 'function_call' }>;