UNPKG

typia

Version:

Superfast runtime validators with only one line

415 lines (396 loc) 14.1 kB
import { ILlmApplication, ILlmSchema, IOpenApiSchemaError, IResult, LlmTypeChecker, OpenApi, } from "@samchon/openapi"; import { LlmSchemaComposer } from "@samchon/openapi/lib/composers/LlmSchemaComposer"; import { ILlmFunction } from "@samchon/openapi/lib/structures/ILlmFunction"; import ts from "typescript"; import { MetadataFactory } from "../../factories/MetadataFactory"; import { TypeFactory } from "../../factories/TypeFactory"; import { IJsonSchemaApplication } from "../../schemas/json/IJsonSchemaApplication"; import { Metadata } from "../../schemas/metadata/Metadata"; import { MetadataFunction } from "../../schemas/metadata/MetadataFunction"; import { MetadataObjectType } from "../../schemas/metadata/MetadataObjectType"; import { MetadataParameter } from "../../schemas/metadata/MetadataParameter"; import { ITypiaContext } from "../../transformers/ITypiaContext"; import { IValidation } from "../../IValidation"; import { ValidateProgrammer } from "../ValidateProgrammer"; import { JsonApplicationProgrammer } from "../json/JsonApplicationProgrammer"; import { LlmSchemaProgrammer } from "./LlmSchemaProgrammer"; export namespace LlmApplicationProgrammer { export const validate = (props: { config?: Partial<ILlmSchema.IConfig>; metadata: Metadata; explore: MetadataFactory.IExplore; }): string[] => { const top: Metadata = props.metadata; if (props.explore.top === false) if ( props.explore.object === top?.objects[0]?.type && typeof props.explore.property === "string" && props.metadata.size() === 1 && props.metadata.nullable === false && props.metadata.isRequired() === true && props.metadata.functions.length === 1 ) return validateFunction( props.explore.property, props.metadata.functions[0]!, ); else return LlmSchemaProgrammer.validate(props); const output: string[] = []; const validity: boolean = props.metadata.size() === 1 && props.metadata.objects.length === 1 && props.metadata.isRequired() === true && props.metadata.nullable === false; if (validity === false) output.push( "LLM application's generic argument must be a class/interface type.", ); const object: MetadataObjectType | undefined = props.metadata.objects[0]?.type; if (object !== undefined) { if (object.properties.some((p) => p.key.isSoleLiteral() === false)) output.push( "LLM application does not allow dynamic keys on class/interface type.", ); let least: boolean = false; for (const p of object.properties) { const name: string = JSON.stringify(p.key.getSoleLiteral()!); const value: Metadata = p.value; if (value.functions.length) { least ||= true; if (validity === false) { if (value.functions.length !== 1 || value.size() !== 1) output.push( `LLM application's function (${name}) type does not allow union type.`, ); if (value.isRequired() === false) output.push( `LLM application's function (${name}) type must be required.`, ); if (value.nullable === true) output.push( `LLM application's function (${name}) type must not be nullable.`, ); } const description: string | undefined = concatDescription( JsonApplicationProgrammer.writeDescription({ description: p.description ?? p.jsDocTags.find((tag) => tag.name === "description")?.text?.[0] ?.text ?? null, jsDocTags: p.jsDocTags, kind: "summary", }), ); if (description !== undefined && description.length > 1_024) output.push( `LLM application's function (${name}) description must not exceed 1,024 characters.`, ); } } if (least === false) output.push( "LLM application's target type must have at least a function type.", ); } return output; }; const validateFunction = (name: string, func: MetadataFunction): string[] => { const output: string[] = []; const prefix: string = `LLM application's function (${JSON.stringify(name)})`; if (func.output.size() && func.output.isRequired() === false) output.push( `${prefix} return type cannot be optional (union with undefined).`, ); if (/^[0-9]/.test(name[0] ?? "") === true) output.push(`${prefix} name cannot start with a number.`); if (/^[a-zA-Z0-9_-]+$/.test(name) === false) output.push( `${prefix} name must contain only alphanumeric characters, underscores, or hyphens.`, ); if (name.length > 64) output.push(`${prefix} name cannot exceed 64 characters.`); if (func.parameters.length !== 0 && func.parameters.length !== 1) output.push( `${prefix} must have exactly one parameter or no parameters.`, ); if (func.parameters.length !== 0) { const type: Metadata = func.parameters[0]!.type; if (type.size() !== 1 || type.objects.length !== 1) output.push(`${prefix} parameter must be a single object type.`); else { if ( type.objects[0]!.type.properties.some( (p) => p.key.isSoleLiteral() === false, ) ) output.push(`${prefix} parameter cannot have dynamic property keys.`); if (type.isRequired() === false) output.push( `${prefix} parameter cannot be optional (union with undefined).`, ); if (type.nullable === true) output.push(`${prefix} parameter cannot be nullable.`); } } return output; }; export const write = (props: { context: ITypiaContext; modulo: ts.LeftHandSideExpression; metadata: Metadata; config?: Partial< ILlmSchema.IConfig & { equals: boolean; } >; name?: string; }): ILlmApplication => { const metadata: Metadata = Metadata.unalias(props.metadata); const functionParameters: Record<string, MetadataParameter> = Object.fromEntries( metadata.objects[0]!.type.properties.filter( (p) => p.key.isSoleLiteral() && p.value.size() === 1 && p.value.nullable === false && p.value.isRequired() === true && Metadata.unalias(p.value).functions.length === 1, ) .filter( (p) => p.jsDocTags.find( (tag) => tag.name === "hidden" || tag.name === "internal", ) === undefined, ) .map((p) => [ p.key.getSoleLiteral()!, Metadata.unalias(p.value).functions[0]!.parameters[0]!, ]), ); const errorMessages: string[] = []; const application: IJsonSchemaApplication<"3.1"> = JsonApplicationProgrammer.write({ version: "3.1", metadata, filter: (p) => p.jsDocTags.some((tag) => tag.name === "human") === false, }); const functions: Array<ILlmFunction | null> = application.functions.map( (func) => writeFunction({ context: props.context, modulo: props.modulo, className: props.name, config: props.config, components: application.components, function: func, errors: errorMessages, parameter: functionParameters[func.name] ?? null, }), ); if (functions.some((func) => func === null)) throw new Error( "Failed to write LLM application:\n\n" + errorMessages.map((str) => ` - ${str}`).join("\n"), ); return { config: { ...LlmSchemaComposer.getConfig(props.config), separate: null, validate: null, }, functions: functions.filter((f) => f !== null), }; }; const writeFunction = (props: { context: ITypiaContext; modulo: ts.LeftHandSideExpression; components: OpenApi.IComponents; function: IJsonSchemaApplication.IFunction<OpenApi.IJsonSchema>; parameter: MetadataParameter | null; errors: string[]; className?: string; config: | Partial< ILlmSchema.IConfig & { equals: boolean; } > | undefined; }): ILlmFunction | null => { const config: ILlmSchema.IConfig = LlmSchemaComposer.getConfig( props.config, ); const parameters: ILlmSchema.IParameters | null = writeParameters({ ...props, config, accessor: `$input.${props.function.name}.parameters`, }); if (parameters === null) return null; const output: ILlmSchema | null | undefined = writeOutput({ parameters, config, components: props.components, schema: props.function.output?.schema ?? null, errors: props.errors, accessor: `$input.${props.function.name}.output`, }); if (output === null) return null; if ( output && LlmTypeChecker.isReference(output) === false && output.description === undefined && !!props.function.output?.description?.length ) output.description = props.function.output.description; return { name: props.function.name, parameters, output: output ?? undefined, description: (() => { if ( !props.function.summary?.length || !props.function.description?.length ) return props.function.summary || props.function.description; const summary: string = props.function.summary.endsWith(".") ? props.function.summary.slice(0, -1) : props.function.summary; return props.function.description.startsWith(summary) ? props.function.description : summary + ".\n\n" + props.function.description; })(), deprecated: props.function.deprecated, tags: props.function.tags, validate: writeValidator({ context: props.context, modulo: props.modulo, parameter: props.parameter, name: props.function.name, className: props.className, equals: props.config?.equals ?? false, }), }; }; const writeParameters = (props: { components: OpenApi.IComponents; function: IJsonSchemaApplication.IFunction<OpenApi.IJsonSchema>; errors: string[]; accessor: string; config: ILlmSchema.IConfig; }): ILlmSchema.IParameters | null => { const schema: OpenApi.IJsonSchema.IObject | OpenApi.IJsonSchema.IReference = (props.function.parameters[0]?.schema as | OpenApi.IJsonSchema.IObject | OpenApi.IJsonSchema.IReference) ?? { type: "object", properties: {}, additionalProperties: false, required: [], }; const result: IResult<ILlmSchema.IParameters, IOpenApiSchemaError> = LlmSchemaComposer.parameters({ config: props.config, components: props.components, schema: { ...schema, title: schema.title ?? props.function.parameters[0]?.title, description: schema.description ?? props.function.parameters[0]?.description, }, accessor: props.accessor, }); if (result.success === false) { props.errors.push( ...result.error.reasons.map((r) => ` - ${r.accessor}: ${r.message}`), ); return null; } return result.value; }; const writeOutput = (props: { parameters: ILlmSchema.IParameters; components: OpenApi.IComponents; config: ILlmSchema.IConfig; schema: OpenApi.IJsonSchema | null; errors: string[]; accessor: string; }): ILlmSchema | null | undefined => { if (props.schema === null) return undefined; const result: IResult<ILlmSchema, IOpenApiSchemaError> = LlmSchemaComposer.schema({ config: props.config, components: props.components, schema: props.schema, $defs: (props.parameters as any).$defs, accessor: props.accessor, }); if (result.success === false) { props.errors.push( ...result.error.reasons.map((r) => ` - ${r.accessor}: ${r.message}`), ); return null; } return result.value; }; const writeValidator = (props: { context: ITypiaContext; modulo: ts.LeftHandSideExpression; parameter: MetadataParameter | null; name: string; equals: boolean; className?: string; }): ((props: unknown) => IValidation<unknown>) => { if (props.parameter === null) return ValidateProgrammer.write({ ...props, type: props.context.checker.getTypeFromTypeNode( TypeFactory.keyword("any"), ), config: { equals: props.equals, }, name: undefined, }) as any; const type: ts.Type | undefined = props.parameter.tsType; if (type === undefined) // unreachable throw new Error( "Failed to write LLM application's function validator. You don't have to call `LlmApplicationOfValidator.write()` function by yourself, but only by the `typia.llm.applicationOfValidate()` function.", ); return ValidateProgrammer.write({ ...props, type: props.parameter.tsType!, config: { equals: props.equals, }, name: props.className ? `Parameters<${props.className}[${JSON.stringify(props.name)}]>[0]` : undefined, }) satisfies ts.CallExpression as any as ( props: unknown, ) => IValidation<unknown>; }; } const concatDescription = (p: { summary?: string | undefined; description?: string | undefined; }): string | undefined => { if (!p.summary?.length || !p.description?.length) return p.summary ?? p.description; const summary: string = p.summary.endsWith(".") ? p.summary.slice(0, -1) : p.summary; return p.description.startsWith(summary) ? p.description : summary + ".\n\n" + p.description; };