diff --git a/README.md b/README.md index 765723b..05d128a 100644 --- a/README.md +++ b/README.md @@ -72,6 +72,7 @@ Here is an example code utilizing the `@samchon/openapi` for LLM function callin ```typescript import { HttpLlm, + IChatGptSchema, IHttpLlmApplication, IHttpLlmFunction, OpenApi, @@ -95,13 +96,17 @@ const main = async (): Promise => { // convert to emended OpenAPI document, // and compose LLM function calling application const document: OpenApi.IDocument = OpenApi.convert(swagger); - const application: IHttpLlmApplication = HttpLlm.application(document); + const application: IHttpLlmApplication<"chatgpt"> = HttpLlm.application({ + model: "chatgpt", + document, + }); // Let's imagine that LLM has selected a function to call - const func: IHttpLlmFunction | undefined = application.functions.find( - // (f) => f.name === "llm_selected_fuction_name" - (f) => f.path === "/bbs/articles" && f.method === "post", - ); + const func: IHttpLlmFunction | undefined = + application.functions.find( + // (f) => f.name === "llm_selected_fuction_name" + (f) => f.path === "/bbs/articles" && f.method === "post", + ); if (func === undefined) throw new Error("No matched function exists."); // actual execution is by yourself diff --git a/examples/function-calling/arguments/chatgpt.example.input.json b/examples/function-calling/arguments/chatgpt.example.input.json index 9dff986..99ec23e 100644 --- a/examples/function-calling/arguments/chatgpt.example.input.json +++ b/examples/function-calling/arguments/chatgpt.example.input.json @@ -1,4 +1,4 @@ { - "name": "John Doe", - "age": 30 + "name": "name", + "age": 0 } \ No newline at end of file diff --git a/examples/function-calling/arguments/chatgpt.sale.input.json b/examples/function-calling/arguments/chatgpt.sale.input.json index e4ca85b..b80b752 100644 --- a/examples/function-calling/arguments/chatgpt.sale.input.json +++ b/examples/function-calling/arguments/chatgpt.sale.input.json @@ -88,7 +88,7 @@ ], "stocks": [ { - "name": "Surface Pro 9 i3/8GB/128GB", + "name": "Surface Pro 9 (i3, 8GB, 128GB)", "price": { "nominal": 1000000, "real": 899000 @@ -110,7 +110,7 @@ ] }, { - "name": "Surface Pro 9 i3/16GB/256GB", + "name": "Surface Pro 9 (i3, 16GB, 256GB)", "price": { "nominal": 1200000, "real": 1099000 @@ -132,7 +132,7 @@ ] }, { - "name": "Surface Pro 9 i3/16GB/512GB", + "name": "Surface Pro 9 (i3, 16GB, 512GB)", "price": { "nominal": 1400000, "real": 1299000 @@ -154,7 +154,7 @@ ] }, { - "name": "Surface Pro 9 i5/16GB/256GB", + "name": "Surface Pro 9 (i5, 16GB, 256GB)", "price": { "nominal": 1500000, "real": 1399000 @@ -176,7 +176,7 @@ ] }, { - "name": "Surface Pro 9 i5/32GB/512GB", + "name": "Surface Pro 9 (i5, 32GB, 512GB)", "price": { "nominal": 1800000, "real": 1699000 @@ -198,7 +198,7 @@ ] }, { - "name": "Surface Pro 9 i7/16GB/512GB", + "name": "Surface Pro 9 (i7, 16GB, 512GB)", "price": { "nominal": 1800000, "real": 1699000 @@ -220,7 +220,7 @@ ] }, { - "name": "Surface Pro 9 i7/32GB/512GB", + "name": "Surface Pro 9 (i7, 32GB, 512GB)", "price": { "nominal": 2000000, "real": 1899000 @@ -250,7 +250,7 @@ "options": [], "stocks": [ { - "name": "Extended Warranty Program", + "name": "Surface Pro 9 Warranty", "price": { "nominal": 100000, "real": 89000 @@ -267,7 +267,7 @@ "options": [], "stocks": [ { - "name": "Magnetic Keyboard", + "name": "Surface Pro 9 Keyboard", "price": { "nominal": 200000, "real": 169000 @@ -283,10 +283,12 @@ ], "tags": [ "Surface Pro 9", - "laptop", - "tablet", "2-in-1", + "tablet", + "laptop", "Microsoft", - "technology" + "5G", + "detachable keyboard", + "touchscreen" ] } \ No newline at end of file diff --git a/examples/function-calling/arguments/gemini.example.input.json b/examples/function-calling/arguments/gemini.example.input.json index e56ddcf..06531c8 100644 --- a/examples/function-calling/arguments/gemini.example.input.json +++ b/examples/function-calling/arguments/gemini.example.input.json @@ -1,4 +1,4 @@ { "age": 30, - "name": "test" + "name": "John" } \ No newline at end of file diff --git a/examples/function-calling/arguments/gemini.sale.input.json b/examples/function-calling/arguments/gemini.sale.input.json index 1b1ca87..04c253c 100644 --- a/examples/function-calling/arguments/gemini.sale.input.json +++ b/examples/function-calling/arguments/gemini.sale.input.json @@ -1,85 +1,35 @@ { - "opened_at": "2024-01-01T00:00:00.000Z", + "closed_at": "2024-07-19T12:00:00.000Z", "content": { "title": "Surface Pro 9", + "format": "md", + "files": [], + "body": "The Surface Pro 9 is a versatile 2-in-1 device that combines the power of a laptop with the flexibility of a tablet. It features advanced technology, making it suitable for both professional and personal use.\\n\\n- \\\"Unleash Your Creativity Anywhere\\\": The Surface Pro 9 is designed for those who need power and portability, making it perfect for creative professionals and students alike.\\n- \\\"The Ultimate 2-in-1 Experience\\\": With its detachable keyboard and touchscreen capabilities, the Surface Pro 9 adapts to your needs, whether you're working, studying, or relaxing.\\n- \\\"Stay Connected with 5G\\\": Experience lightning-fast internet speeds and seamless connectivity, no matter where you are.\\n- \\\"Power Meets Flexibility\\\": The Surface Pro 9 combines the performance of a laptop with the convenience of a tablet, making it the ideal device for multitasking.\\n \\nIn summary, the Surface Pro 9 stands out as a powerful and flexible device, perfect for users who require both performance and portability. With its advanced features and sleek design, it is an excellent choice for anyone looking to enhance their productivity and creativity. Whether for work or play, the Surface Pro 9 is ready to meet your needs.", "thumbnails": [ { - "extension": "jpeg", "name": "microsoft-surface-pro-9-thumbnail-1", + "extension": "jpeg", "url": "https://serpapi.com/searches/673d3a37e45f3316ecd8ab3e/images/1be25e6e2b1fb7509f1af89c326cb41749301b94375eb5680b9bddcdf88fabcb.jpeg" }, { + "name": "microsoft-surface-pro-9-thumbnail-2", "url": "https://serpapi.com/searches/673d3a37e45f3316ecd8ab3e/images/1be25e6e2b1fb750d6c1bc749467f5aba0340886f4f4943fe72302c5e658b15a.jpeg", - "extension": "jpeg", - "name": "microsoft-surface-pro-9-thumbnail-2" + "extension": "jpeg" }, { "name": "microsoft-surface-pro-9-thumbnail-3", - "url": "https://serpapi.com/searches/673d3a37e45f3316ecd8ab3e/images/1be25e6e2b1fb7505946d975aac683f8826bcb8c509672de4a5f8c71f149fdef.jpeg", - "extension": "jpeg" + "extension": "jpeg", + "url": "https://serpapi.com/searches/673d3a37e45f3316ecd8ab3e/images/1be25e6e2b1fb7505946d975aac683f8826bcb8c509672de4a5f8c71f149fdef.jpeg" } - ], - "files": [], - "body": "The Surface Pro 9 is a versatile 2-in-1 device that combines the power of a laptop with the flexibility of a tablet. It features advanced technology, making it suitable for both professional and personal use.\\n\\n- \\\"Unleash Your Creativity Anywhere\\\": The Surface Pro 9 is designed for those who need power and portability, making it perfect for creative professionals and students alike.\\n- \\\"The Ultimate 2-in-1 Experience\\\": With its detachable keyboard and touchscreen capabilities, the Surface Pro 9 adapts to your needs, whether you're working, studying, or relaxing.\\n- \\\"Stay Connected with 5G\\\": Experience lightning-fast internet speeds and seamless connectivity, no matter where you are.\\n- \\\"Power Meets Flexibility\\\": The Surface Pro 9 combines the performance of a laptop with the convenience of a tablet, making it the ideal device for multitasking.\\n\\nIn summary, the Surface Pro 9 stands out as a powerful and flexible device, perfect for users who require both performance and portability. With its advanced features and sleek design, it is an excellent choice for anyone looking to enhance their productivity and creativity. Whether for work or play, the Surface Pro 9 is ready to meet your needs.", - "format": "md" + ] }, "units": [ { "required": true, - "primary": true, - "options": [ - { - "variable": true, - "candidates": [ - { - "name": "Intel Core i3" - }, - { - "name": "Intel Core i5" - }, - { - "name": "Intel Core i7" - } - ], - "name": "CPU", - "type": "select" - }, - { - "candidates": [ - { - "name": "8 GB" - }, - { - "name": "16 GB" - }, - { - "name": "32 GB" - } - ], - "variable": true, - "type": "select", - "name": "RAM" - }, - { - "name": "Storage", - "variable": true, - "candidates": [ - { - "name": "128 GB" - }, - { - "name": "256 GB" - }, - { - "name": "512 GB" - } - ], - "type": "select" - } - ], + "name": "Surface Pro 9 Entity", "stocks": [ { - "name": "i3, 8 GB, 128 GB", + "quantity": 1000, "choices": [ { "candidate_index": 0, @@ -90,22 +40,17 @@ "candidate_index": 0 }, { - "option_index": 2, - "candidate_index": 0 + "candidate_index": 0, + "option_index": 2 } ], "price": { - "nominal": 1000000, - "real": 899000 + "real": 899000, + "nominal": 1000000 }, - "quantity": 1000 + "name": "(i3, 8 GB, 128 GB)" }, { - "price": { - "nominal": 1200000, - "real": 1099000 - }, - "quantity": 1000, "choices": [ { "option_index": 0, @@ -116,18 +61,23 @@ "candidate_index": 1 }, { - "option_index": 2, - "candidate_index": 1 + "candidate_index": 1, + "option_index": 2 } ], - "name": "i3, 16 GB, 256 GB" + "name": "(i3, 16 GB, 256 GB)", + "quantity": 1000, + "price": { + "real": 1099000, + "nominal": 1200000 + } }, { "quantity": 1000, - "name": "i3, 16 GB, 512 GB", + "name": "(i3, 16 GB, 512 GB)", "price": { - "nominal": 1400000, - "real": 1299000 + "real": 1299000, + "nominal": 1400000 }, "choices": [ { @@ -145,14 +95,16 @@ ] }, { + "quantity": 1000, + "name": "(i5, 16 GB, 256 GB)", "price": { - "real": 1399000, - "nominal": 1500000 + "nominal": 1500000, + "real": 1399000 }, "choices": [ { - "option_index": 0, - "candidate_index": 1 + "candidate_index": 1, + "option_index": 0 }, { "option_index": 1, @@ -162,19 +114,21 @@ "option_index": 2, "candidate_index": 1 } - ], - "name": "i5, 16 GB, 256 GB", - "quantity": 1000 + ] }, { + "price": { + "real": 1699000, + "nominal": 1800000 + }, "choices": [ { "option_index": 0, "candidate_index": 1 }, { - "candidate_index": 2, - "option_index": 1 + "option_index": 1, + "candidate_index": 2 }, { "candidate_index": 2, @@ -182,37 +136,31 @@ } ], "quantity": 1000, - "name": "i5, 32 GB, 512 GB", - "price": { - "real": 1699000, - "nominal": 1800000 - } + "name": "(i5, 32 GB, 512 GB)" }, { - "name": "i7, 16 GB, 512 GB", - "quantity": 1000, + "name": "(i7, 16 GB, 512 GB)", "choices": [ { - "candidate_index": 2, - "option_index": 0 + "option_index": 0, + "candidate_index": 2 }, { "candidate_index": 1, "option_index": 1 }, { - "option_index": 2, - "candidate_index": 2 + "candidate_index": 2, + "option_index": 2 } ], + "quantity": 1000, "price": { "real": 1699000, "nominal": 1800000 } }, { - "quantity": 1000, - "name": "i7, 32 GB, 512 GB", "choices": [ { "candidate_index": 2, @@ -223,73 +171,116 @@ "option_index": 1 }, { - "candidate_index": 2, - "option_index": 2 + "option_index": 2, + "candidate_index": 2 } ], + "name": "(i7, 32 GB, 512 GB)", "price": { "real": 1899000, "nominal": 2000000 - } + }, + "quantity": 1000 } ], - "name": "Surface Pro 9 Entity" + "primary": true, + "options": [ + { + "candidates": [ + { + "name": "Intel Core i3" + }, + { + "name": "Intel Core i5" + }, + { + "name": "Intel Core i7" + } + ], + "name": "CPU", + "type": "select", + "variable": true + }, + { + "type": "select", + "candidates": [ + { + "name": "8 GB" + }, + { + "name": "16 GB" + }, + { + "name": "32 GB" + } + ], + "name": "RAM", + "variable": true + }, + { + "variable": true, + "candidates": [ + { + "name": "128 GB" + }, + { + "name": "256 GB" + }, + { + "name": "512 GB" + } + ], + "type": "select", + "name": "Storage" + } + ] }, { - "options": [], - "required": false, - "name": "Warranty Program", "primary": false, + "options": [], "stocks": [ { "name": "1 year warranty", - "quantity": 10000, "price": { - "nominal": 100000, - "real": 89000 + "real": 89000, + "nominal": 100000 }, - "choices": [] + "choices": [], + "quantity": 10000 } - ] + ], + "required": false, + "name": "Warranty Program" }, { "name": "Magnetic Keyboard", "required": false, + "options": [], + "primary": false, "stocks": [ { - "price": { - "nominal": 200000, - "real": 169000 - }, "name": "Black", "quantity": 8000, - "choices": [] + "choices": [], + "price": { + "real": 169000, + "nominal": 200000 + } } - ], - "options": [], - "primary": false + ] } ], "channels": [ { "category_codes": [ + "electronics", "laptops", - "macbooks", "2_in_1_laptops" ], "code": "samchon" } ], - "closed_at": "2024-07-31T12:00:00.000Z", - "tags": [ - "surface", - "pro", - "9", - "2_in_1", - "laptop", - "tablet", - "5g", - "microsoft" - ], - "section_code": "general" + "opened_at": "2023-12-25T12:00:00.000Z", + "section_code": "general", + "tags": [] } \ No newline at end of file diff --git a/examples/function-calling/schemas/chatgpt.sale.schema.json b/examples/function-calling/schemas/chatgpt.sale.schema.json index 9c8c923..022285c 100644 --- a/examples/function-calling/schemas/chatgpt.sale.schema.json +++ b/examples/function-calling/schemas/chatgpt.sale.schema.json @@ -372,5 +372,6 @@ "required": [ "input" ], - "additionalProperties": false + "additionalProperties": false, + "$defs": {} } \ No newline at end of file diff --git a/package.json b/package.json index 39ffea3..ac9f56b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@samchon/openapi", - "version": "2.0.0-dev.20241123-3", + "version": "2.0.0-dev.20241123-5", "description": "OpenAPI definitions and converters for 'typia' and 'nestia'.", "main": "./lib/index.js", "module": "./lib/index.mjs", diff --git a/src/HttpLlm.ts b/src/HttpLlm.ts index cbbf24b..98235c6 100644 --- a/src/HttpLlm.ts +++ b/src/HttpLlm.ts @@ -90,38 +90,31 @@ export namespace HttpLlm { (props.document as OpenApi.IDocument)["x-samchon-emend-version"] === "2.0" ? HttpMigration.application(props.document as OpenApi.IDocument) : (props.document as IHttpMigrateApplication); - return HttpLlmConverter.compose({ + return HttpLlmConverter.application({ migrate, model: props.model, - options: (props.model === "chatgpt" + options: (props.model === "chatgpt" || props.model === "3.1" ? ({ separate: (props.options - ?.separate as IHttpLlmApplication.IChatGptOptions["separate"]) ?? + ?.separate as IHttpLlmApplication.IOptions["separate"]) ?? null, reference: - (props.options as IHttpLlmApplication.IChatGptOptions | undefined) + (props.options as IHttpLlmApplication.IOptions | undefined) ?.reference ?? false, constraint: - (props.options as IHttpLlmApplication.IChatGptOptions | undefined) + (props.options as IHttpLlmApplication.IOptions | undefined) ?.constraint ?? false, - } satisfies IHttpLlmApplication.IChatGptOptions) + } satisfies IHttpLlmApplication.IOptions) : ({ separate: - (props.options?.separate as IHttpLlmApplication.ICommonOptions< - Exclude - >["separate"]) ?? null, + (props.options + ?.separate as IHttpLlmApplication.IOptions["separate"]) ?? + null, recursive: - ( - props.options as - | IHttpLlmApplication.ICommonOptions< - Exclude - > - | undefined - )?.recursive ?? 3, - } satisfies IHttpLlmApplication.ICommonOptions< - Exclude - >)) as IHttpLlmApplication.IOptions, + (props.options as IHttpLlmApplication.IOptions | undefined) + ?.recursive ?? 3, + } satisfies IHttpLlmApplication.IOptions)) as IHttpLlmApplication.IOptions, }); }; diff --git a/src/converters/ChatGptConverter.ts b/src/converters/ChatGptConverter.ts index 31fe13e..d751322 100644 --- a/src/converters/ChatGptConverter.ts +++ b/src/converters/ChatGptConverter.ts @@ -1,177 +1,85 @@ import { OpenApi } from "../OpenApi"; import { IChatGptSchema } from "../structures/IChatGptSchema"; -import { ILlmApplication } from "../structures/ILlmApplication"; +import { ILlmSchemaV3_1 } from "../structures/ILlmSchemaV3_1"; import { ChatGptTypeChecker } from "../utils/ChatGptTypeChecker"; -import { OpenApiContraintShifter } from "../utils/OpenApiContraintShifter"; +import { LlmTypeCheckerV3_1 } from "../utils/LlmTypeCheckerV3_1"; import { OpenApiTypeChecker } from "../utils/OpenApiTypeChecker"; +import { LlmConverterV3_1 } from "./LlmConverterV3_1"; export namespace ChatGptConverter { export const parameters = (props: { - options: Omit; + config: IChatGptSchema.IConfig; components: OpenApi.IComponents; schema: OpenApi.IJsonSchema.IObject; }): IChatGptSchema.IParameters | null => { - const $defs: Record = {}; - const res: IChatGptSchema.IParameters | null = schema({ - options: props.options, - components: props.components, - schema: props.schema, - $defs, - }) as IChatGptSchema.IParameters | null; - if (res === null) return null; - else if (Object.keys($defs).length) res.$defs = $defs; - return res; + const params: ILlmSchemaV3_1.IParameters | null = + LlmConverterV3_1.parameters({ + config: props.config, + components: props.components, + schema: props.schema, + }); + if (params === null) return null; + for (const key of Object.keys(params.$defs)) + params.$defs[key] = transform(params.$defs[key]); + for (const key of Object.keys(params.properties)) + params.properties[key] = transform(params.properties[key]); + return params; }; export const schema = (props: { - options: Omit; + config: IChatGptSchema.IConfig; components: OpenApi.IComponents; $defs: Record; schema: OpenApi.IJsonSchema; }): IChatGptSchema | null => { - const union: Array = []; + const oldbie: Set = new Set(Object.keys(props.$defs)); + const schema: ILlmSchemaV3_1 | null = LlmConverterV3_1.schema({ + config: props.config, + components: props.components, + $defs: props.$defs, + schema: props.schema, + }); + if (schema === null) return null; + for (const key of Object.keys(props.$defs)) + if (oldbie.has(key) === false) + props.$defs[key] = transform(props.$defs[key]); + return transform(schema); + }; + + const transform = (schema: ILlmSchemaV3_1): IChatGptSchema => { + const union: Array = []; const attribute: IChatGptSchema.__IAttribute = { - title: props.schema.title, - description: props.schema.description, - example: props.schema.example, - examples: props.schema.examples, + title: schema.title, + description: schema.description, + example: schema.example, + examples: schema.examples, ...Object.fromEntries( - Object.entries(props.schema).filter( + Object.entries(schema).filter( ([key, value]) => key.startsWith("x-") && value !== undefined, ), ), }; - const visit = (input: OpenApi.IJsonSchema): number => { - if (OpenApiTypeChecker.isOneOf(input)) { - input.oneOf.forEach(visit); - return 0; - } else if (OpenApiTypeChecker.isReference(input)) { - const key: string = input.$ref.split("#/components/schemas/")[1]; - const target: OpenApi.IJsonSchema | undefined = - props.components.schemas?.[key]; - if (target === undefined) return 0; - if ( - props.options.reference === true || - OpenApiTypeChecker.isRecursiveReference({ - components: props.components, - schema: input, - }) - ) { - const out = () => - union.push({ - ...input, - $ref: `#/$defs/${key}`, - title: undefined, - description: undefined, - }); - if (props.$defs[key] !== undefined) return out(); - props.$defs[key] = {}; - const converted: IChatGptSchema | null = schema({ - options: props.options, - components: props.components, - $defs: props.$defs, - schema: target, - }); - if (converted === null) return union.push(null); - converted.description = OpenApiTypeChecker.writeReferenceDescription({ - components: props.components, - $ref: input.$ref, - description: converted.description, - escape: false, - }); - props.$defs[key] = converted; - return out(); - } else { - const length: number = union.length; - visit(target); - if (length === union.length - 1 && union[union.length - 1] !== null) - union[union.length - 1] = { - ...union[union.length - 1]!, - description: OpenApiTypeChecker.writeReferenceDescription({ - components: props.components, - $ref: input.$ref, - description: union[union.length - 1]!.description, - escape: true, - }), - }; - else - attribute.description = - OpenApiTypeChecker.writeReferenceDescription({ - components: props.components, - $ref: input.$ref, - description: attribute.description, - escape: true, - }); - return union.length; - } - } else if (OpenApiTypeChecker.isObject(input)) { - const properties: Record = - Object.entries(input.properties || {}).reduce( - (acc, [key, value]) => { - const converted: IChatGptSchema | null = schema({ - options: props.options, - components: props.components, - $defs: props.$defs, - schema: value, - }); - if (converted === null) return acc; - acc[key] = converted; - return acc; - }, - {} as Record, - ); - if (Object.values(properties).some((v) => v === null)) - return union.push(null); - if (!!input.additionalProperties === null) return union.push(null); - return union.push({ + const visit = (input: ILlmSchemaV3_1): void => { + if (LlmTypeCheckerV3_1.isOneOf(input)) input.oneOf.forEach(visit); + else if (LlmTypeCheckerV3_1.isArray(input)) + union.push({ ...input, - properties: properties as Record, - additionalProperties: false, - required: Object.keys(properties), + items: transform(input.items), }); - } else if (OpenApiTypeChecker.isArray(input)) { - const items: IChatGptSchema | null = schema({ - options: props.options, - components: props.components, - $defs: props.$defs, - schema: input.items, + else if (LlmTypeCheckerV3_1.isObject(input)) + union.push({ + ...input, + properties: Object.fromEntries( + Object.entries(input.properties).map(([key, value]) => [ + key, + transform(value), + ]), + ), }); - if (items === null) return union.push(null); - return union.push( - (props.options.constraint - ? (x: IChatGptSchema.IArray) => x - : (x: IChatGptSchema.IArray) => - OpenApiContraintShifter.shiftArray(x))({ - ...input, - items, - }), - ); - } else if (OpenApiTypeChecker.isString(input)) - return union.push( - (props.options.constraint - ? (x: IChatGptSchema.IString) => x - : (x: IChatGptSchema.IString) => - OpenApiContraintShifter.shiftString(x))({ - ...input, - }), - ); - else if ( - OpenApiTypeChecker.isNumber(input) || - OpenApiTypeChecker.isInteger(input) - ) - return union.push( - (props.options.constraint - ? (x: IChatGptSchema.INumber | IChatGptSchema.IInteger) => x - : (x: IChatGptSchema.INumber | IChatGptSchema.IInteger) => - OpenApiContraintShifter.shiftNumeric(x))({ - ...input, - }), - ); - else if (OpenApiTypeChecker.isConstant(input)) return 0; - else if (OpenApiTypeChecker.isTuple(input)) return union.push(null); - else return union.push({ ...input }); + else if (LlmTypeCheckerV3_1.isConstant(input) === false) + union.push(input); }; - const visitConstant = (input: OpenApi.IJsonSchema): void => { + const visitConstant = (input: ILlmSchemaV3_1): void => { const insert = (value: any): void => { const matched: IChatGptSchema.IString | undefined = union.find( (u) => @@ -190,26 +98,10 @@ export namespace ChatGptConverter { if (OpenApiTypeChecker.isConstant(input)) insert(input.const); else if (OpenApiTypeChecker.isOneOf(input)) input.oneOf.forEach(visitConstant); - else if ( - props.options.reference === false && - OpenApiTypeChecker.isReference(input) && - OpenApiTypeChecker.isRecursiveReference({ - components: props.components, - schema: input, - }) === false - ) { - const target: OpenApi.IJsonSchema | undefined = - props.components.schemas?.[ - input.$ref.split("#/components/schemas/")[1] - ]; - if (target !== undefined) visitConstant(target); - } }; - visit(props.schema); - visitConstant(props.schema); - - if (union.some((u) => u === null)) return null; - else if (union.length === 0) + visit(schema); + visitConstant(schema); + if (union.length === 0) return { ...attribute, type: undefined, @@ -217,18 +109,18 @@ export namespace ChatGptConverter { else if (union.length === 1) return { ...attribute, - ...union[0]!, + ...union[0], description: ChatGptTypeChecker.isReference(union[0]!) ? undefined - : union[0]!.description, + : union[0].description, }; return { ...attribute, anyOf: union.map((u) => ({ - ...u!, - description: ChatGptTypeChecker.isReference(u!) + ...u, + description: ChatGptTypeChecker.isReference(u) ? undefined - : u!.description, + : u.description, })), }; }; diff --git a/src/converters/GeminiConverter.ts b/src/converters/GeminiConverter.ts index 48071f3..4c498df 100644 --- a/src/converters/GeminiConverter.ts +++ b/src/converters/GeminiConverter.ts @@ -9,16 +9,23 @@ export namespace GeminiConverter { export const parameters = (props: { components: OpenApi.IComponents; schema: OpenApi.IJsonSchema; - recursive: false | number; + config: IGeminiSchema.IConfig; }): IGeminiSchema.IParameters | null => schema(props) as IGeminiSchema.IParameters | null; export const schema = (props: { components: OpenApi.IComponents; schema: OpenApi.IJsonSchema; - recursive: false | number; + config: IGeminiSchema.IConfig; }): IGeminiSchema | null => { - const schema: ILlmSchemaV3 | null = LlmConverterV3.schema(props); + const schema: ILlmSchemaV3 | null = LlmConverterV3.schema({ + components: props.components, + schema: props.schema, + config: { + recursive: props.config.recursive, + constraint: false, + }, + }); if (schema === null) return null; let union: boolean = false; diff --git a/src/converters/HttpLlmConverter.ts b/src/converters/HttpLlmConverter.ts index b68f210..45db7e1 100644 --- a/src/converters/HttpLlmConverter.ts +++ b/src/converters/HttpLlmConverter.ts @@ -8,9 +8,10 @@ import { ChatGptConverter } from "./ChatGptConverter"; import { GeminiConverter } from "./GeminiConverter"; import { LlmConverterV3 } from "./LlmConverterV3"; import { LlmConverterV3_1 } from "./LlmConverterV3_1"; +import { LlmSchemaConverter } from "./LlmSchemaConverter"; export namespace HttpLlmConverter { - export const compose = < + export const application = < Model extends IHttpLlmApplication.Model, Parameters extends IHttpLlmApplication.ModelParameters[Model] = IHttpLlmApplication.ModelParameters[Model], @@ -22,12 +23,7 @@ export namespace HttpLlmConverter { >(props: { model: Model; migrate: IHttpMigrateApplication; - options: IHttpLlmApplication.IOptions< - Model, - Parameters["properties"][string] extends IHttpLlmApplication.ModelSchema[Model] - ? Parameters["properties"][string] - : IHttpLlmApplication.ModelSchema[Model] - >; + options: IHttpLlmApplication.IOptions; }): IHttpLlmApplication => { // COMPOSE FUNCTIONS const errors: IHttpLlmApplication.IError[] = @@ -95,7 +91,7 @@ export namespace HttpLlmConverter { }; }; - export const separateParameters = < + export const separate = < Model extends IHttpLlmApplication.Model, Parameters extends IHttpLlmApplication.ModelParameters[Model] = IHttpLlmApplication.ModelParameters[Model], @@ -134,19 +130,14 @@ export namespace HttpLlmConverter { model: Model; components: OpenApi.IComponents; route: IHttpMigrateRoute; - options: IHttpLlmApplication.IOptions< - Model, - Parameters["properties"][string] extends IHttpLlmApplication.ModelSchema[Model] - ? Parameters["properties"][string] - : IHttpLlmApplication.ModelSchema[Model] - >; + options: IHttpLlmApplication.IOptions; }): IHttpLlmFunction | null => { const $defs: Record = {}; const cast = ( s: OpenApi.IJsonSchema, ): Parameters["properties"][string] | null => - CASTERS[props.model]({ - options: props.options as any, + LlmSchemaConverter.schema(props.model)({ + config: props.options as any, schema: s, components: props.components, $defs, @@ -217,7 +208,7 @@ export namespace HttpLlmConverter { strict: true, parameters, separated: props.options.separate - ? separateParameters({ + ? separate({ model: props.model, predicate: props.options.separate as any, parameters, @@ -242,51 +233,6 @@ export namespace HttpLlmConverter { }; } -const CASTERS = { - "3.0": (props: { - components: OpenApi.IComponents; - schema: OpenApi.IJsonSchema; - options: IHttpLlmApplication.IOptions<"3.0">; - }) => - LlmConverterV3.schema({ - components: props.components, - schema: props.schema, - recursive: props.options.recursive, - }), - "3.1": (props: { - components: OpenApi.IComponents; - schema: OpenApi.IJsonSchema; - options: IHttpLlmApplication.IOptions<"3.1">; - }) => - LlmConverterV3_1.schema({ - components: props.components, - schema: props.schema, - recursive: props.options.recursive, - }), - chatgpt: (props: { - components: OpenApi.IComponents; - schema: OpenApi.IJsonSchema; - $defs: Record; - options: Omit; - }) => - ChatGptConverter.schema({ - components: props.components, - schema: props.schema, - $defs: props.$defs, - options: props.options, - }), - gemini: (props: { - components: OpenApi.IComponents; - schema: OpenApi.IJsonSchema; - options: IHttpLlmApplication.IOptions<"gemini">; - }) => - GeminiConverter.schema({ - components: props.components, - schema: props.schema, - recursive: props.options.recursive, - }), -}; - const SEPARATORS = { "3.0": LlmConverterV3.separate, "3.1": LlmConverterV3_1.separate, diff --git a/src/converters/LlmConverterV3.ts b/src/converters/LlmConverterV3.ts index 3bccad1..af6004a 100644 --- a/src/converters/LlmConverterV3.ts +++ b/src/converters/LlmConverterV3.ts @@ -8,19 +8,19 @@ export namespace LlmConverterV3 { export const parameters = (props: { components: OpenApi.IComponents; schema: OpenApi.IJsonSchema.IObject; - recursive: false | number; + config: ILlmSchemaV3.IConfig; }): ILlmSchemaV3.IParameters | null => schema(props) as ILlmSchemaV3.IParameters | null; export const schema = (props: { components: OpenApi.IComponents; schema: OpenApi.IJsonSchema; - recursive: false | number; + config: Omit; }): ILlmSchemaV3 | null => { const resolved: OpenApi.IJsonSchema | null = OpenApiTypeChecker.escape({ components: props.components, schema: props.schema, - recursive: props.recursive, + recursive: props.config.recursive, }); if (resolved === null) return null; const downgraded: ILlmSchemaV3 = OpenApiV3Downgrader.downgradeSchema({ diff --git a/src/converters/LlmConverterV3_1.ts b/src/converters/LlmConverterV3_1.ts index 57fb396..7235fab 100644 --- a/src/converters/LlmConverterV3_1.ts +++ b/src/converters/LlmConverterV3_1.ts @@ -1,21 +1,203 @@ import { OpenApi } from "../OpenApi"; import { ILlmSchemaV3_1 } from "../structures/ILlmSchemaV3_1"; import { LlmTypeCheckerV3_1 } from "../utils/LlmTypeCheckerV3_1"; +import { OpenApiContraintShifter } from "../utils/OpenApiContraintShifter"; import { OpenApiTypeChecker } from "../utils/OpenApiTypeChecker"; +import { JsonDescriptionUtil } from "../utils/internal/JsonDescriptionUtil"; export namespace LlmConverterV3_1 { export const parameters = (props: { + config: ILlmSchemaV3_1.IConfig; components: OpenApi.IComponents; schema: OpenApi.IJsonSchema.IObject; - recursive: false | number; - }): ILlmSchemaV3_1.IParameters | null => - schema(props) as ILlmSchemaV3_1.IParameters | null; + }): ILlmSchemaV3_1.IParameters | null => { + const $defs: Record = {}; + const res: ILlmSchemaV3_1.IParameters | null = schema({ + config: props.config, + components: props.components, + schema: props.schema, + $defs, + }) as ILlmSchemaV3_1.IParameters | null; + if (res === null) return null; + res.$defs = $defs; + return res; + }; export const schema = (props: { + config: ILlmSchemaV3_1.IConfig; components: OpenApi.IComponents; + $defs: Record; schema: OpenApi.IJsonSchema; - recursive: false | number; - }): ILlmSchemaV3_1 | null => OpenApiTypeChecker.escape(props); + }): ILlmSchemaV3_1 | null => { + const union: Array = []; + const attribute: ILlmSchemaV3_1.__IAttribute = { + title: props.schema.title, + description: props.schema.description, + example: props.schema.example, + examples: props.schema.examples, + ...Object.fromEntries( + Object.entries(props.schema).filter( + ([key, value]) => key.startsWith("x-") && value !== undefined, + ), + ), + }; + const visit = (input: OpenApi.IJsonSchema): number => { + if (OpenApiTypeChecker.isOneOf(input)) { + input.oneOf.forEach(visit); + return 0; + } else if (OpenApiTypeChecker.isReference(input)) { + const key: string = input.$ref.split("#/components/schemas/")[1]; + const target: OpenApi.IJsonSchema | undefined = + props.components.schemas?.[key]; + if (target === undefined) return 0; + if ( + props.config.reference === true || + OpenApiTypeChecker.isRecursiveReference({ + components: props.components, + schema: input, + }) + ) { + const out = () => + union.push({ + ...input, + $ref: `#/$defs/${key}`, + title: undefined, + description: undefined, + }); + if (props.$defs[key] !== undefined) return out(); + props.$defs[key] = {}; + const converted: ILlmSchemaV3_1 | null = schema({ + config: props.config, + components: props.components, + $defs: props.$defs, + schema: target, + }); + if (converted === null) return union.push(null); + converted.description = JsonDescriptionUtil.cascade({ + prefix: "#/components/schemas/", + components: props.components, + $ref: input.$ref, + description: converted.description, + escape: false, + }); + props.$defs[key] = converted; + return out(); + } else { + const length: number = union.length; + visit(target); + if (length === union.length - 1 && union[union.length - 1] !== null) + union[union.length - 1] = { + ...union[union.length - 1]!, + description: JsonDescriptionUtil.cascade({ + prefix: "#/components/schemas/", + components: props.components, + $ref: input.$ref, + description: union[union.length - 1]!.description, + escape: true, + }), + }; + else + attribute.description = JsonDescriptionUtil.cascade({ + prefix: "#/components/schemas/", + components: props.components, + $ref: input.$ref, + description: attribute.description, + escape: true, + }); + return union.length; + } + } else if (OpenApiTypeChecker.isObject(input)) { + const properties: Record = + Object.entries(input.properties || {}).reduce( + (acc, [key, value]) => { + const converted: ILlmSchemaV3_1 | null = schema({ + config: props.config, + components: props.components, + $defs: props.$defs, + schema: value, + }); + if (converted === null) return acc; + acc[key] = converted; + return acc; + }, + {} as Record, + ); + if (Object.values(properties).some((v) => v === null)) + return union.push(null); + if (!!input.additionalProperties === null) return union.push(null); + return union.push({ + ...input, + properties: properties as Record, + additionalProperties: false, + required: Object.keys(properties), + }); + } else if (OpenApiTypeChecker.isArray(input)) { + const items: ILlmSchemaV3_1 | null = schema({ + config: props.config, + components: props.components, + $defs: props.$defs, + schema: input.items, + }); + if (items === null) return union.push(null); + return union.push( + (props.config.constraint + ? (x: ILlmSchemaV3_1.IArray) => x + : (x: ILlmSchemaV3_1.IArray) => + OpenApiContraintShifter.shiftArray(x))({ + ...input, + items, + }), + ); + } else if (OpenApiTypeChecker.isString(input)) + return union.push( + (props.config.constraint + ? (x: ILlmSchemaV3_1.IString) => x + : (x: ILlmSchemaV3_1.IString) => + OpenApiContraintShifter.shiftString(x))({ + ...input, + }), + ); + else if ( + OpenApiTypeChecker.isNumber(input) || + OpenApiTypeChecker.isInteger(input) + ) + return union.push( + (props.config.constraint + ? (x: ILlmSchemaV3_1.INumber | ILlmSchemaV3_1.IInteger) => x + : (x: ILlmSchemaV3_1.INumber | ILlmSchemaV3_1.IInteger) => + OpenApiContraintShifter.shiftNumeric(x))({ + ...input, + }), + ); + else if (OpenApiTypeChecker.isTuple(input)) return union.push(null); + else return union.push({ ...input }); + }; + visit(props.schema); + + if (union.some((u) => u === null)) return null; + else if (union.length === 0) + return { + ...attribute, + type: undefined, + }; + else if (union.length === 1) + return { + ...attribute, + ...union[0]!, + description: LlmTypeCheckerV3_1.isReference(union[0]!) + ? undefined + : union[0]!.description, + }; + return { + ...attribute, + oneOf: union.map((u) => ({ + ...u!, + description: LlmTypeCheckerV3_1.isReference(u!) + ? undefined + : u!.description, + })), + }; + }; export const separate = (props: { predicate: (schema: ILlmSchemaV3_1) => boolean; @@ -84,29 +266,11 @@ export namespace LlmConverterV3_1 { if (x !== null) llm.properties[key] = x; if (y !== null) human.properties[key] = y; } - if ( - typeof props.schema.additionalProperties === "object" && - props.schema.additionalProperties !== null - ) { - const [x, y] = separate({ - predicate: props.predicate, - schema: props.schema.additionalProperties, - }); - if (x !== null) llm.additionalProperties = x; - if (y !== null) human.additionalProperties = y; - } else { - llm.additionalProperties = false; - human.additionalProperties = false; - } + llm.additionalProperties = false; + human.additionalProperties = false; return [ - Object.keys(llm.properties).length === 0 && - llm.additionalProperties === false - ? null - : shrinkRequired(llm), - Object.keys(human.properties).length === 0 && - human.additionalProperties === false - ? null - : shrinkRequired(human), + Object.keys(llm.properties).length === 0 ? null : shrinkRequired(llm), + Object.keys(human.properties).length === 0 ? null : shrinkRequired(human), ]; }; diff --git a/src/converters/LlmSchemaConverter.ts b/src/converters/LlmSchemaConverter.ts new file mode 100644 index 0000000..b257857 --- /dev/null +++ b/src/converters/LlmSchemaConverter.ts @@ -0,0 +1,111 @@ +import { OpenApi } from "../OpenApi"; +import { IChatGptSchema } from "../structures/IChatGptSchema"; +import { IGeminiSchema } from "../structures/IGeminiSchema"; +import { ILlmApplication } from "../structures/ILlmApplication"; +import { ILlmSchemaV3 } from "../structures/ILlmSchemaV3"; +import { ILlmSchemaV3_1 } from "../structures/ILlmSchemaV3_1"; +import { ChatGptConverter } from "./ChatGptConverter"; +import { GeminiConverter } from "./GeminiConverter"; +import { LlmConverterV3 } from "./LlmConverterV3"; +import { LlmConverterV3_1 } from "./LlmConverterV3_1"; + +export namespace LlmSchemaConverter { + export const parameters = ( + model: Model, + ) => PARAMETERS_CASTERS[model]; + + export const schema = (model: Model) => + SCHEMA_CASTERS[model]; +} + +const PARAMETERS_CASTERS = { + "3.0": (props: { + components: OpenApi.IComponents; + schema: OpenApi.IJsonSchema.IObject; + config: ILlmSchemaV3.IConfig; + }) => + LlmConverterV3.parameters({ + components: props.components, + schema: props.schema, + config: props.config, + }), + gemini: (props: { + components: OpenApi.IComponents; + schema: OpenApi.IJsonSchema.IObject; + config: IGeminiSchema.IConfig; + }) => + GeminiConverter.parameters({ + components: props.components, + schema: props.schema, + config: props.config, + }), + "3.1": (props: { + components: OpenApi.IComponents; + schema: OpenApi.IJsonSchema.IObject; + $defs: Record; + config: ILlmSchemaV3_1.IConfig; + }) => + LlmConverterV3_1.parameters({ + components: props.components, + schema: props.schema, + config: props.config, + }), + chatgpt: (props: { + components: OpenApi.IComponents; + schema: OpenApi.IJsonSchema.IObject; + $defs: Record; + config: IChatGptSchema.IConfig; + }) => + ChatGptConverter.parameters({ + components: props.components, + schema: props.schema, + config: props.config, + }), +}; + +const SCHEMA_CASTERS = { + "3.0": (props: { + components: OpenApi.IComponents; + schema: OpenApi.IJsonSchema; + config: ILlmSchemaV3.IConfig; + }) => + LlmConverterV3.schema({ + components: props.components, + schema: props.schema, + config: props.config, + }), + gemini: (props: { + components: OpenApi.IComponents; + schema: OpenApi.IJsonSchema; + config: IGeminiSchema.IConfig; + }) => + GeminiConverter.schema({ + components: props.components, + schema: props.schema, + config: props.config, + }), + "3.1": (props: { + components: OpenApi.IComponents; + schema: OpenApi.IJsonSchema; + $defs: Record; + config: ILlmSchemaV3_1.IConfig; + }) => + LlmConverterV3_1.schema({ + config: props.config, + $defs: props.$defs, + components: props.components, + schema: props.schema, + }), + chatgpt: (props: { + components: OpenApi.IComponents; + schema: OpenApi.IJsonSchema; + $defs: Record; + config: IChatGptSchema.IConfig; + }) => + ChatGptConverter.schema({ + components: props.components, + schema: props.schema, + $defs: props.$defs, + config: props.config, + }), +}; diff --git a/src/index.ts b/src/index.ts index b8d2948..b79c7d6 100644 --- a/src/index.ts +++ b/src/index.ts @@ -26,6 +26,7 @@ export * from "./HttpMigration"; export * from "./structures/IHttpLlmApplication"; export * from "./structures/IHttpLlmFunction"; export * from "./structures/ILlmApplication"; +export * from "./structures/ILlmFunction"; export * from "./structures/IChatGptSchema"; export * from "./structures/IGeminiSchema"; diff --git a/src/structures/IChatGptSchema.ts b/src/structures/IChatGptSchema.ts index 2adb125..c4b9b2b 100644 --- a/src/structures/IChatGptSchema.ts +++ b/src/structures/IChatGptSchema.ts @@ -19,7 +19,7 @@ * - Merge {@link OpenApiV3_1.IJsonSchema.IRecursiveReference} to {@link IChatGptSchema.IReference} * - Forcibly transform every object properties to be required * - * If compare with the {@link OpenApi.IJsonSchema}, the emended JSON schema type of + * If compare with the {@link OpenApi.IJsonSchema}, the emended JSON schema specification, * * - {@link IChatGptSchema.IAnyOf} instead of the {@link OpenApi.IJsonSchema.IOneOf} * - {@link IChatGptSchema.IParameters.$defs} instead of the {@link OpenApi.IJsonSchema.IComponents.schemas} @@ -28,13 +28,13 @@ * - Forcibly transform every object properties to be required * * For reference, if you've composed the `IChatGptSchema` type with the - * {@link ILlmApplication.IChatGptOptions.reference} `false` option (default is `false`), + * {@link IChatGptSchema.IConfig.reference} `false` option (default is `false`), * only the recursived named types would be archived into the * {@link IChatGptSchema.IParameters.$defs}, and the others would be ecaped from the * {@link IChatGptSchema.IReference} type. * * Also, if you've composed the `IChatGptSchema` type with the - * {@link ILlmApplication.IChatGptOptions.constraint} `false` option (default `false`), + * {@link IChatGptSchema.IConfig.constraint} `false` option (default `false`), * the `IChatGptSchema` would not compose these properties. Instead, these * properties would be written on {@link IChatGptSchema.__IAttribute.descripotion} * field like `@format uuid` case. @@ -74,12 +74,19 @@ export type IChatGptSchema = export namespace IChatGptSchema { /** * Type of the function parameters. + * + * `IChatGptSchema.IParameters` is a type defining a function's parameters + * as a keyworded object type. + * + * It also can be utilized for the structured output metadata. + * + * @reference https://platform.openai.com/docs/guides/structured-outputs */ export interface IParameters extends IObject { /** * Collection of the named types. */ - $defs?: Record; + $defs: Record; } /** @@ -380,19 +387,19 @@ export namespace IChatGptSchema { /** * Reference type directing named schema. */ - export interface IReference extends __IAttribute { + export interface IReference extends __IAttribute { /** * Reference to the named schema. * * The `ref` is a reference to the named schema. Format of the `$ref` is * following the JSON Pointer specification. In the OpenAPI, the `$ref` * starts with `#/$defs/` which means the type is stored in - * the {@link IChatGptSchema.ITop.$defs} object. + * the {@link IChatGptSchema.IParameters.$defs} object. * * - `#/$defs/SomeObject` * - `#/$defs/AnotherObject` */ - $ref: Key; + $ref: string; } /** @@ -470,4 +477,60 @@ export namespace IChatGptSchema { */ examples?: Record; } + + /** + * Configuration for ChatGPT schema composition. + */ + export interface IConfig { + /** + * Whether to allow contraint properties or not. + * + * If you configure this property to `false`, the schemas do not containt + * the constraint properties of below. Instead, below properties would be + * written to the {@link IChatGptSchema.__IAttribute.description} property + * as a comment string like `"@format uuid"`. + * + * This is because the ChatGPT function calling understands the constraint + * properties when the function parameter types are simple, however it occurs + * some errors when the parameter types are complex. + * + * Therefore, considering the complexity of your parameter types, determine + * which is better, to allow the constraint properties or not. + * + * - {@link IChatGptSchema.INumber.minimum} + * - {@link IChatGptSchema.INumber.maximum} + * - {@link IChatGptSchema.INumber.multipleOf} + * - {@link IChatGptSchema.IString.minLength} + * - {@link IChatGptSchema.IString.maxLength} + * - {@link IChatGptSchema.IString.format} + * - {@link IChatGptSchema.IString.pattern} + * - {@link IChatGptSchema.IString.contentMediaType} + * - {@link IChatGptSchema.IArray.minItems} + * - {@link IChatGptSchema.IArray.maxItems} + * - {@link IChatGptSchema.IArray.unique} + * + * @default false + */ + constraint: boolean; + + /** + * Whether to allow reference type in everywhere. + * + * If you configure this property to `false`, most of reference types + * represented by {@link IChatGptSchema.IReference} would be escaped to + * a plain type unless recursive type case. + * + * This is because the lower version of ChatGPT does not understand the + * reference type well, and even the modern version of ChatGPT sometimes occur + * the hallucination. + * + * However, the reference type makes the schema size smaller, so that reduces + * the LLM token cost. Therefore, if you're using the modern version of ChatGPT, + * and want to reduce the LLM token cost, you can configure this property to + * `true`. + * + * @default false + */ + reference: boolean; + } } diff --git a/src/structures/IGeminiSchema.ts b/src/structures/IGeminiSchema.ts index ba05060..51aad82 100644 --- a/src/structures/IGeminiSchema.ts +++ b/src/structures/IGeminiSchema.ts @@ -1,7 +1,7 @@ /** - * Type schema info of the Gemini. + * Type schema info for the Gemini function calling. * - * `IGeminiSchema` iis a type metadata of LLM (Large Language Model) + * `IGeminiSchema` is a type metadata for the LLM (Large Language Model) * function calling in the Geminimi. * * `IGeminiSchema` basically follows the JSON schema definition of the @@ -48,6 +48,7 @@ * * @reference https://cloud.google.com/vertex-ai/generative-ai/docs/model-reference/function-calling * @reference https://cloud.google.com/vertex-ai/generative-ai/docs/multimodal/function-calling + * @reference https://ai.google.dev/gemini-api/docs/structured-output * @warning Specified not by the official documentation, but by my experiments. * Therefore, its definitions can be inaccurate or be changed in the * future. If you find any wrong or outdated definitions, please let me @@ -66,6 +67,13 @@ export type IGeminiSchema = export namespace IGeminiSchema { /** * Type of the function parameters. + * + * `IGeminiSchema.IParameters` is a type defining a function's parameters + * as a keyworded object type. + * + * It also can be utilized for the structured output metadata. + * + * @reference https://ai.google.dev/gemini-api/docs/structured-output */ export type IParameters = IObject; @@ -272,4 +280,21 @@ export namespace IGeminiSchema { */ examples?: Record; } + + /** + * Configuration for the Gemini schema composition. + */ + export interface IConfig { + /** + * Whether to allow recursive types or not. + * + * If allow, then how many times to repeat the recursive types. + * + * By the way, if the model is "chatgpt", the recursive types are always + * allowed without any limitation, due to it supports the reference type. + * + * @default 3 + */ + recursive: false | number; + } } diff --git a/src/structures/IHttpLlmApplication.ts b/src/structures/IHttpLlmApplication.ts index 344d2ad..0456a1d 100644 --- a/src/structures/IHttpLlmApplication.ts +++ b/src/structures/IHttpLlmApplication.ts @@ -1,11 +1,8 @@ import { OpenApi } from "../OpenApi"; -import { IChatGptSchema } from "./IChatGptSchema"; -import { IGeminiSchema } from "./IGeminiSchema"; import { IHttpLlmFunction } from "./IHttpLlmFunction"; import { IHttpMigrateRoute } from "./IHttpMigrateRoute"; import { ILlmApplication } from "./ILlmApplication"; import { ILlmSchemaV3 } from "./ILlmSchemaV3"; -import { ILlmSchemaV3_1 } from "./ILlmSchemaV3_1"; /** * Application of LLM function call from OpenAPI document. @@ -19,11 +16,11 @@ import { ILlmSchemaV3_1 } from "./ILlmSchemaV3_1"; * * About the {@link OpenApi.IOperation API operations}, they are converted to * {@link IHttpLlmFunction} type which represents LLM function calling schema. - * By the way, if tehre're some recursive types which can't escape the - * {@link OpenApi.IJsonSchema.IReference} type, the operation would be failed and - * pushed into the {@link IHttpLlmApplication.errors}. Otherwise not, the operation - * would be successfully converted to {@link IHttpLlmFunction} and its type schemas - * are downgraded to {@link OpenApiV3.IJsonSchema} and converted to {@link ILlmSchemaV3}. + * By the way, if there're some types which does not supported by LLM, the operation + * would be failed and pushed into the {@link IHttpLlmApplication.errors}. Otherwise not, + * the operation would be successfully converted to {@link IHttpLlmFunction} and its + * type schemas are downgraded to {@link OpenApiV3.IJsonSchema} and converted to + * {@link ILlmSchemaV3}. * * About the options, if you've configured {@link IHttpLlmApplication.options.keyword} * (as `true`), number of {@link IHttpLlmFunction.parameters} are always 1 and the first @@ -98,10 +95,7 @@ export interface IHttpLlmApplication< errors: IHttpLlmApplication.IError[]; /** - * Options for the application. - * - * Adjusted options when composing the application through - * {@link HttpLlm.application} function. + * Configuration for the application. */ options: IHttpLlmApplication.IOptions< Model, @@ -111,19 +105,12 @@ export interface IHttpLlmApplication< >; } export namespace IHttpLlmApplication { - export type Model = "3.0" | "3.1" | "chatgpt" | "gemini"; - export type ModelParameters = { - "3.0": ILlmSchemaV3.IParameters; - "3.1": ILlmSchemaV3_1.IParameters; - chatgpt: IChatGptSchema.IParameters; - gemini: IGeminiSchema.IParameters; - }; - export type ModelSchema = { - "3.0": ILlmSchemaV3; - "3.1": ILlmSchemaV3_1; - chatgpt: IChatGptSchema; - gemini: IGeminiSchema; - }; + export import Model = ILlmApplication.Model; + export import ModelParameters = ILlmApplication.ModelParameters; + export import ModelSchema = ILlmApplication.ModelSchema; + export import ModelConfig = ILlmApplication.ModelConfig; + + export import IOptions = ILlmApplication.IOptions; /** * Error occurred in the composition. @@ -166,8 +153,4 @@ export namespace IHttpLlmApplication { */ route: () => Route | undefined; } - - export import IOptions = ILlmApplication.IOptions; - export import ICommonOptions = ILlmApplication.ICommonOptions; - export import IChatGptOptions = ILlmApplication.IChatGptOptions; } diff --git a/src/structures/ILlmApplication.ts b/src/structures/ILlmApplication.ts index 5c569f6..043323a 100644 --- a/src/structures/ILlmApplication.ts +++ b/src/structures/ILlmApplication.ts @@ -12,13 +12,6 @@ import { ILlmSchemaV3_1 } from "./ILlmSchemaV3_1"; * TypeScript class (or interface) type by the `typia.llm.application()` * function. * - * By the way, the LLM function calling application composition, converting - * `ILlmApplication` instance from TypeScript interface (or class) type is not always - * successful. As LLM provider like OpenAI cannot understand the recursive reference - * type that is embodied by {@link IOpenApiSchemachema.IReference}, if there're some - * recursive types in the TypeScript interface (or class) type, the conversion would - * be failed. - * * Also, there can be some parameters (or their nested properties) which must be * composed by Human, not by LLM. File uploading feature or some sensitive information * like secrety key (password) are the examples. In that case, you can separate the @@ -52,7 +45,7 @@ export interface ILlmApplication< functions: ILlmFunction[]; /** - * Options for the application. + * Configuration for the application. */ options: ILlmApplication.IOptions< Model, @@ -75,87 +68,29 @@ export namespace ILlmApplication { chatgpt: IChatGptSchema; gemini: IGeminiSchema; }; + export type ModelConfig = { + "3.0": ILlmSchemaV3.IConfig; + "3.1": ILlmSchemaV3_1.IConfig; + chatgpt: IChatGptSchema.IConfig; + gemini: IGeminiSchema.IConfig; + }; /** - * Options for composing the LLM application. + * Options for application composition. */ export type IOptions< Model extends ILlmApplication.Model, - Schema extends - ILlmApplication.ModelSchema[Model] = ILlmApplication.ModelSchema[Model], - > = Model extends "chatgpt" - ? IChatGptOptions - : ICommonOptions< - Exclude, - Schema extends ILlmApplication.ModelSchema[Exclude] - ? Schema - : ILlmApplication.ModelSchema[Exclude] - >; - - /** - * Options for non "chatgpt" model. - */ - export interface ICommonOptions< - Model extends Exclude, - Schema extends - ILlmApplication.ModelSchema[Model] = ILlmApplication.ModelSchema[Model], - > { - /** - * Whether to allow recursive types or not. - * - * If allow, then how many times to repeat the recursive types. - * - * By the way, if the model is "chatgpt", the recursive types are always - * allowed without any limitation, due to it supports the reference type. - * - * @default 3 - */ - recursive: false | number; - + Schema extends ModelSchema[Model] = ModelSchema[Model], + > = { /** * Separator function for the parameters. * * When composing parameter arguments through LLM function call, * there can be a case that some parameters must be composed by human, - * or LLM cannot understand the parameter. For example, if the - * parameter type has configured - * {@link ILlmSchemaV3.IString.contentMediaType} which indicates file - * uploading, it must be composed by human, not by LLM - * (Large Language Model). - * - * In that case, if you configure this property with a function that - * predicating whether the schema value must be composed by human or - * not, the parameters would be separated into two parts. - * - * - {@link ILlmFunction.separated.llm} - * - {@link ILlmFunction.separated.human} - * - * When writing the function, note that returning value `true` means - * to be a human composing the value, and `false` means to LLM - * composing the value. Also, when predicating the schema, it would - * better to utilize the {@link LlmTypeChecker} features. - * - * @param schema Schema to be separated. - * @returns Whether the schema value must be composed by human or not. - * @default null - */ - separate: null | ((schema: Schema) => boolean); - } - - /** - * Options for "chatgpt" model. - */ - export interface IChatGptOptions< - Schema extends IChatGptSchema = IChatGptSchema, - > { - /** - * Separator function for the parameters. + * or LLM cannot understand the parameter. * - * When composing parameter arguments through LLM function call, - * there can be a case that some parameters must be composed by human, - * or LLM cannot understand the parameter. For example, if the - * parameter type has configured - * {@link IChatGptSchema.IString.contentMediaType} which indicates file + * For example, if the parameter type has configured + * {@link IGeminiSchema.IString.contentMediaType} which indicates file * uploading, it must be composed by human, not by LLM * (Large Language Model). * @@ -169,63 +104,12 @@ export namespace ILlmApplication { * When writing the function, note that returning value `true` means * to be a human composing the value, and `false` means to LLM * composing the value. Also, when predicating the schema, it would - * better to utilize the {@link ChatGptTypeChecker} features. + * better to utilize the {@link GeminiTypeChecker} like features. * * @param schema Schema to be separated. * @returns Whether the schema value must be composed by human or not. * @default null */ separate: null | ((schema: Schema) => boolean); - - /** - * Whether to allow reference type in everywhere. - * - * If you configure this property to `false`, most of reference types - * represented by {@link IChatGptSchema.IReference} would be escaped to - * a plain type unless recursive type case. - * - * This is because the lower version of ChatGPT does not understand the - * reference type well, and even the modern version of ChatGPT sometimes occur - * the hallucination. - * - * However, the reference type makes the schema size smaller, so that reduces - * the LLM token cost. Therefore, if you're using the modern version of ChatGPT, - * and want to reduce the LLM token cost, you can configure this property to - * `true`. - * - * @default false - */ - reference: boolean; - - /** - * Whether to allow contraint properties or not. - * - * If you configure this property to `false`, the schemas do not containt - * the constraint properties of below. Instead, below properties would be - * written to the {@link IChatGptSchema.__IAttribute.description} property - * as a comment string like `"@format uuid"`. - * - * This is because the ChatGPT function calling understands the constraint - * properties when the function parameter types are simple, however it occurs - * some errors when the parameter types are complex. - * - * Therefore, considering the complexity of your parameter types, determine - * which is better, to allow the constraint properties or not. - * - * - {@link IChatGptSchema.INumber.minimum} - * - {@link IChatGptSchema.INumber.maximum} - * - {@link IChatGptSchema.INumber.multipleOf} - * - {@link IChatGptSchema.IString.minLength} - * - {@link IChatGptSchema.IString.maxLength} - * - {@link IChatGptSchema.IString.format} - * - {@link IChatGptSchema.IString.pattern} - * - {@link IChatGptSchema.IString.contentMediaType} - * - {@link IChatGptSchema.IArray.minItems} - * - {@link IChatGptSchema.IArray.maxItems} - * - {@link IChatGptSchema.IArray.unique} - * - * @default false - */ - constraint: boolean; - } + } & ModelConfig[Model]; } diff --git a/src/structures/ILlmSchema.ts b/src/structures/ILlmSchema.ts new file mode 100644 index 0000000..4c068da --- /dev/null +++ b/src/structures/ILlmSchema.ts @@ -0,0 +1,34 @@ +import { IChatGptSchema } from "./IChatGptSchema"; +import { IGeminiSchema } from "./IGeminiSchema"; +import { ILlmSchemaV3 } from "./ILlmSchemaV3"; +import { ILlmSchemaV3_1 } from "./ILlmSchemaV3_1"; + +/** + * The schemas for the LLM function calling. + * + * `ILlmSchema` is an union type collecting all the schemas for the + * LLM function calling. + * + * Select a proper schema type according to the LLM provider you're using. + * + * @author Jeongho Nam - https://github.com/samchon + */ +export type ILlmSchema = + | IChatGptSchema + | IGeminiSchema + | ILlmSchemaV3 + | ILlmSchemaV3_1; + +export namespace ILlmSchema { + export type IParameters = + | IChatGptSchema.IParameters + | IGeminiSchema.IParameters + | ILlmSchemaV3.IParameters + | ILlmSchemaV3_1.IParameters; + + export type IConfig = + | IChatGptSchema.IConfig + | IGeminiSchema.IConfig + | ILlmSchemaV3.IConfig + | ILlmSchemaV3_1.IConfig; +} diff --git a/src/structures/ILlmSchemaV3.ts b/src/structures/ILlmSchemaV3.ts index bb70b9a..88977fb 100644 --- a/src/structures/ILlmSchemaV3.ts +++ b/src/structures/ILlmSchemaV3.ts @@ -1,19 +1,41 @@ /** - * Type schema info of LLM function call. + * Type schema based on OpenAPI v3.0 for LLM function calling. * - * `ILlmSchemaV3` is a type metadata of LLM (Large Language Model) - * function calling. + * `ILlmSchemaV3` is a type metadata for LLM (Large Language Model) + * function calling, based on the OpenAPI v3.0 speicification. This type + * is not the final type for the LLM function calling, but the intermediate + * structure for the conversion to the final type like {@link IGeminiSchema}. * * `ILlmSchemaV3` basically follows the JSON schema definition of OpenAPI * v3.0 specification; {@link OpenApiV3.IJsonSchema}. However, `ILlmSchemaV3` * does not have the reference type; {@link OpenApiV3.IJsonSchema.IReference}. - * It's because the LLM cannot compose the reference typed arguments. + * It's because the LLM cannot compose the reference typed arguments. If + * recursive type comes, its type would be repeated in + * {@link ILlmSchemaV3.IConfig.recursive} times. Otherwise you've configured + * it to `false`, the recursive types are not allowed. * * For reference, the OpenAPI v3.0 based JSON schema definition can't * express the tuple array type. It has been supported since OpenAPI v3.1; * {@link OpenApi.IJsonSchema.ITuple}. Therefore, it would better to avoid * using the tuple array type in the LLM function calling. * + * Also, if you configure {@link ILlmSchemaV3.IConfig.constraint} to `false`, + * tehse properties would be banned and written to the + * {@link ILlmSchemaV3.__IAttribute.description} property instead. It's because + * there are some LLM models which does not support the constraint properties. + * + * - {@link ILlmSchemaV3.INumber.minimum} + * - {@link ILlmSchemaV3.INumber.maximum} + * - {@link ILlmSchemaV3.INumber.multipleOf} + * - {@link ILlmSchemaV3.IString.minLength} + * - {@link ILlmSchemaV3.IString.maxLength} + * - {@link ILlmSchemaV3.IString.format} + * - {@link ILlmSchemaV3.IString.pattern} + * - {@link ILlmSchemaV3.IString.contentMediaType} + * - {@link ILlmSchemaV3.IArray.minItems} + * - {@link ILlmSchemaV3.IArray.maxItems} + * - {@link ILlmSchemaV3.IArray.unique} + * * @reference https://platform.openai.com/docs/guides/function-calling * @author Jeongho Nam - https://github.com/samchon */ @@ -30,13 +52,15 @@ export type ILlmSchemaV3 = export namespace ILlmSchemaV3 { /** * Type of the function parameters. + * + * `ILlmSchemaV3.IParameters` is a type defining a function's parameters + * as a keyworded object type. + * + * It also can be utilized for the structured output metadata. + * + * @reference https://platform.openai.com/docs/guides/structured-outputs */ - export interface IParameters extends Omit { - /** - * Do not allow additional properties in the parameters. - */ - additionalProperties: false; - } + export type IParameters = IObject; /** * Boolean type schema info. @@ -425,4 +449,52 @@ export namespace ILlmSchemaV3 { */ examples?: Record; } + + /** + * Configuration for OpenAPI v3.0 based LLM schema composition. + */ + export interface IConfig { + /** + * Whether to allow contraint properties or not. + * + * If you configure this property to `false`, the schemas do not containt + * the constraint properties of below. Instead, below properties would be + * written to the {@link ILlmSchemaV3.__IAttribute.description} property + * as a comment string like `"@format uuid"`. + * + * This is because the some LLM model's function calling understands the constraint + * properties when the function parameter types are simple, however it occurs + * some errors when the parameter types are complex. + * + * Therefore, considering the complexity of your parameter types, determine + * which is better, to allow the constraint properties or not. + * + * - {@link ILlmSchemaV3.INumber.minimum} + * - {@link ILlmSchemaV3.INumber.maximum} + * - {@link ILlmSchemaV3.INumber.multipleOf} + * - {@link ILlmSchemaV3.IString.minLength} + * - {@link ILlmSchemaV3.IString.maxLength} + * - {@link ILlmSchemaV3.IString.format} + * - {@link ILlmSchemaV3.IString.pattern} + * - {@link ILlmSchemaV3.IString.contentMediaType} + * - {@link ILlmSchemaV3.IArray.minItems} + * - {@link ILlmSchemaV3.IArray.maxItems} + * - {@link ILlmSchemaV3.IArray.unique} + * + * @default false + */ + constraint: boolean; + + /** + * Whether to allow recursive types or not. + * + * If allow, then how many times to repeat the recursive types. + * + * By the way, if the model is "chatgpt", the recursive types are always + * allowed without any limitation, due to it supports the reference type. + * + * @default 3 + */ + recursive: false | number; + } } diff --git a/src/structures/ILlmSchemaV3_1.ts b/src/structures/ILlmSchemaV3_1.ts index b65964b..c9de2bb 100644 --- a/src/structures/ILlmSchemaV3_1.ts +++ b/src/structures/ILlmSchemaV3_1.ts @@ -1,23 +1,84 @@ +/** + * Type schema based on OpenAPI v3.1 for LLM function calling. + * + * `ILlmSchemaV3_1` is a type metadata for LLM (Large Language Model) + * function calling, based on the OpenAPI v3.1 speicification. This type + * is not the final type for the LLM function calling, but the intermediate + * structure for the conversion to the final type like {@link IChatGptSchema}. + * + * However, the `IChatGptSchema` does not follow the entire specification of + * the OpenAPI v3.1. It has own specific restrictions and definitions. Here is the + * list of how `ILlmSchemaV3_1` is different with the OpenAPI v3.1 JSON schema. + * + * - Decompose mixed type: {@link OpenApiV3_1.IJsonSchema.IMixed} + * - Resolve nullable property: {@link OpenApiV3_1.IJsonSchema.__ISignificant.nullable} + * - Tuple type is banned: {@link OpenApiV3_1.IJsonSchema.ITuple.prefixItems} + * - Constant type is banned: {@link OpenApiV3_1.IJsonSchema.IConstant} + * - Merge {@link OpenApiV3_1.IJsonSchema.IAnyOf} to {@link ILlmSchemaV3_1.IOneOf} + * - Merge {@link OpenApiV3_1.IJsonSchema.IAllOf} to {@link ILlmSchemaV3_1.IObject} + * - Merge {@link OpenApiV3_1.IJsonSchema.IRecursiveReference} to {@link ILlmSchemaV3_1.IReference} + * - Do not support {@link OpenApiV3_1.IJsonSchema.ITuple} type + * + * If compare with the {@link OpenApi.IJsonSchema}, the emended JSON schema specification, + * + * - {@link ILlmSchemaV3_1.IParameters.$defs} instead of the {@link OpenApi.IJsonSchema.schemas} + * - Do not support {@link OpenApi.IJsonSchema.ITuple} type + * + * For reference, if you've composed the `ILlmSchemaV3_1` type with the + * {@link ILlmSchemaV3_1.IConfig.reference} `false` option (default is `false`), + * only the recursived named types would be archived into the + * {@link ILlmSchemaV3_1.IParameters.$defs}, and the others would be ecaped from the + * {@link ILlmSchemaV3_1.IReference} type. + * + * Also, if you've composed the `ILlmSchemaV3_1` type with the + * {@link ILlmSchemaV3_1.IConfig.constraint} `false` option (default `false`), + * the `ILlmSchemaV3_1` would not compose these properties. Instead, these + * properties would be written on {@link ILlmSchemaV3_1.__IAttribute.descripotion} + * field like `@format uuid` case. + * + * - {@link ILlmSchemaV3_1.INumber.minimum} + * - {@link ILlmSchemaV3_1.INumber.maximum} + * - {@link ILlmSchemaV3_1.INumber.multipleOf} + * - {@link ILlmSchemaV3_1.IString.minLength} + * - {@link ILlmSchemaV3_1.IString.maxLength} + * - {@link ILlmSchemaV3_1.IString.format} + * - {@link ILlmSchemaV3_1.IString.pattern} + * - {@link ILlmSchemaV3_1.IString.contentMediaType} + * - {@link ILlmSchemaV3_1.IArray.minItems} + * - {@link ILlmSchemaV3_1.IArray.maxItems} + * - {@link ILlmSchemaV3_1.IArray.unique} + * + * @reference https://platform.openai.com/docs/guides/function-calling + * @reference https://platform.openai.com/docs/guides/structured-outputs + * @author Jeongho Nam - https://github.com/samchon + */ export type ILlmSchemaV3_1 = | ILlmSchemaV3_1.IBoolean | ILlmSchemaV3_1.IInteger | ILlmSchemaV3_1.INumber | ILlmSchemaV3_1.IString | ILlmSchemaV3_1.IArray - | ILlmSchemaV3_1.ITuple | ILlmSchemaV3_1.IObject | ILlmSchemaV3_1.IOneOf + | ILlmSchemaV3_1.IReference | ILlmSchemaV3_1.INull | ILlmSchemaV3_1.IUnknown; export namespace ILlmSchemaV3_1 { /** * Type of the function parameters. + * + * `ILlmSchemaV3_1.IParameters` is a type defining a function's parameters + * as a keyworded object type. + * + * It also can be utilized for the structured output metadata. + * + * @reference https://platform.openai.com/docs/guides/structured-outputs */ - export interface IParameters extends Omit { + export interface IParameters extends IObject { /** - * Do not allow additional properties in the parameters. + * Collection of the named types. */ - additionalProperties: false; + $defs: Record; } /** @@ -242,64 +303,6 @@ export namespace ILlmSchemaV3_1 { maxItems?: number; } - /** - * Tuple type info. - */ - export interface ITuple extends __ISignificant<"array"> { - /** - * Prefix items. - * - * The `prefixItems` means the type schema info of the prefix items in the - * tuple type. In the TypeScript, it is expressed as `[T1, T2]`. - * - * If you want to express `[T1, T2, ...TO[]]` type, you can configure the - * `...TO[]` through the {@link additionalItems} property. - */ - prefixItems: ILlmSchemaV3_1[]; - - /** - * Additional items. - * - * The `additionalItems` means the type schema info of the additional items - * after the {@link prefixItems}. In the TypeScript, if there's a type - * `[T1, T2, ...TO[]]`, the `...TO[]` is represented by the `additionalItems`. - * - * By the way, if you configure the `additionalItems` as `true`, it means - * the additional items are not restricted. They can be any type, so that - * it is equivalent to the TypeScript type `[T1, T2, ...any[]]`. - * - * Otherwise configure the `additionalItems` as the {@link IJsonSchema}, - * it means the additional items must follow the type schema info. - * Therefore, it is equivalent to the TypeScript type `[T1, T2, ...TO[]]`. - */ - additionalItems?: boolean | ILlmSchemaV3_1; - - /** - * Unique items restriction. - * - * If this property value is `true`, target tuple must have unique items. - */ - uniqueItems?: boolean; - - /** - * Minimum items restriction. - * - * Restriction of minumum number of items in the tuple. - * - * @type uint64 - */ - minItems?: number; - - /** - * Maximum items restriction. - * - * Restriction of maximum number of items in the tuple. - * - * @type uint64 - */ - maxItems?: number; - } - /** * Object type info. */ @@ -322,15 +325,10 @@ export namespace ILlmSchemaV3_1 { * The `additionalProperties` means the type schema info of the additional * properties that are not listed in the {@link properties}. * - * If the value is `true`, it means that the additional properties are not - * restricted. They can be any type. Otherwise, if the value is - * {@link IOpenAiSchema} type, it means that the additional properties must - * follow the type schema info. - * - * - `true`: `Record` - * - `IOpenAiSchema`: `Record` + * By the way, as LLM function calling does not support such dynamic key + * typed properties, the `additionalProperties` becomes always `false`. */ - additionalProperties?: boolean | ILlmSchemaV3_1; + additionalProperties: false; /** * List of key values of the required properties. @@ -368,6 +366,24 @@ export namespace ILlmSchemaV3_1 { required: string[]; } + /** + * Reference type directing named schema. + */ + export interface IReference extends __IAttribute { + /** + * Reference to the named schema. + * + * The `ref` is a reference to the named schema. Format of the `$ref` is + * following the JSON Pointer specification. In the OpenAPI, the `$ref` + * starts with `#/$defs/` which means the type is stored in + * the {@link ILlmSchemaV3_1.IParameters.$defs} object. + * + * - `#/$defs/SomeObject` + * - `#/$defs/AnotherObject` + */ + $ref: string; + } + /** * Union type. * @@ -469,4 +485,60 @@ export namespace ILlmSchemaV3_1 { */ examples?: Record; } + + /** + * Configuration for OpenAPI v3.1 based LLM schema composition. + */ + export interface IConfig { + /** + * Whether to allow contraint properties or not. + * + * If you configure this property to `false`, the schemas do not containt + * the constraint properties of below. Instead, below properties would be + * written to the {@link ILlmSchemaV3_1.__IAttribute.description} property + * as a comment string like `"@format uuid"`. + * + * This is because the some LLM model's function calling understands the constraint + * properties when the function parameter types are simple, however it occurs + * some errors when the parameter types are complex. + * + * Therefore, considering the complexity of your parameter types, determine + * which is better, to allow the constraint properties or not. + * + * - {@link ILlmSchemaV3_1.INumber.minimum} + * - {@link ILlmSchemaV3_1.INumber.maximum} + * - {@link ILlmSchemaV3_1.INumber.multipleOf} + * - {@link ILlmSchemaV3_1.IString.minLength} + * - {@link ILlmSchemaV3_1.IString.maxLength} + * - {@link ILlmSchemaV3_1.IString.format} + * - {@link ILlmSchemaV3_1.IString.pattern} + * - {@link ILlmSchemaV3_1.IString.contentMediaType} + * - {@link ILlmSchemaV3_1.IArray.minItems} + * - {@link ILlmSchemaV3_1.IArray.maxItems} + * - {@link ILlmSchemaV3_1.IArray.unique} + * + * @default false + */ + constraint: boolean; + + /** + * Whether to allow reference type in everywhere. + * + * If you configure this property to `false`, most of reference types + * represented by {@link ILlmSchemaV3_1.IReference} would be escaped to + * a plain type unless recursive type case. + * + * This is because some low sized LLM models does not understand the + * reference type well, and even the large size LLM models sometimes occur + * the hallucination. + * + * However, the reference type makes the schema size smaller, so that reduces + * the LLM token cost. Therefore, if you're using the large size of LLM model, + * and want to reduce the LLM token cost, you can configure this property to + * `true`. + * + * @default false + */ + reference: boolean; + } } diff --git a/src/utils/ChatGptTypeChecker.ts b/src/utils/ChatGptTypeChecker.ts index 6692f4f..1fdee55 100644 --- a/src/utils/ChatGptTypeChecker.ts +++ b/src/utils/ChatGptTypeChecker.ts @@ -9,6 +9,7 @@ export namespace ChatGptTypeChecker { schema: IChatGptSchema, ): schema is IChatGptSchema.INull => (schema as IChatGptSchema.INull).type === "null"; + export const isUnknown = ( schema: IChatGptSchema, ): schema is IChatGptSchema.IUnknown => @@ -20,14 +21,17 @@ export namespace ChatGptTypeChecker { schema: IChatGptSchema, ): schema is IChatGptSchema.IBoolean => (schema as IChatGptSchema.IBoolean).type === "boolean"; + export const isInteger = ( schema: IChatGptSchema, ): schema is IChatGptSchema.IInteger => (schema as IChatGptSchema.IInteger).type === "integer"; + export const isNumber = ( schema: IChatGptSchema, ): schema is IChatGptSchema.INumber => (schema as IChatGptSchema.INumber).type === "number"; + export const isString = ( schema: IChatGptSchema, ): schema is IChatGptSchema.IString => @@ -38,13 +42,16 @@ export namespace ChatGptTypeChecker { ): schema is IChatGptSchema.IArray => (schema as IChatGptSchema.IArray).type === "array" && (schema as IChatGptSchema.IArray).items !== undefined; + export const isObject = ( schema: IChatGptSchema, ): schema is IChatGptSchema.IObject => (schema as IChatGptSchema.IObject).type === "object"; + export const isReference = ( schema: IChatGptSchema, ): schema is IChatGptSchema.IReference => (schema as any).$ref !== undefined; + export const isAnyOf = ( schema: IChatGptSchema, ): schema is IChatGptSchema.IAnyOf => diff --git a/src/utils/LlmTypeCheckerV3_1.ts b/src/utils/LlmTypeCheckerV3_1.ts index 7f15e7f..332e198 100644 --- a/src/utils/LlmTypeCheckerV3_1.ts +++ b/src/utils/LlmTypeCheckerV3_1.ts @@ -1,4 +1,6 @@ +import { OpenApi } from "../OpenApi"; import { ILlmSchemaV3_1 } from "../structures/ILlmSchemaV3_1"; +import { OpenApiTypeCheckerBase } from "./internal/OpenApiTypeCheckerBase"; export namespace LlmTypeCheckerV3_1 { /* ----------------------------------------------------------- @@ -6,60 +8,110 @@ export namespace LlmTypeCheckerV3_1 { ----------------------------------------------------------- */ export const isNull = ( schema: ILlmSchemaV3_1, - ): schema is ILlmSchemaV3_1.INull => - (schema as ILlmSchemaV3_1.INull).type === "null"; + ): schema is ILlmSchemaV3_1.INull => OpenApiTypeCheckerBase.isNull(schema); export const isUnknown = ( schema: ILlmSchemaV3_1, ): schema is ILlmSchemaV3_1.IUnknown => - (schema as ILlmSchemaV3_1.IUnknown).type === undefined && - !isConstant(schema) && - !isOneOf(schema); + OpenApiTypeCheckerBase.isUnknown(schema); export const isConstant = ( schema: ILlmSchemaV3_1, ): schema is ILlmSchemaV3_1.IConstant => - (schema as ILlmSchemaV3_1.IConstant).const !== undefined; + OpenApiTypeCheckerBase.isConstant(schema); export const isBoolean = ( schema: ILlmSchemaV3_1, ): schema is ILlmSchemaV3_1.IBoolean => - (schema as ILlmSchemaV3_1.IBoolean).type === "boolean"; + OpenApiTypeCheckerBase.isBoolean(schema); export const isInteger = ( schema: ILlmSchemaV3_1, ): schema is ILlmSchemaV3_1.IInteger => - (schema as ILlmSchemaV3_1.IInteger).type === "integer"; + OpenApiTypeCheckerBase.isInteger(schema); export const isNumber = ( schema: ILlmSchemaV3_1, ): schema is ILlmSchemaV3_1.INumber => - (schema as ILlmSchemaV3_1.INumber).type === "number"; + OpenApiTypeCheckerBase.isNumber(schema); export const isString = ( schema: ILlmSchemaV3_1, ): schema is ILlmSchemaV3_1.IString => - (schema as ILlmSchemaV3_1.IString).type === "string"; + OpenApiTypeCheckerBase.isString(schema); export const isArray = ( schema: ILlmSchemaV3_1, - ): schema is ILlmSchemaV3_1.IArray => - (schema as ILlmSchemaV3_1.IArray).type === "array" && - (schema as ILlmSchemaV3_1.IArray).items !== undefined; - - export const isTuple = ( - schema: ILlmSchemaV3_1, - ): schema is ILlmSchemaV3_1.ITuple => - (schema as ILlmSchemaV3_1.ITuple).type === "array" && - (schema as ILlmSchemaV3_1.ITuple).prefixItems !== undefined; + ): schema is ILlmSchemaV3_1.IArray => OpenApiTypeCheckerBase.isArray(schema); export const isObject = ( schema: ILlmSchemaV3_1, ): schema is ILlmSchemaV3_1.IObject => - (schema as ILlmSchemaV3_1.IObject).type === "object"; + OpenApiTypeCheckerBase.isObject(schema); + + export const isReference = ( + schema: ILlmSchemaV3_1, + ): schema is ILlmSchemaV3_1.IReference => + OpenApiTypeCheckerBase.isReference(schema); export const isOneOf = ( schema: ILlmSchemaV3_1, - ): schema is ILlmSchemaV3_1.IOneOf => - (schema as ILlmSchemaV3_1.IOneOf).oneOf !== undefined; + ): schema is ILlmSchemaV3_1.IOneOf => OpenApiTypeCheckerBase.isOneOf(schema); + + export const isRecursiveReference = (props: { + $defs?: Record; + schema: ILlmSchemaV3_1; + }): boolean => + OpenApiTypeCheckerBase.isRecursiveReference({ + prefix: "#/$defs/", + components: { + schemas: props.$defs, + }, + schema: props.schema, + }); + + /* ----------------------------------------------------------- + OPERATORS + ----------------------------------------------------------- */ + export const escape = (props: { + $defs?: Record; + schema: ILlmSchemaV3_1; + recursive: false | number; + }): ILlmSchemaV3_1 | null => + OpenApiTypeCheckerBase.escape({ + prefix: "#/$defs/", + components: { + schemas: props.$defs, + }, + schema: props.schema, + recursive: props.recursive, + }) as ILlmSchemaV3_1 | null; + + export const visit = (props: { + closure: (schema: ILlmSchemaV3_1) => void; + $defs?: Record; + schema: ILlmSchemaV3_1; + }): void => + OpenApiTypeCheckerBase.visit({ + prefix: "#/$defs/", + closure: props.closure as (schema: OpenApi.IJsonSchema) => void, + components: { + schemas: props.$defs, + }, + schema: props.schema, + }); + + export const covers = (props: { + $defs?: Record; + x: ILlmSchemaV3_1; + y: ILlmSchemaV3_1; + }): boolean => + OpenApiTypeCheckerBase.covers({ + prefix: "#/$defs/", + components: { + schemas: props.$defs, + }, + x: props.x, + y: props.y, + }); } diff --git a/src/utils/OpenApiTypeChecker.ts b/src/utils/OpenApiTypeChecker.ts index 945aba4..0334788 100644 --- a/src/utils/OpenApiTypeChecker.ts +++ b/src/utils/OpenApiTypeChecker.ts @@ -1,5 +1,5 @@ import { OpenApi } from "../OpenApi"; -import { MapUtil } from "./MapUtil"; +import { OpenApiTypeCheckerBase } from "./internal/OpenApiTypeCheckerBase"; export namespace OpenApiTypeChecker { /* ----------------------------------------------------------- @@ -8,87 +8,72 @@ export namespace OpenApiTypeChecker { export const isNull = ( schema: OpenApi.IJsonSchema, ): schema is OpenApi.IJsonSchema.INull => - (schema as OpenApi.IJsonSchema.INull).type === "null"; + OpenApiTypeCheckerBase.isNull(schema); export const isUnknown = ( schema: OpenApi.IJsonSchema, ): schema is OpenApi.IJsonSchema.IUnknown => - (schema as OpenApi.IJsonSchema.IUnknown).type === undefined && - !isConstant(schema) && - !isOneOf(schema) && - !isReference(schema); + OpenApiTypeCheckerBase.isUnknown(schema); export const isConstant = ( schema: OpenApi.IJsonSchema, ): schema is OpenApi.IJsonSchema.IConstant => - (schema as OpenApi.IJsonSchema.IConstant).const !== undefined; + OpenApiTypeCheckerBase.isConstant(schema); export const isBoolean = ( schema: OpenApi.IJsonSchema, ): schema is OpenApi.IJsonSchema.IBoolean => - (schema as OpenApi.IJsonSchema.IBoolean).type === "boolean"; + OpenApiTypeCheckerBase.isBoolean(schema); export const isInteger = ( schema: OpenApi.IJsonSchema, ): schema is OpenApi.IJsonSchema.IInteger => - (schema as OpenApi.IJsonSchema.IInteger).type === "integer"; + OpenApiTypeCheckerBase.isInteger(schema); export const isNumber = ( schema: OpenApi.IJsonSchema, ): schema is OpenApi.IJsonSchema.INumber => - (schema as OpenApi.IJsonSchema.INumber).type === "number"; + OpenApiTypeCheckerBase.isNumber(schema); export const isString = ( schema: OpenApi.IJsonSchema, ): schema is OpenApi.IJsonSchema.IString => - (schema as OpenApi.IJsonSchema.IString).type === "string"; + OpenApiTypeCheckerBase.isString(schema); export const isArray = ( schema: OpenApi.IJsonSchema, ): schema is OpenApi.IJsonSchema.IArray => - (schema as OpenApi.IJsonSchema.IArray).type === "array" && - (schema as OpenApi.IJsonSchema.IArray).items !== undefined; + OpenApiTypeCheckerBase.isArray(schema); export const isTuple = ( schema: OpenApi.IJsonSchema, ): schema is OpenApi.IJsonSchema.ITuple => - (schema as OpenApi.IJsonSchema.ITuple).type === "array" && - (schema as OpenApi.IJsonSchema.ITuple).prefixItems !== undefined; + OpenApiTypeCheckerBase.isTuple(schema); export const isObject = ( schema: OpenApi.IJsonSchema, ): schema is OpenApi.IJsonSchema.IObject => - (schema as OpenApi.IJsonSchema.IObject).type === "object"; + OpenApiTypeCheckerBase.isObject(schema); export const isReference = ( schema: OpenApi.IJsonSchema, ): schema is OpenApi.IJsonSchema.IReference => - (schema as any).$ref !== undefined; + OpenApiTypeCheckerBase.isReference(schema); export const isOneOf = ( schema: OpenApi.IJsonSchema, ): schema is OpenApi.IJsonSchema.IOneOf => - (schema as OpenApi.IJsonSchema.IOneOf).oneOf !== undefined; + OpenApiTypeCheckerBase.isOneOf(schema); export const isRecursiveReference = (props: { components: OpenApi.IComponents; schema: OpenApi.IJsonSchema; - }): boolean => { - if (isReference(props.schema) === false) return false; - const current: string = props.schema.$ref.split("#/components/schemas/")[1]; - let counter: number = 0; - visit({ + }): boolean => + OpenApiTypeCheckerBase.isRecursiveReference({ + prefix: "#/components/schemas/", components: props.components, schema: props.schema, - closure: (schema) => { - if (OpenApiTypeChecker.isReference(schema)) { - const next: string = schema.$ref.split("#/components/schemas/")[1]; - if (current === next) ++counter; - } - }, }); - return counter > 1; - }; /* ----------------------------------------------------------- OPERATORS @@ -98,579 +83,34 @@ export namespace OpenApiTypeChecker { schema: OpenApi.IJsonSchema; recursive: false | number; }): OpenApi.IJsonSchema | null => - escapeSchema({ + OpenApiTypeCheckerBase.escape({ + prefix: "#/components/schemas/", components: props.components, schema: props.schema, recursive: props.recursive, - visited: new Map(), - }) || null; + }); export const visit = (props: { closure: (schema: OpenApi.IJsonSchema) => void; components: OpenApi.IComponents; schema: OpenApi.IJsonSchema; - }): void => { - const already: Set = new Set(); - const next = (schema: OpenApi.IJsonSchema): void => { - props.closure(schema); - if (OpenApiTypeChecker.isReference(schema)) { - const key: string = schema.$ref.split("#/components/schemas/").pop()!; - if (already.has(key) === true) return; - already.add(key); - const found: OpenApi.IJsonSchema | undefined = - props.components.schemas?.[key]; - if (found !== undefined) next(found); - } else if (OpenApiTypeChecker.isOneOf(schema)) schema.oneOf.forEach(next); - else if (OpenApiTypeChecker.isObject(schema)) { - for (const value of Object.values(schema.properties ?? {})) next(value); - if ( - typeof schema.additionalProperties === "object" && - schema.additionalProperties !== null - ) - next(schema.additionalProperties); - } else if (OpenApiTypeChecker.isArray(schema)) next(schema.items); - else if (OpenApiTypeChecker.isTuple(schema)) { - (schema.prefixItems ?? []).forEach(next); - if ( - typeof schema.additionalItems === "object" && - schema.additionalItems !== null - ) - next(schema.additionalItems); - } - }; - next(props.schema); - }; + }): void => + OpenApiTypeCheckerBase.visit({ + prefix: "#/components/schemas/", + closure: props.closure, + components: props.components, + schema: props.schema, + }); export const covers = (props: { components: OpenApi.IComponents; x: OpenApi.IJsonSchema; y: OpenApi.IJsonSchema; }): boolean => - coverStation({ + OpenApiTypeCheckerBase.covers({ + prefix: "#/components/schemas/", components: props.components, x: props.x, y: props.y, - visited: new Map(), }); - - const escapeSchema = (props: { - components: OpenApi.IComponents; - schema: OpenApi.IJsonSchema; - recursive: false | number; - visited: Map; - }): OpenApi.IJsonSchema | null | undefined => { - if (isReference(props.schema)) { - // REFERENCE - const name: string = props.schema.$ref.split("#/components/schemas/")[1]; - const target: OpenApi.IJsonSchema | undefined = - props.components.schemas?.[name]; - if (target === undefined) return null; - else if (props.visited.has(name) === true) { - if (props.recursive === false) return null; - const depth: number = props.visited.get(name)!; - if (depth > props.recursive) return undefined; - props.visited.set(name, depth + 1); - const res: OpenApi.IJsonSchema | null | undefined = escapeSchema({ - components: props.components, - schema: target, - recursive: props.recursive, - visited: props.visited, - }); - return res - ? { - ...res, - description: writeReferenceDescription({ - components: props.components, - $ref: props.schema.$ref, - description: res.description, - escape: true, - }), - } - : res; - } - const res: OpenApi.IJsonSchema | null | undefined = escapeSchema({ - components: props.components, - schema: target, - recursive: props.recursive, - visited: new Map([...props.visited, [name, 1]]), - }); - return res - ? { - ...res, - description: writeReferenceDescription({ - components: props.components, - $ref: props.schema.$ref, - description: res.description, - escape: true, - }), - } - : res; - } else if (isOneOf(props.schema)) { - // UNION - const elements: Array = - props.schema.oneOf.map((schema) => - escapeSchema({ - components: props.components, - schema: schema, - recursive: props.recursive, - visited: props.visited, - }), - ); - if (elements.some((v) => v === null)) return null; - const filtered: OpenApi.IJsonSchema[] = elements.filter( - (v) => v !== undefined, - ) as OpenApi.IJsonSchema[]; - if (filtered.length === 0) return undefined; - return { - ...props, - oneOf: filtered.map((v) => flatSchema(props.components, v)).flat(), - }; - } else if (isObject(props.schema)) { - // OBJECT - const object: OpenApi.IJsonSchema.IObject = props.schema; - const properties: Array< - [string, OpenApi.IJsonSchema | null | undefined] - > = Object.entries(object.properties ?? {}).map(([key, value]) => [ - key, - escapeSchema({ - components: props.components, - schema: value, - recursive: props.recursive, - visited: props.visited, - }), - ]); - const additionalProperties: - | OpenApi.IJsonSchema - | null - | boolean - | undefined = object.additionalProperties - ? typeof object.additionalProperties === "object" && - object.additionalProperties !== null - ? escapeSchema({ - components: props.components, - schema: object.additionalProperties, - recursive: props.recursive, - visited: props.visited, - }) - : object.additionalProperties - : false; - if ( - properties.some(([_k, v]) => v === null) || - additionalProperties === null - ) - return null; - else if ( - properties.some( - ([k, v]) => v === undefined && object.required.includes(k) === true, - ) === true - ) - return undefined; - return { - ...object, - properties: Object.fromEntries( - properties.filter(([_k, v]) => v !== undefined) as Array< - [string, OpenApi.IJsonSchema] - >, - ), - additionalProperties: additionalProperties ?? false, - required: object.required.filter((k) => - properties.some(([key, value]) => key === k && value !== undefined), - ), - }; - } else if (isTuple(props.schema)) { - // TUPLE - const elements: Array = - props.schema.prefixItems.map((schema) => - escapeSchema({ - components: props.components, - schema: schema, - recursive: props.recursive, - visited: props.visited, - }), - ); - const additionalItems: OpenApi.IJsonSchema | null | boolean | undefined = - props.schema.additionalItems - ? typeof props.schema.additionalItems === "object" && - props.schema.additionalItems !== null - ? escapeSchema({ - components: props.components, - schema: props.schema.additionalItems, - recursive: props.recursive, - visited: props.visited, - }) - : props.schema.additionalItems - : false; - if (elements.some((v) => v === null) || additionalItems === null) - return null; - else if (elements.some((v) => v === undefined)) return undefined; - return { - ...props.schema, - prefixItems: elements as OpenApi.IJsonSchema[], - additionalItems: additionalItems ?? false, - }; - } else if (isArray(props.schema)) { - // ARRAY - const items: OpenApi.IJsonSchema | null | undefined = escapeSchema({ - components: props.components, - schema: props.schema.items, - recursive: props.recursive, - visited: props.visited, - }); - if (items === null) return null; - else if (items === undefined) - return { - ...props.schema, - minItems: undefined, - maxItems: 0, - items: {}, - }; - return { - ...props.schema, - items: items, - }; - } - return props.schema; - }; - - const coverStation = (p: { - components: OpenApi.IComponents; - visited: Map>; - x: OpenApi.IJsonSchema; - y: OpenApi.IJsonSchema; - }): boolean => { - const cache: boolean | undefined = p.visited.get(p.x)?.get(p.y); - if (cache !== undefined) return cache; - - // FOR RECURSIVE CASE - const nested: Map = MapUtil.take(p.visited)( - p.x, - )(() => new Map()); - nested.set(p.y, true); - - // COMPUTE IT - const result: boolean = coverSchema(p); - nested.set(p.y, result); - return result; - }; - - const coverSchema = (p: { - components: OpenApi.IComponents; - visited: Map>; - x: OpenApi.IJsonSchema; - y: OpenApi.IJsonSchema; - }): boolean => { - // CHECK EQUALITY - if (p.x === p.y) return true; - else if (isReference(p.x) && isReference(p.y) && p.x.$ref === p.y.$ref) - return true; - - // COMPARE WITH FLATTENING - const alpha: OpenApi.IJsonSchema[] = flatSchema(p.components, p.x); - const beta: OpenApi.IJsonSchema[] = flatSchema(p.components, p.y); - if (alpha.some((x) => isUnknown(x))) return true; - else if (beta.some((x) => isUnknown(x))) return false; - return beta.every((b) => - alpha.some((a) => - coverEscapedSchema({ - components: p.components, - visited: p.visited, - x: a, - y: b, - }), - ), - ); - }; - - const coverEscapedSchema = (p: { - components: OpenApi.IComponents; - visited: Map>; - x: OpenApi.IJsonSchema; - y: OpenApi.IJsonSchema; - }): boolean => { - // CHECK EQUALITY - if (p.x === p.y) return true; - else if (isUnknown(p.x)) return true; - else if (isUnknown(p.y)) return false; - else if (isNull(p.x)) return isNull(p.y); - // ATOMIC CASE - else if (isConstant(p.x)) return isConstant(p.y) && p.x.const === p.y.const; - else if (isBoolean(p.x)) - return ( - isBoolean(p.y) || (isConstant(p.y) && typeof p.y.const === "boolean") - ); - else if (isInteger(p.x)) - return (isInteger(p.y) || isConstant(p.y)) && coverInteger(p.x, p.y); - else if (isNumber(p.x)) - return ( - (isConstant(p.y) || isInteger(p.y) || isNumber(p.y)) && - coverNumber(p.x, p.y) - ); - else if (isString(p.x)) - return (isConstant(p.y) || isString(p.y)) && coverString(p.x, p.y); - // INSTANCE CASE - else if (isArray(p.x)) - return ( - (isArray(p.y) || isTuple(p.y)) && - coverArray({ - components: p.components, - visited: p.visited, - x: p.x, - y: p.y, - }) - ); - else if (isObject(p.x)) - return ( - isObject(p.y) && - coverObject({ - components: p.components, - visited: p.visited, - x: p.x, - y: p.y, - }) - ); - else if (isReference(p.x)) return isReference(p.y) && p.x.$ref === p.y.$ref; - return false; - }; - - const coverArray = (p: { - components: OpenApi.IComponents; - visited: Map>; - x: OpenApi.IJsonSchema.IArray; - y: OpenApi.IJsonSchema.IArray | OpenApi.IJsonSchema.ITuple; - }): boolean => { - if (isTuple(p.y)) - return ( - p.y.prefixItems.every((v) => - coverStation({ - components: p.components, - visited: p.visited, - x: p.x.items, - y: v, - }), - ) && - (p.y.additionalItems === undefined || - (typeof p.y.additionalItems === "object" && - coverStation({ - components: p.components, - visited: p.visited, - x: p.x.items, - y: p.y.additionalItems, - }))) - ); - else if ( - !( - p.x.minItems === undefined || - (p.y.minItems !== undefined && p.x.minItems <= p.y.minItems) - ) - ) - return false; - else if ( - !( - p.x.maxItems === undefined || - (p.y.maxItems !== undefined && p.x.maxItems >= p.y.maxItems) - ) - ) - return false; - return coverStation({ - components: p.components, - visited: p.visited, - x: p.x.items, - y: p.y.items, - }); - }; - - const coverObject = (p: { - components: OpenApi.IComponents; - visited: Map>; - x: OpenApi.IJsonSchema.IObject; - y: OpenApi.IJsonSchema.IObject; - }): boolean => { - if (!p.x.additionalProperties && !!p.y.additionalProperties) return false; - else if ( - !!p.x.additionalProperties && - !!p.y.additionalProperties && - ((typeof p.x.additionalProperties === "object" && - p.y.additionalProperties === true) || - (typeof p.x.additionalProperties === "object" && - typeof p.y.additionalProperties === "object" && - !coverStation({ - components: p.components, - visited: p.visited, - x: p.x.additionalProperties, - y: p.y.additionalProperties, - }))) - ) - return false; - return Object.entries(p.y.properties ?? {}).every(([key, b]) => { - const a: OpenApi.IJsonSchema | undefined = p.x.properties?.[key]; - if (a === undefined) return false; - else if ( - p.x.required.includes(key) === true && - p.y.required.includes(key) === false - ) - return false; - return coverStation({ - components: p.components, - visited: p.visited, - x: a, - y: b, - }); - }); - }; - - const coverInteger = ( - x: OpenApi.IJsonSchema.IInteger, - y: OpenApi.IJsonSchema.IConstant | OpenApi.IJsonSchema.IInteger, - ): boolean => { - if (isConstant(y)) - return typeof y.const === "number" && Number.isInteger(y.const); - return [ - x.type === y.type, - x.minimum === undefined || - (y.minimum !== undefined && x.minimum <= y.minimum), - x.maximum === undefined || - (y.maximum !== undefined && x.maximum >= y.maximum), - x.exclusiveMinimum !== true || - x.minimum === undefined || - (y.minimum !== undefined && - (y.exclusiveMinimum === true || x.minimum < y.minimum)), - x.exclusiveMaximum !== true || - x.maximum === undefined || - (y.maximum !== undefined && - (y.exclusiveMaximum === true || x.maximum > y.maximum)), - x.multipleOf === undefined || - (y.multipleOf !== undefined && - y.multipleOf / x.multipleOf === - Math.floor(y.multipleOf / x.multipleOf)), - ].every((v) => v); - }; - - const coverNumber = ( - x: OpenApi.IJsonSchema.INumber, - y: - | OpenApi.IJsonSchema.IConstant - | OpenApi.IJsonSchema.IInteger - | OpenApi.IJsonSchema.INumber, - ): boolean => { - if (isConstant(y)) return typeof y.const === "number"; - return [ - x.type === y.type || (x.type === "number" && y.type === "integer"), - x.minimum === undefined || - (y.minimum !== undefined && x.minimum <= y.minimum), - x.maximum === undefined || - (y.maximum !== undefined && x.maximum >= y.maximum), - x.exclusiveMinimum !== true || - x.minimum === undefined || - (y.minimum !== undefined && - (y.exclusiveMinimum === true || x.minimum < y.minimum)), - x.exclusiveMaximum !== true || - x.maximum === undefined || - (y.maximum !== undefined && - (y.exclusiveMaximum === true || x.maximum > y.maximum)), - x.multipleOf === undefined || - (y.multipleOf !== undefined && - y.multipleOf / x.multipleOf === - Math.floor(y.multipleOf / x.multipleOf)), - ].every((v) => v); - }; - - const coverString = ( - x: OpenApi.IJsonSchema.IString, - y: OpenApi.IJsonSchema.IConstant | OpenApi.IJsonSchema.IString, - ): boolean => { - if (isConstant(y)) return typeof y.const === "string"; - return [ - x.format === undefined || - (y.format !== undefined && coverFormat(x.format, y.format)), - x.pattern === undefined || x.pattern === y.pattern, - x.minLength === undefined || - (y.minLength !== undefined && x.minLength <= y.minLength), - x.maxLength === undefined || - (y.maxLength !== undefined && x.maxLength >= y.maxLength), - ].every((v) => v); - }; - - const coverFormat = ( - x: Required["format"], - y: Required["format"], - ): boolean => - x === y || - (x === "idn-email" && y === "email") || - (x === "idn-hostname" && y === "hostname") || - (["uri", "iri"].includes(x) && y === "url") || - (x === "iri" && y === "uri") || - (x === "iri-reference" && y === "uri-reference"); - - const flatSchema = ( - components: OpenApi.IComponents, - schema: OpenApi.IJsonSchema, - ): OpenApi.IJsonSchema[] => { - schema = escapeReferenceOfFlatSchema(components, schema); - if (OpenApiTypeChecker.isOneOf(schema)) - return schema.oneOf.map((v) => flatSchema(components, v)).flat(); - return [schema]; - }; - - const escapeReferenceOfFlatSchema = ( - components: OpenApi.IComponents, - schema: OpenApi.IJsonSchema, - ): Exclude => { - if (OpenApiTypeChecker.isReference(schema) === false) return schema; - const key = schema.$ref.replace("#/components/schemas/", ""); - const found: OpenApi.IJsonSchema | undefined = escapeReferenceOfFlatSchema( - components, - components.schemas?.[key] ?? {}, - ); - if (found === undefined) - throw new Error( - `Reference type not found: ${JSON.stringify(schema.$ref)}`, - ); - return escapeReferenceOfFlatSchema(components, found); - }; - - /** - * @internal - */ - export const writeReferenceDescription = (props: { - components: OpenApi.IComponents; - $ref: string; - description: string | undefined; - escape: boolean; - }): string | undefined => { - const index: number = props.$ref.lastIndexOf("."); - if (index === -1) return props.description; - - const accessors: string[] = props.$ref - .split("#/components/schemas/")[1] - .split("."); - const pReferences: IParentReference[] = accessors - .slice(0, props.escape ? accessors.length : accessors.length - 1) - .map((_, i, array) => array.slice(0, i + 1).join(".")) - .map((key) => ({ - key, - description: props.components.schemas?.[key]?.description, - })) - .filter((schema): schema is IParentReference => !!schema?.description) - .reverse(); - if (pReferences.length === 0) return props.description; - return [ - ...(props.description?.length ? [props.description] : []), - ...pReferences.map( - (pRef, i) => - `Description of the ${i === 0 && props.escape ? "current" : "parent"} {@link ${pRef.key}} type:\n\n` + - pRef.description - .split("\n") - .map((str) => `> ${str}`) - .join("\n"), - ), - ].join("\n\n------------------------------\n\n"); - }; -} - -/** - * @internal - */ -interface IParentReference { - key: string; - description: string; } diff --git a/src/utils/internal/JsonDescriptionUtil.ts b/src/utils/internal/JsonDescriptionUtil.ts new file mode 100644 index 0000000..dca5908 --- /dev/null +++ b/src/utils/internal/JsonDescriptionUtil.ts @@ -0,0 +1,42 @@ +import { OpenApi } from "../../OpenApi"; + +export namespace JsonDescriptionUtil { + export const cascade = (props: { + prefix: string; + components: OpenApi.IComponents; + $ref: string; + description: string | undefined; + escape: boolean; + }): string | undefined => { + const index: number = props.$ref.lastIndexOf("."); + if (index === -1) return props.description; + + const accessors: string[] = props.$ref.split(props.prefix)[1].split("."); + const pReferences: IParentReference[] = accessors + .slice(0, props.escape ? accessors.length : accessors.length - 1) + .map((_, i, array) => array.slice(0, i + 1).join(".")) + .map((key) => ({ + key, + description: props.components.schemas?.[key]?.description, + })) + .filter((schema): schema is IParentReference => !!schema?.description) + .reverse(); + if (pReferences.length === 0) return props.description; + return [ + ...(props.description?.length ? [props.description] : []), + ...pReferences.map( + (pRef, i) => + `Description of the ${i === 0 && props.escape ? "current" : "parent"} {@link ${pRef.key}} type:\n\n` + + pRef.description + .split("\n") + .map((str) => `> ${str}`) + .join("\n"), + ), + ].join("\n\n------------------------------\n\n"); + }; +} + +interface IParentReference { + key: string; + description: string; +} diff --git a/src/utils/internal/OpenApiTypeCheckerBase.ts b/src/utils/internal/OpenApiTypeCheckerBase.ts new file mode 100644 index 0000000..513bedb --- /dev/null +++ b/src/utils/internal/OpenApiTypeCheckerBase.ts @@ -0,0 +1,696 @@ +import { OpenApi } from "../../OpenApi"; +import { MapUtil } from "../MapUtil"; +import { JsonDescriptionUtil } from "./JsonDescriptionUtil"; + +/** + * @internal + */ +export namespace OpenApiTypeCheckerBase { + /* ----------------------------------------------------------- + TYPE CHECKERS + ----------------------------------------------------------- */ + export const isNull = ( + schema: OpenApi.IJsonSchema, + ): schema is OpenApi.IJsonSchema.INull => + (schema as OpenApi.IJsonSchema.INull).type === "null"; + + export const isUnknown = ( + schema: OpenApi.IJsonSchema, + ): schema is OpenApi.IJsonSchema.IUnknown => + (schema as OpenApi.IJsonSchema.IUnknown).type === undefined && + !isConstant(schema) && + !isOneOf(schema) && + !isReference(schema); + + export const isConstant = ( + schema: OpenApi.IJsonSchema, + ): schema is OpenApi.IJsonSchema.IConstant => + (schema as OpenApi.IJsonSchema.IConstant).const !== undefined; + + export const isBoolean = ( + schema: OpenApi.IJsonSchema, + ): schema is OpenApi.IJsonSchema.IBoolean => + (schema as OpenApi.IJsonSchema.IBoolean).type === "boolean"; + + export const isInteger = ( + schema: OpenApi.IJsonSchema, + ): schema is OpenApi.IJsonSchema.IInteger => + (schema as OpenApi.IJsonSchema.IInteger).type === "integer"; + + export const isNumber = ( + schema: OpenApi.IJsonSchema, + ): schema is OpenApi.IJsonSchema.INumber => + (schema as OpenApi.IJsonSchema.INumber).type === "number"; + + export const isString = ( + schema: OpenApi.IJsonSchema, + ): schema is OpenApi.IJsonSchema.IString => + (schema as OpenApi.IJsonSchema.IString).type === "string"; + + export const isArray = ( + schema: OpenApi.IJsonSchema, + ): schema is OpenApi.IJsonSchema.IArray => + (schema as OpenApi.IJsonSchema.IArray).type === "array" && + (schema as OpenApi.IJsonSchema.IArray).items !== undefined; + + export const isTuple = ( + schema: OpenApi.IJsonSchema, + ): schema is OpenApi.IJsonSchema.ITuple => + (schema as OpenApi.IJsonSchema.ITuple).type === "array" && + (schema as OpenApi.IJsonSchema.ITuple).prefixItems !== undefined; + + export const isObject = ( + schema: OpenApi.IJsonSchema, + ): schema is OpenApi.IJsonSchema.IObject => + (schema as OpenApi.IJsonSchema.IObject).type === "object"; + + export const isReference = ( + schema: OpenApi.IJsonSchema, + ): schema is OpenApi.IJsonSchema.IReference => + (schema as any).$ref !== undefined; + + export const isOneOf = ( + schema: OpenApi.IJsonSchema, + ): schema is OpenApi.IJsonSchema.IOneOf => + (schema as OpenApi.IJsonSchema.IOneOf).oneOf !== undefined; + + export const isRecursiveReference = (props: { + prefix: string; + components: OpenApi.IComponents; + schema: OpenApi.IJsonSchema; + }): boolean => { + if (isReference(props.schema) === false) return false; + const current: string = props.schema.$ref.split(props.prefix)[1]; + let counter: number = 0; + visit({ + prefix: props.prefix, + components: props.components, + schema: props.schema, + closure: (schema) => { + if (isReference(schema)) { + const next: string = schema.$ref.split(props.prefix)[1]; + if (current === next) ++counter; + } + }, + }); + return counter > 1; + }; + + /* ----------------------------------------------------------- + OPERATORS + ----------------------------------------------------------- */ + export const escape = (props: { + prefix: string; + components: OpenApi.IComponents; + schema: OpenApi.IJsonSchema; + recursive: false | number; + }): OpenApi.IJsonSchema | null => + escapeSchema({ + prefix: props.prefix, + components: props.components, + schema: props.schema, + recursive: props.recursive, + visited: new Map(), + }) || null; + + export const visit = (props: { + prefix: string; + closure: (schema: OpenApi.IJsonSchema) => void; + components: OpenApi.IComponents; + schema: OpenApi.IJsonSchema; + }): void => { + const already: Set = new Set(); + const next = (schema: OpenApi.IJsonSchema): void => { + props.closure(schema); + if (isReference(schema)) { + const key: string = schema.$ref.split(props.prefix).pop()!; + if (already.has(key) === true) return; + already.add(key); + const found: OpenApi.IJsonSchema | undefined = + props.components.schemas?.[key]; + if (found !== undefined) next(found); + } else if (isOneOf(schema)) schema.oneOf.forEach(next); + else if (isObject(schema)) { + for (const value of Object.values(schema.properties ?? {})) next(value); + if ( + typeof schema.additionalProperties === "object" && + schema.additionalProperties !== null + ) + next(schema.additionalProperties); + } else if (isArray(schema)) next(schema.items); + else if (isTuple(schema)) { + (schema.prefixItems ?? []).forEach(next); + if ( + typeof schema.additionalItems === "object" && + schema.additionalItems !== null + ) + next(schema.additionalItems); + } + }; + next(props.schema); + }; + + export const covers = (props: { + prefix: string; + components: OpenApi.IComponents; + x: OpenApi.IJsonSchema; + y: OpenApi.IJsonSchema; + }): boolean => + coverStation({ + prefix: props.prefix, + components: props.components, + x: props.x, + y: props.y, + visited: new Map(), + }); + + const escapeSchema = (props: { + components: OpenApi.IComponents; + prefix: string; + schema: OpenApi.IJsonSchema; + recursive: false | number; + visited: Map; + }): OpenApi.IJsonSchema | null | undefined => { + if (isReference(props.schema)) { + // REFERENCE + const name: string = props.schema.$ref.split(props.prefix)[1]; + const target: OpenApi.IJsonSchema | undefined = + props.components.schemas?.[name]; + if (target === undefined) return null; + else if (props.visited.has(name) === true) { + if (props.recursive === false) return null; + const depth: number = props.visited.get(name)!; + if (depth > props.recursive) return undefined; + props.visited.set(name, depth + 1); + const res: OpenApi.IJsonSchema | null | undefined = escapeSchema({ + prefix: props.prefix, + recursive: props.recursive, + components: props.components, + schema: target, + visited: props.visited, + }); + return res + ? { + ...res, + description: JsonDescriptionUtil.cascade({ + prefix: props.prefix, + components: props.components, + $ref: props.schema.$ref, + description: res.description, + escape: true, + }), + } + : res; + } + const res: OpenApi.IJsonSchema | null | undefined = escapeSchema({ + prefix: props.prefix, + recursive: props.recursive, + components: props.components, + schema: target, + visited: new Map([...props.visited, [name, 1]]), + }); + return res + ? { + ...res, + description: JsonDescriptionUtil.cascade({ + prefix: props.prefix, + components: props.components, + $ref: props.schema.$ref, + description: res.description, + escape: true, + }), + } + : res; + } else if (isOneOf(props.schema)) { + // UNION + const elements: Array = + props.schema.oneOf.map((schema) => + escapeSchema({ + prefix: props.prefix, + recursive: props.recursive, + components: props.components, + schema: schema, + visited: props.visited, + }), + ); + if (elements.some((v) => v === null)) return null; + const filtered: OpenApi.IJsonSchema[] = elements.filter( + (v) => v !== undefined, + ) as OpenApi.IJsonSchema[]; + if (filtered.length === 0) return undefined; + return { + ...props, + oneOf: filtered + .map((v) => + flatSchema({ + prefix: props.prefix, + components: props.components, + schema: v, + }), + ) + .flat(), + }; + } else if (isObject(props.schema)) { + // OBJECT + const object: OpenApi.IJsonSchema.IObject = props.schema; + const properties: Array< + [string, OpenApi.IJsonSchema | null | undefined] + > = Object.entries(object.properties ?? {}).map(([key, value]) => [ + key, + escapeSchema({ + prefix: props.prefix, + recursive: props.recursive, + components: props.components, + schema: value, + visited: props.visited, + }), + ]); + const additionalProperties: + | OpenApi.IJsonSchema + | null + | boolean + | undefined = object.additionalProperties + ? typeof object.additionalProperties === "object" && + object.additionalProperties !== null + ? escapeSchema({ + prefix: props.prefix, + recursive: props.recursive, + components: props.components, + schema: object.additionalProperties, + visited: props.visited, + }) + : object.additionalProperties + : false; + if ( + properties.some(([_k, v]) => v === null) || + additionalProperties === null + ) + return null; + else if ( + properties.some( + ([k, v]) => v === undefined && object.required.includes(k) === true, + ) === true + ) + return undefined; + return { + ...object, + properties: Object.fromEntries( + properties.filter(([_k, v]) => v !== undefined) as Array< + [string, OpenApi.IJsonSchema] + >, + ), + additionalProperties: additionalProperties ?? false, + required: object.required.filter((k) => + properties.some(([key, value]) => key === k && value !== undefined), + ), + }; + } else if (isTuple(props.schema)) { + // TUPLE + const elements: Array = + props.schema.prefixItems.map((schema) => + escapeSchema({ + prefix: props.prefix, + recursive: props.recursive, + components: props.components, + schema: schema, + visited: props.visited, + }), + ); + const additionalItems: OpenApi.IJsonSchema | null | boolean | undefined = + props.schema.additionalItems + ? typeof props.schema.additionalItems === "object" && + props.schema.additionalItems !== null + ? escapeSchema({ + prefix: props.prefix, + recursive: props.recursive, + components: props.components, + schema: props.schema.additionalItems, + visited: props.visited, + }) + : props.schema.additionalItems + : false; + if (elements.some((v) => v === null) || additionalItems === null) + return null; + else if (elements.some((v) => v === undefined)) return undefined; + return { + ...props.schema, + prefixItems: elements as OpenApi.IJsonSchema[], + additionalItems: additionalItems ?? false, + }; + } else if (isArray(props.schema)) { + // ARRAY + const items: OpenApi.IJsonSchema | null | undefined = escapeSchema({ + prefix: props.prefix, + recursive: props.recursive, + components: props.components, + schema: props.schema.items, + visited: props.visited, + }); + if (items === null) return null; + else if (items === undefined) + return { + ...props.schema, + minItems: undefined, + maxItems: 0, + items: {}, + }; + return { + ...props.schema, + items: items, + }; + } + return props.schema; + }; + + const coverStation = (p: { + prefix: string; + components: OpenApi.IComponents; + visited: Map>; + x: OpenApi.IJsonSchema; + y: OpenApi.IJsonSchema; + }): boolean => { + const cache: boolean | undefined = p.visited.get(p.x)?.get(p.y); + if (cache !== undefined) return cache; + + // FOR RECURSIVE CASE + const nested: Map = MapUtil.take(p.visited)( + p.x, + )(() => new Map()); + nested.set(p.y, true); + + // COMPUTE IT + const result: boolean = coverSchema(p); + nested.set(p.y, result); + return result; + }; + + const coverSchema = (p: { + prefix: string; + components: OpenApi.IComponents; + visited: Map>; + x: OpenApi.IJsonSchema; + y: OpenApi.IJsonSchema; + }): boolean => { + // CHECK EQUALITY + if (p.x === p.y) return true; + else if (isReference(p.x) && isReference(p.y) && p.x.$ref === p.y.$ref) + return true; + + // COMPARE WITH FLATTENING + const alpha: OpenApi.IJsonSchema[] = flatSchema({ + prefix: p.prefix, + components: p.components, + schema: p.x, + }); + const beta: OpenApi.IJsonSchema[] = flatSchema({ + prefix: p.prefix, + components: p.components, + schema: p.y, + }); + if (alpha.some((x) => isUnknown(x))) return true; + else if (beta.some((x) => isUnknown(x))) return false; + return beta.every((b) => + alpha.some((a) => + coverEscapedSchema({ + prefix: p.prefix, + components: p.components, + visited: p.visited, + x: a, + y: b, + }), + ), + ); + }; + + const coverEscapedSchema = (p: { + prefix: string; + components: OpenApi.IComponents; + visited: Map>; + x: OpenApi.IJsonSchema; + y: OpenApi.IJsonSchema; + }): boolean => { + // CHECK EQUALITY + if (p.x === p.y) return true; + else if (isUnknown(p.x)) return true; + else if (isUnknown(p.y)) return false; + else if (isNull(p.x)) return isNull(p.y); + // ATOMIC CASE + else if (isConstant(p.x)) return isConstant(p.y) && p.x.const === p.y.const; + else if (isBoolean(p.x)) + return ( + isBoolean(p.y) || (isConstant(p.y) && typeof p.y.const === "boolean") + ); + else if (isInteger(p.x)) + return (isInteger(p.y) || isConstant(p.y)) && coverInteger(p.x, p.y); + else if (isNumber(p.x)) + return ( + (isConstant(p.y) || isInteger(p.y) || isNumber(p.y)) && + coverNumber(p.x, p.y) + ); + else if (isString(p.x)) + return (isConstant(p.y) || isString(p.y)) && coverString(p.x, p.y); + // INSTANCE CASE + else if (isArray(p.x)) + return ( + (isArray(p.y) || isTuple(p.y)) && + coverArray({ + prefix: p.prefix, + components: p.components, + visited: p.visited, + x: p.x, + y: p.y, + }) + ); + else if (isObject(p.x)) + return ( + isObject(p.y) && + coverObject({ + prefix: p.prefix, + components: p.components, + visited: p.visited, + x: p.x, + y: p.y, + }) + ); + else if (isReference(p.x)) return isReference(p.y) && p.x.$ref === p.y.$ref; + return false; + }; + + const coverArray = (p: { + prefix: string; + components: OpenApi.IComponents; + visited: Map>; + x: OpenApi.IJsonSchema.IArray; + y: OpenApi.IJsonSchema.IArray | OpenApi.IJsonSchema.ITuple; + }): boolean => { + if (isTuple(p.y)) + return ( + p.y.prefixItems.every((v) => + coverStation({ + prefix: p.prefix, + components: p.components, + visited: p.visited, + x: p.x.items, + y: v, + }), + ) && + (p.y.additionalItems === undefined || + (typeof p.y.additionalItems === "object" && + coverStation({ + prefix: p.prefix, + components: p.components, + visited: p.visited, + x: p.x.items, + y: p.y.additionalItems, + }))) + ); + else if ( + !( + p.x.minItems === undefined || + (p.y.minItems !== undefined && p.x.minItems <= p.y.minItems) + ) + ) + return false; + else if ( + !( + p.x.maxItems === undefined || + (p.y.maxItems !== undefined && p.x.maxItems >= p.y.maxItems) + ) + ) + return false; + return coverStation({ + prefix: p.prefix, + components: p.components, + visited: p.visited, + x: p.x.items, + y: p.y.items, + }); + }; + + const coverObject = (p: { + prefix: string; + components: OpenApi.IComponents; + visited: Map>; + x: OpenApi.IJsonSchema.IObject; + y: OpenApi.IJsonSchema.IObject; + }): boolean => { + if (!p.x.additionalProperties && !!p.y.additionalProperties) return false; + else if ( + !!p.x.additionalProperties && + !!p.y.additionalProperties && + ((typeof p.x.additionalProperties === "object" && + p.y.additionalProperties === true) || + (typeof p.x.additionalProperties === "object" && + typeof p.y.additionalProperties === "object" && + !coverStation({ + prefix: p.prefix, + components: p.components, + visited: p.visited, + x: p.x.additionalProperties, + y: p.y.additionalProperties, + }))) + ) + return false; + return Object.entries(p.y.properties ?? {}).every(([key, b]) => { + const a: OpenApi.IJsonSchema | undefined = p.x.properties?.[key]; + if (a === undefined) return false; + else if ( + p.x.required.includes(key) === true && + p.y.required.includes(key) === false + ) + return false; + return coverStation({ + prefix: p.prefix, + components: p.components, + visited: p.visited, + x: a, + y: b, + }); + }); + }; + + const coverInteger = ( + x: OpenApi.IJsonSchema.IInteger, + y: OpenApi.IJsonSchema.IConstant | OpenApi.IJsonSchema.IInteger, + ): boolean => { + if (isConstant(y)) + return typeof y.const === "number" && Number.isInteger(y.const); + return [ + x.type === y.type, + x.minimum === undefined || + (y.minimum !== undefined && x.minimum <= y.minimum), + x.maximum === undefined || + (y.maximum !== undefined && x.maximum >= y.maximum), + x.exclusiveMinimum !== true || + x.minimum === undefined || + (y.minimum !== undefined && + (y.exclusiveMinimum === true || x.minimum < y.minimum)), + x.exclusiveMaximum !== true || + x.maximum === undefined || + (y.maximum !== undefined && + (y.exclusiveMaximum === true || x.maximum > y.maximum)), + x.multipleOf === undefined || + (y.multipleOf !== undefined && + y.multipleOf / x.multipleOf === + Math.floor(y.multipleOf / x.multipleOf)), + ].every((v) => v); + }; + + const coverNumber = ( + x: OpenApi.IJsonSchema.INumber, + y: + | OpenApi.IJsonSchema.IConstant + | OpenApi.IJsonSchema.IInteger + | OpenApi.IJsonSchema.INumber, + ): boolean => { + if (isConstant(y)) return typeof y.const === "number"; + return [ + x.type === y.type || (x.type === "number" && y.type === "integer"), + x.minimum === undefined || + (y.minimum !== undefined && x.minimum <= y.minimum), + x.maximum === undefined || + (y.maximum !== undefined && x.maximum >= y.maximum), + x.exclusiveMinimum !== true || + x.minimum === undefined || + (y.minimum !== undefined && + (y.exclusiveMinimum === true || x.minimum < y.minimum)), + x.exclusiveMaximum !== true || + x.maximum === undefined || + (y.maximum !== undefined && + (y.exclusiveMaximum === true || x.maximum > y.maximum)), + x.multipleOf === undefined || + (y.multipleOf !== undefined && + y.multipleOf / x.multipleOf === + Math.floor(y.multipleOf / x.multipleOf)), + ].every((v) => v); + }; + + const coverString = ( + x: OpenApi.IJsonSchema.IString, + y: OpenApi.IJsonSchema.IConstant | OpenApi.IJsonSchema.IString, + ): boolean => { + if (isConstant(y)) return typeof y.const === "string"; + return [ + x.format === undefined || + (y.format !== undefined && coverFormat(x.format, y.format)), + x.pattern === undefined || x.pattern === y.pattern, + x.minLength === undefined || + (y.minLength !== undefined && x.minLength <= y.minLength), + x.maxLength === undefined || + (y.maxLength !== undefined && x.maxLength >= y.maxLength), + ].every((v) => v); + }; + + const coverFormat = ( + x: Required["format"], + y: Required["format"], + ): boolean => + x === y || + (x === "idn-email" && y === "email") || + (x === "idn-hostname" && y === "hostname") || + (["uri", "iri"].includes(x) && y === "url") || + (x === "iri" && y === "uri") || + (x === "iri-reference" && y === "uri-reference"); + + const flatSchema = (props: { + prefix: string; + components: OpenApi.IComponents; + schema: OpenApi.IJsonSchema; + }): OpenApi.IJsonSchema[] => { + const schema = escapeReferenceOfFlatSchema(props); + if (isOneOf(schema)) + return schema.oneOf + .map((v) => + flatSchema({ + prefix: props.prefix, + components: props.components, + schema: v, + }), + ) + .flat(); + return [schema]; + }; + + const escapeReferenceOfFlatSchema = (props: { + prefix: string; + components: OpenApi.IComponents; + schema: OpenApi.IJsonSchema; + }): Exclude => { + if (isReference(props.schema) === false) return props.schema; + const key = props.schema.$ref.replace(props.prefix, ""); + const found: OpenApi.IJsonSchema | undefined = escapeReferenceOfFlatSchema({ + prefix: props.prefix, + components: props.components, + schema: props.components.schemas?.[key] ?? {}, + }); + if (found === undefined) + throw new Error( + `Reference type not found: ${JSON.stringify(props.schema.$ref)}`, + ); + return escapeReferenceOfFlatSchema({ + prefix: props.prefix, + components: props.components, + schema: found, + }); + }; +} diff --git a/test/examples/chatgpt-function-calling.ts b/test/examples/chatgpt-function-calling.ts index 979909c..30714c6 100644 --- a/test/examples/chatgpt-function-calling.ts +++ b/test/examples/chatgpt-function-calling.ts @@ -18,7 +18,7 @@ const main = async (): Promise => { ChatGptConverter.parameters({ components: collection.components, schema: typia.assert(collection.schemas[0]), - options: { + config: { reference: process.argv.includes("--reference"), constraint: process.argv.includes("--constraint"), }, diff --git a/test/examples/gemini-function-calling.ts b/test/examples/gemini-function-calling.ts index fe77548..79f661f 100644 --- a/test/examples/gemini-function-calling.ts +++ b/test/examples/gemini-function-calling.ts @@ -23,7 +23,9 @@ const main = async (): Promise => { GeminiConverter.parameters({ components: collection.components, schema: typia.assert(collection.schemas[0]), - recursive: 3, + config: { + recursive: 3, + }, }); if (parameters === null) throw new Error("Failed to convert the JSON schema to the Gemini schema."); diff --git a/test/features/llm/chatgpt/test_chatgpt_schema_anyof.ts b/test/features/llm/chatgpt/test_chatgpt_schema_anyof.ts index 3b542fe..53236c0 100644 --- a/test/features/llm/chatgpt/test_chatgpt_schema_anyof.ts +++ b/test/features/llm/chatgpt/test_chatgpt_schema_anyof.ts @@ -12,7 +12,7 @@ export const test_chatgpt_schema_anyof = (): void => { $defs, components: collection.components, schema: collection.schemas[0], - options: { + config: { constraint: false, reference: false, }, diff --git a/test/features/llm/chatgpt/test_chatgpt_schema_recursive_array.ts b/test/features/llm/chatgpt/test_chatgpt_schema_recursive_array.ts index 7e1cec2..a356814 100644 --- a/test/features/llm/chatgpt/test_chatgpt_schema_recursive_array.ts +++ b/test/features/llm/chatgpt/test_chatgpt_schema_recursive_array.ts @@ -28,7 +28,7 @@ export const test_chatgpt_schema_recursive_array = (): void => { schema: { $ref: "#/components/schemas/Department", }, - options: { + config: { constraint: true, reference: false, }, diff --git a/test/features/llm/chatgpt/test_chatgpt_schema_ref.ts b/test/features/llm/chatgpt/test_chatgpt_schema_ref.ts index 8724b33..755b9da 100644 --- a/test/features/llm/chatgpt/test_chatgpt_schema_ref.ts +++ b/test/features/llm/chatgpt/test_chatgpt_schema_ref.ts @@ -74,7 +74,7 @@ const test = ( $defs, components: collection.components, schema: collection.schemas[0], - options: { + config: { reference: false, constraint: true, }, diff --git a/test/features/llm/function-calling/test_llm_function_calling_chatgpt_example.ts b/test/features/llm/function-calling/test_llm_function_calling_chatgpt_example.ts index 113412b..bd0f410 100644 --- a/test/features/llm/function-calling/test_llm_function_calling_chatgpt_example.ts +++ b/test/features/llm/function-calling/test_llm_function_calling_chatgpt_example.ts @@ -20,7 +20,7 @@ export const test_llm_function_calling_chatgpt_example = schema: typia.assert( collection.schemas[0], ), - options: { + config: { reference: true, constraint: true, }, diff --git a/test/features/llm/function-calling/test_llm_function_calling_chatgpt_recursive.ts b/test/features/llm/function-calling/test_llm_function_calling_chatgpt_recursive.ts index ffb94a5..49e3196 100644 --- a/test/features/llm/function-calling/test_llm_function_calling_chatgpt_recursive.ts +++ b/test/features/llm/function-calling/test_llm_function_calling_chatgpt_recursive.ts @@ -20,7 +20,7 @@ export const test_llm_function_calling_chatgpt_recursive = schema: typia.assert( collection.schemas[0], ), - options: { + config: { reference: true, constraint: false, }, diff --git a/test/features/llm/function-calling/test_llm_function_calling_chatgpt_sale.ts b/test/features/llm/function-calling/test_llm_function_calling_chatgpt_sale.ts index 7e5bc12..667bd11 100644 --- a/test/features/llm/function-calling/test_llm_function_calling_chatgpt_sale.ts +++ b/test/features/llm/function-calling/test_llm_function_calling_chatgpt_sale.ts @@ -21,7 +21,7 @@ export const test_llm_function_calling_chatgpt_sale = schema: typia.assert( collection.schemas[0], ), - options: { + config: { reference: process.argv.includes("--reference"), constraint: process.argv.includes("--constraint"), }, diff --git a/test/features/llm/function-calling/test_llm_function_calling_chatgpt_tags.ts b/test/features/llm/function-calling/test_llm_function_calling_chatgpt_tags.ts index 0858693..aa2d320 100644 --- a/test/features/llm/function-calling/test_llm_function_calling_chatgpt_tags.ts +++ b/test/features/llm/function-calling/test_llm_function_calling_chatgpt_tags.ts @@ -20,7 +20,7 @@ export const test_llm_function_calling_chatgpt_tags = schema: typia.assert( collection.schemas[0], ), - options: { + config: { reference: true, constraint: true, }, diff --git a/test/features/llm/function-calling/test_llm_function_calling_gemini_example.ts b/test/features/llm/function-calling/test_llm_function_calling_gemini_example.ts index 4b4c8db..5641d46 100644 --- a/test/features/llm/function-calling/test_llm_function_calling_gemini_example.ts +++ b/test/features/llm/function-calling/test_llm_function_calling_gemini_example.ts @@ -25,7 +25,9 @@ export const test_llm_function_calling_gemini_example = schema: typia.assert( collection.schemas[0], ), - recursive: false, + config: { + recursive: false, + }, }); if (parameters === null) throw new Error( diff --git a/test/features/llm/function-calling/test_llm_function_calling_gemini_sale.ts b/test/features/llm/function-calling/test_llm_function_calling_gemini_sale.ts index 5112e3a..d0474f3 100644 --- a/test/features/llm/function-calling/test_llm_function_calling_gemini_sale.ts +++ b/test/features/llm/function-calling/test_llm_function_calling_gemini_sale.ts @@ -26,7 +26,9 @@ export const test_llm_function_calling_gemini_sale = schema: typia.assert( collection.schemas[0], ), - recursive: 3, + config: { + recursive: 3, + }, }); if (parameters === null) throw new Error( diff --git a/test/features/llm/test_llm_schema_nullable.ts b/test/features/llm/test_llm_schema_nullable.ts index 8fc7ed5..03b5ca6 100644 --- a/test/features/llm/test_llm_schema_nullable.ts +++ b/test/features/llm/test_llm_schema_nullable.ts @@ -43,7 +43,10 @@ export const test_llm_schema_union = (): void => { const llm: ILlmSchemaV3 | null = LlmConverterV3.schema({ components, schema, - recursive: false, + config: { + recursive: false, + constraint: true, + }, }); TestValidator.equals("nullable")(llm)({ oneOf: [ diff --git a/test/features/llm/test_llm_schema_object.ts b/test/features/llm/test_llm_schema_object.ts index 206aa83..ccbba2c 100644 --- a/test/features/llm/test_llm_schema_object.ts +++ b/test/features/llm/test_llm_schema_object.ts @@ -8,7 +8,10 @@ export const test_llm_schema_object = (): void => { const schema: ILlmSchemaV3 | null = LlmConverterV3.schema({ components: app.components, schema: app.schemas[0], - recursive: false, + config: { + recursive: false, + constraint: true, + }, }); TestValidator.equals("schema")(schema)({ type: "object", diff --git a/test/features/llm/test_llm_schema_oneof.ts b/test/features/llm/test_llm_schema_oneof.ts index 51e30ab..1e0dfcf 100644 --- a/test/features/llm/test_llm_schema_oneof.ts +++ b/test/features/llm/test_llm_schema_oneof.ts @@ -9,7 +9,10 @@ export const test_llm_schema_oneof = (): void => { const casted: ILlmSchemaV3 | null = LlmConverterV3.schema({ components: app.components, schema: app.schemas[0], - recursive: false, + config: { + recursive: false, + constraint: true, + }, }); TestValidator.equals("oneOf")(casted)({ oneOf: [ diff --git a/test/features/llm/test_llm_schema_recursive_array.ts b/test/features/llm/test_llm_schema_recursive_array.ts index 4a9b6e2..b5f9423 100644 --- a/test/features/llm/test_llm_schema_recursive_array.ts +++ b/test/features/llm/test_llm_schema_recursive_array.ts @@ -1,8 +1,8 @@ import { TestValidator } from "@nestia/e2e"; -import { LlmConverterV3_1 } from "@samchon/openapi/lib/converters/LlmConverterV3_1"; +import { LlmConverterV3 } from "@samchon/openapi/lib/converters/LlmConverterV3"; export const test_llm_schema_recursive_array = (): void => { - const schema = LlmConverterV3_1.schema({ + const schema = LlmConverterV3.schema({ components: { schemas: { Department: { @@ -29,7 +29,10 @@ export const test_llm_schema_recursive_array = (): void => { schema: { $ref: "#/components/schemas/Department", }, - recursive: 3, + config: { + recursive: 3, + constraint: true, + }, }); TestValidator.equals("recursive")(schema)({ type: "object", diff --git a/test/features/llm/test_llm_schema_reference_description.ts b/test/features/llm/test_llm_schema_reference_description.ts index d83e9b4..1ec71c7 100644 --- a/test/features/llm/test_llm_schema_reference_description.ts +++ b/test/features/llm/test_llm_schema_reference_description.ts @@ -11,7 +11,10 @@ export const test_llm_schema_reference_description = (): void => { const schema: ILlmSchemaV3 | null = LlmConverterV3.schema({ components: collection.components, schema: collection.schemas[0], - recursive: false, + config: { + recursive: false, + constraint: true, + }, }); TestValidator.predicate("description")( () => diff --git a/test/features/llm/test_llm_schema_separate_array.ts b/test/features/llm/test_llm_schema_separate_array.ts index 39f7fed..393a69d 100644 --- a/test/features/llm/test_llm_schema_separate_array.ts +++ b/test/features/llm/test_llm_schema_separate_array.ts @@ -35,7 +35,10 @@ const schema = (props: { const schema: ILlmSchemaV3 | null = LlmConverterV3.schema({ components: props.components, schema: props.schemas[0], - recursive: false, + config: { + recursive: false, + constraint: true, + }, }); if (schema === null) throw new Error("Invalid schema"); return schema; diff --git a/test/features/llm/test_llm_schema_separate_nested.ts b/test/features/llm/test_llm_schema_separate_nested.ts index a20a631..5394fa0 100644 --- a/test/features/llm/test_llm_schema_separate_nested.ts +++ b/test/features/llm/test_llm_schema_separate_nested.ts @@ -58,7 +58,10 @@ const schema = (props: { const schema: ILlmSchemaV3 | null = LlmConverterV3.schema({ components: props.components, schema: props.schemas[0], - recursive: false, + config: { + recursive: false, + constraint: true, + }, }); if (schema === null) throw new Error("Invalid schema"); return schema; diff --git a/test/features/llm/test_llm_schema_separate_object.ts b/test/features/llm/test_llm_schema_separate_object.ts index ddc4c4e..6b30288 100644 --- a/test/features/llm/test_llm_schema_separate_object.ts +++ b/test/features/llm/test_llm_schema_separate_object.ts @@ -44,7 +44,10 @@ const schema = (props: { const schema: ILlmSchemaV3 | null = LlmConverterV3.schema({ components: props.components, schema: props.schemas[0], - recursive: false, + config: { + recursive: false, + constraint: true, + }, }); if (schema === null) throw new Error("Invalid schema"); return schema; diff --git a/test/features/llm/test_llm_schema_union.ts b/test/features/llm/test_llm_schema_union.ts index e9422b4..abf4865 100644 --- a/test/features/llm/test_llm_schema_union.ts +++ b/test/features/llm/test_llm_schema_union.ts @@ -31,7 +31,10 @@ export const test_llm_schema_union = (): void => { const llm: ILlmSchemaV3 | null = LlmConverterV3.schema({ components, schema, - recursive: false, + config: { + recursive: false, + constraint: true, + }, }); TestValidator.equals("union")(llm)({ type: "number", diff --git a/test/features/llm/test_llm_schema_union_const.ts b/test/features/llm/test_llm_schema_union_const.ts index 21f428a..1fee7e8 100644 --- a/test/features/llm/test_llm_schema_union_const.ts +++ b/test/features/llm/test_llm_schema_union_const.ts @@ -10,7 +10,7 @@ export const test_llm_schema_union_const = (): void => { $defs, components: collection.components, schema: collection.schemas[0], - options: { + config: { constraint: true, reference: false, }, diff --git a/test/features/llm/v3.1/test_llm_schema_v31_ultimate_union.ts b/test/features/llm/v3.1/test_llm_schema_v31_ultimate_union.ts index 4f36b15..c3452e1 100644 --- a/test/features/llm/v3.1/test_llm_schema_v31_ultimate_union.ts +++ b/test/features/llm/v3.1/test_llm_schema_v31_ultimate_union.ts @@ -1,14 +1,20 @@ -import { OpenApi } from "@samchon/openapi"; +import { ILlmSchemaV3_1, OpenApi } from "@samchon/openapi"; import { LlmConverterV3_1 } from "@samchon/openapi/lib/converters/LlmConverterV3_1"; import typia from "typia"; export const test_llm_schema_v31_ultimate_union = (): void => { - const collection = typia.json.schemas<[IJsonSchemaCollection[]]>(); - LlmConverterV3_1.schema({ + const collection: IJsonSchemaCollection = + typia.json.schemas<[IJsonSchemaCollection]>(); + const schema: ILlmSchemaV3_1 | null = LlmConverterV3_1.schema({ components: collection.components, schema: collection.schemas[0], - recursive: 3, + $defs: {}, + config: { + reference: true, + constraint: true, + }, }); + typia.assert(schema); }; interface IJsonSchemaCollection {