import { z } from "zod"; import { Parameter } from "../types"; export type JSONSchemaString = { type: "string"; description?: string; enum?: string[]; }; export type JSONSchemaNumber = { type: "number"; description?: string; }; export type JSONSchemaBoolean = { type: "boolean"; description?: string; }; export type JSONSchemaObject = { type: "object"; properties?: Record; required?: string[]; description?: string; }; export type JSONSchemaArray = { type: "array"; items: JSONSchema; description?: string; }; export type JSONSchema = | JSONSchemaString | JSONSchemaNumber | JSONSchemaBoolean | JSONSchemaObject | JSONSchemaArray; export function actionParametersToJsonSchema( actionParameters: Parameter[], ): JSONSchema { // Create the parameters object based on the argumentAnnotations let parameters: { [key: string]: any } = {}; for (let parameter of actionParameters || []) { parameters[parameter.name] = convertAttribute(parameter); } let requiredParameterNames: string[] = []; for (let arg of actionParameters || []) { if (arg.required !== false) { requiredParameterNames.push(arg.name); } } // Create the ChatCompletionFunctions object return { type: "object", properties: parameters, required: requiredParameterNames, }; } // Convert JSONSchema to Parameter[] export function jsonSchemaToActionParameters( jsonSchema: JSONSchema, ): Parameter[] { if (jsonSchema.type !== "object" || !jsonSchema.properties) { return []; } const parameters: Parameter[] = []; const requiredFields = jsonSchema.required || []; for (const [name, schema] of Object.entries(jsonSchema.properties)) { const parameter = convertJsonSchemaToParameter( name, schema, requiredFields.includes(name), ); parameters.push(parameter); } return parameters; } // Convert JSONSchema property to Parameter function convertJsonSchemaToParameter( name: string, schema: JSONSchema, isRequired: boolean, ): Parameter { const baseParameter: Parameter = { name, description: schema.description, }; if (!isRequired) { baseParameter.required = false; } // Handle null-union types like ["string", "null"] by picking the non-null type if (Array.isArray(schema.type)) { const types = schema.type as string[]; const hasNull = types.includes("null"); const nonNullTypes = types.filter((t: string) => t !== "null"); const resolvedType = nonNullTypes.length > 0 ? nonNullTypes[0] : "string"; return convertJsonSchemaToParameter( name, { ...schema, type: resolvedType } as JSONSchema, hasNull ? false : isRequired, ); } switch (schema.type) { case "string": return { ...baseParameter, type: "string", ...(schema.enum && { enum: schema.enum }), }; case "number": case "boolean": return { ...baseParameter, type: schema.type, }; case "object": if (schema.properties) { const attributes: Parameter[] = []; const requiredFields = schema.required || []; for (const [propName, propSchema] of Object.entries( schema.properties, )) { attributes.push( convertJsonSchemaToParameter( propName, propSchema, requiredFields.includes(propName), ), ); } return { ...baseParameter, type: "object", attributes, }; } return { ...baseParameter, type: "object", }; case "array": if (schema.items.type === "object" && "properties" in schema.items) { const attributes: Parameter[] = []; const requiredFields = schema.items.required || []; for (const [propName, propSchema] of Object.entries( schema.items.properties || {}, )) { attributes.push( convertJsonSchemaToParameter( propName, propSchema, requiredFields.includes(propName), ), ); } return { ...baseParameter, type: "object[]", attributes, }; } else if (schema.items.type === "array") { throw new Error("Nested arrays are not supported"); } else { return { ...baseParameter, type: `${schema.items.type}[]`, }; } default: return { ...baseParameter, type: "string", }; } } function convertAttribute(attribute: Parameter): JSONSchema { switch (attribute.type) { case "string": return { type: "string", description: attribute.description, ...(attribute.enum && { enum: attribute.enum }), }; case "number": case "boolean": return { type: attribute.type, description: attribute.description, }; case "object": case "object[]": const properties = attribute.attributes?.reduce( (acc, attr) => { acc[attr.name] = convertAttribute(attr); return acc; }, {} as Record, ); const required = attribute.attributes ?.filter((attr) => attr.required !== false) .map((attr) => attr.name); if (attribute.type === "object[]") { return { type: "array", items: { type: "object", ...(properties && { properties }), ...(required && required.length > 0 && { required }), }, description: attribute.description, }; } return { type: "object", description: attribute.description, ...(properties && { properties }), ...(required && required.length > 0 && { required }), }; default: // Handle arrays of primitive types and undefined attribute.type if (attribute.type?.endsWith("[]")) { const itemType = attribute.type.slice(0, -2); return { type: "array", items: { type: itemType as any }, description: attribute.description, }; } // Fallback for undefined type or any other unexpected type return { type: "string", description: attribute.description, }; } } export function convertJsonSchemaToZodSchema( jsonSchema: any, required: boolean, definitions?: Record, visitedRefs?: Set, ): z.ZodSchema { // Resolve $ref references if (jsonSchema.$ref && definitions) { const refPath = jsonSchema.$ref.replace( /^#\/\$defs\/|^#\/definitions\//, "", ); // Detect circular $ref cycles const refs = visitedRefs ?? new Set(); if (refs.has(refPath)) { console.warn( `[CopilotKit] Circular $ref detected for "${refPath}" — falling back to z.any()`, ); let schema = z.any(); if (jsonSchema.description) { schema = schema.describe(jsonSchema.description); } return required ? schema : schema.optional(); } const resolved = definitions[refPath]; if (resolved) { // Clone the set so sibling branches don't see each other's visited refs const nextRefs = new Set(refs); nextRefs.add(refPath); return convertJsonSchemaToZodSchema( resolved, required, definitions, nextRefs, ); } } // Collect top-level definitions for $ref resolution const defs = definitions ?? jsonSchema.$defs ?? jsonSchema.definitions; // Handle null-union types like ["string", "null"] if (Array.isArray(jsonSchema.type)) { const types = jsonSchema.type as string[]; const hasNull = types.includes("null"); const nonNullTypes = types.filter((t: string) => t !== "null"); const resolvedType = nonNullTypes.length > 0 ? nonNullTypes[0] : "string"; const innerSchema = convertJsonSchemaToZodSchema( { ...jsonSchema, type: resolvedType }, true, defs, visitedRefs, ); let schema = hasNull ? z.union([innerSchema, z.null()]) : innerSchema; if (jsonSchema.description) { schema = schema.describe(jsonSchema.description); } return required ? schema : schema.optional(); } // Handle anyOf / oneOf as z.union const unionVariants = jsonSchema.anyOf ?? jsonSchema.oneOf; if (Array.isArray(unionVariants) && unionVariants.length > 0) { if (unionVariants.length === 1) { return convertJsonSchemaToZodSchema( unionVariants[0], required, defs, visitedRefs, ); } const schemas = unionVariants.map((v: any) => convertJsonSchemaToZodSchema(v, true, defs, visitedRefs), ); let schema = z.union( schemas as [z.ZodSchema, z.ZodSchema, ...z.ZodSchema[]], ); if (jsonSchema.description) { schema = schema.describe(jsonSchema.description); } return required ? schema : schema.optional(); } if (jsonSchema.type === "object") { const spec: { [key: string]: z.ZodSchema } = {}; if (!jsonSchema.properties || !Object.keys(jsonSchema.properties).length) { return !required ? z.object(spec).optional() : z.object(spec); } for (const [key, value] of Object.entries(jsonSchema.properties)) { spec[key] = convertJsonSchemaToZodSchema( value, jsonSchema.required ? jsonSchema.required.includes(key) : false, defs, visitedRefs, ); } let schema = z.object(spec).describe(jsonSchema.description); return required ? schema : schema.optional(); } else if (jsonSchema.type === "string") { if (jsonSchema.enum && jsonSchema.enum.length > 0) { let schema = z .enum(jsonSchema.enum as [string, ...string[]]) .describe(jsonSchema.description); return required ? schema : schema.optional(); } let schema = z.string().describe(jsonSchema.description); return required ? schema : schema.optional(); } else if (jsonSchema.type === "number" || jsonSchema.type === "integer") { let schema = z.number().describe(jsonSchema.description); return required ? schema : schema.optional(); } else if (jsonSchema.type === "boolean") { let schema = z.boolean().describe(jsonSchema.description); return required ? schema : schema.optional(); } else if (jsonSchema.type === "array") { let itemSchema = convertJsonSchemaToZodSchema( jsonSchema.items, true, defs, visitedRefs, ); let schema = z.array(itemSchema).describe(jsonSchema.description); return required ? schema : schema.optional(); } else if (jsonSchema.type === "null") { let schema = z.null().describe(jsonSchema.description); return required ? schema : schema.optional(); } // Fallback: accept any value rather than throwing console.warn( `[CopilotKit] Unsupported JSON schema type "${jsonSchema.type ?? "unknown"}" — falling back to z.any()`, ); let schema = z.any(); if (jsonSchema.description) { schema = schema.describe(jsonSchema.description); } return required ? schema : schema.optional(); } export function getZodParameters( parameters: T, ): any { if (!parameters) return z.object({}); const jsonParams = actionParametersToJsonSchema(parameters); return convertJsonSchemaToZodSchema(jsonParams, true); }