diff --git a/src/composers/LlmSchemaComposer.ts b/src/composers/LlmSchemaComposer.ts index b65cebe3..4bfff9e5 100644 --- a/src/composers/LlmSchemaComposer.ts +++ b/src/composers/LlmSchemaComposer.ts @@ -50,7 +50,10 @@ export namespace LlmSchemaComposer { ? JsonDescriptionUtil.cascade({ prefix: "#/components/schemas/", components: props.components, - schema: props.schema, + schema: { + ...props.schema, + description: result.value.description, + }, escape: true, }) : result.value.description, @@ -59,7 +62,7 @@ export namespace LlmSchemaComposer { }; export const schema = (props: { - config: ILlmSchema.IConfig; + config?: Partial; components: OpenApi.IComponents; $defs: Record; schema: OpenApi.IJsonSchema; @@ -140,6 +143,30 @@ export namespace LlmSchemaComposer { }, }; + const visitConstant = (input: OpenApi.IJsonSchema): void => { + const insert = (value: any): void => { + const matched: + | ILlmSchema.IString + | ILlmSchema.INumber + | ILlmSchema.IBoolean + | undefined = union.find( + (u) => + (u as (IJsonSchemaAttribute & { type: string }) | undefined) + ?.type === typeof value, + ) as ILlmSchema.IString | undefined; + if (matched !== undefined) { + matched.enum ??= []; + matched.enum.push(value); + } else + union.push({ + type: typeof value as "number", + enum: [value], + }); + }; + if (OpenApiTypeChecker.isConstant(input)) insert(input.const); + else if (OpenApiTypeChecker.isOneOf(input)) + input.oneOf.forEach(visitConstant); + }; const visit = (input: OpenApi.IJsonSchema, accessor: string): void => { if (OpenApiTypeChecker.isOneOf(input)) { // UNION TYPE @@ -184,7 +211,8 @@ export namespace LlmSchemaComposer { // DISCARD THE REFERENCE TYPE const length: number = union.length; visit(target, accessor); - if (length === union.length - 1 && union[union.length - 1] !== null) + visitConstant(target); + if (length === union.length - 1) union[union.length - 1] = { ...union[union.length - 1]!, description: JsonDescriptionUtil.cascade({ @@ -257,6 +285,10 @@ export namespace LlmSchemaComposer { properties, additionalProperties, required: input.required ?? [], + description: + props.config.strict === true + ? JsonDescriptionUtil.take(input) + : input.description, }); } else if (OpenApiTypeChecker.isArray(input)) { // ARRAY TYPE @@ -278,7 +310,10 @@ export namespace LlmSchemaComposer { ...input, items: items.value, }) - : items.value, + : { + ...input, + items: items.value, + }, ); } else if (OpenApiTypeChecker.isString(input)) union.push( @@ -297,36 +332,12 @@ export namespace LlmSchemaComposer { ); else if (OpenApiTypeChecker.isTuple(input)) return; // UNREACHABLE - else union.push({ ...input }); + else if (OpenApiTypeChecker.isConstant(input) === false) + union.push({ ...input }); }; - const visitConstant = (input: OpenApi.IJsonSchema): void => { - const insert = (value: any): void => { - const matched: - | ILlmSchema.IString - | ILlmSchema.INumber - | ILlmSchema.IBoolean - | undefined = union.find( - (u) => - (u as (IJsonSchemaAttribute & { type: string }) | undefined) - ?.type === typeof value, - ) as ILlmSchema.IString | undefined; - if (matched !== undefined) { - matched.enum ??= []; - matched.enum.push(value); - } else - union.push({ - type: typeof value as "number", - enum: [value], - }); - }; - if (OpenApiTypeChecker.isConstant(input)) insert(input.const); - else if (OpenApiTypeChecker.isOneOf(input)) - input.oneOf.forEach(visitConstant); - }; - - visit(props.schema, props.accessor ?? "$input.schema"); visitConstant(props.schema); + visit(props.schema, props.accessor ?? "$input.schema"); if (reasons.length > 0) return { @@ -339,6 +350,7 @@ export namespace LlmSchemaComposer { }; else if (union.length === 0) return { + // unknown type success: true, value: { ...attribute, @@ -347,18 +359,28 @@ export namespace LlmSchemaComposer { }; else if (union.length === 1) return { + // single type success: true, value: { ...attribute, ...union[0], - description: union[0].description ?? attribute.description, + description: + props.config.strict === true && LlmTypeChecker.isReference(union[0]) + ? undefined + : (union[0].description ?? attribute.description), }, }; return { success: true, value: { ...attribute, - anyOf: union, + anyOf: union.map((u) => ({ + ...u, + description: + props.config.strict === true && LlmTypeChecker.isReference(u) + ? undefined + : u.description, + })), "x-discriminator": OpenApiTypeChecker.isOneOf(props.schema) && props.schema.discriminator !== undefined && @@ -782,14 +804,14 @@ export namespace LlmSchemaComposer { }), } satisfies OpenApi.IJsonSchema; }; -} -const getConfig = ( - config?: Partial | undefined, -): ILlmSchema.IConfig => ({ - reference: config?.reference ?? true, - strict: config?.strict ?? false, -}); + export const getConfig = ( + config?: Partial | undefined, + ): ILlmSchema.IConfig => ({ + reference: config?.reference ?? true, + strict: config?.strict ?? false, + }); +} const validateStrict = ( schema: OpenApi.IJsonSchema, diff --git a/test/src/examples/chatgpt-function-call-to-sale-create.ts b/test/src/examples/chatgpt-function-call-to-sale-create.ts index 9824d31f..f89e32b2 100644 --- a/test/src/examples/chatgpt-function-call-to-sale-create.ts +++ b/test/src/examples/chatgpt-function-call-to-sale-create.ts @@ -25,17 +25,15 @@ 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<"chatgpt"> = HttpLlm.application({ - model: "chatgpt", + const application: IHttpLlmApplication = HttpLlm.application({ document, }); // Let's imagine that LLM has selected a function to call - const func: IHttpLlmFunction<"chatgpt"> | undefined = - application.functions.find( - // (f) => f.name === "llm_selected_function_name" - (f) => f.path === "/shoppings/sellers/sale" && f.method === "post", - ); + const func: IHttpLlmFunction | undefined = application.functions.find( + // (f) => f.name === "llm_selected_function_name" + (f) => f.path === "/shoppings/sellers/sale" && f.method === "post", + ); if (func === undefined) throw new Error("No matched function exists."); // Get arguments by ChatGPT function calling diff --git a/test/src/examples/claude-function-call-separate-to-sale-create.ts.ts b/test/src/examples/claude-function-call-separate-to-sale-create.ts.ts index 8c0a7c2a..91cd6341 100644 --- a/test/src/examples/claude-function-call-separate-to-sale-create.ts.ts +++ b/test/src/examples/claude-function-call-separate-to-sale-create.ts.ts @@ -1,9 +1,9 @@ import Anthropic from "@anthropic-ai/sdk"; import { - ClaudeTypeChecker, HttpLlm, IHttpLlmApplication, IHttpLlmFunction, + LlmTypeChecker, OpenApi, OpenApiV3, OpenApiV3_1, @@ -26,23 +26,20 @@ 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<"claude"> = HttpLlm.application({ - model: "claude", + const application: IHttpLlmApplication = HttpLlm.application({ document, - options: { - reference: true, + config: { separate: (schema) => - ClaudeTypeChecker.isString(schema) && + LlmTypeChecker.isString(schema) && !!schema.contentMediaType?.startsWith("image"), }, }); // Let's imagine that LLM has selected a function to call - const func: IHttpLlmFunction<"claude"> | undefined = - application.functions.find( - // (f) => f.name === "llm_selected_fuction_name" - (f) => f.path === "/shoppings/sellers/sale" && f.method === "post", - ); + const func: IHttpLlmFunction | undefined = application.functions.find( + // (f) => f.name === "llm_selected_fuction_name" + (f) => f.path === "/shoppings/sellers/sale" && f.method === "post", + ); if (func === undefined) throw new Error("No matched function exists."); // Get arguments by ChatGPT function calling diff --git a/test/src/executable/execute.ts b/test/src/executable/execute.ts index 3b83f3b2..c99d1a98 100644 --- a/test/src/executable/execute.ts +++ b/test/src/executable/execute.ts @@ -23,16 +23,15 @@ 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<"3.0"> = HttpLlm.application({ - model: "3.0", + const application: IHttpLlmApplication = HttpLlm.application({ document, }); // Let's imagine that LLM has selected a function to call - const func: IHttpLlmFunction<"3.0"> | undefined = application.functions.find( + const func: IHttpLlmFunction | undefined = application.functions.find( (f) => f.path === "/bbs/articles" && f.method === "post", ); - typia.assertGuard>(func); + typia.assertGuard(func); // actual execution is by yourself const article = await HttpLlm.execute({ diff --git a/test/src/executable/sale.ts b/test/src/executable/sale.ts index 3c93b10e..9d33ca04 100644 --- a/test/src/executable/sale.ts +++ b/test/src/executable/sale.ts @@ -1,4 +1,3 @@ -import { ILlmSchema } from "@samchon/openapi"; import fs from "fs"; import typia from "typia"; @@ -8,32 +7,30 @@ import { LlmFunctionCaller } from "../utils/LlmFunctionCaller"; import { ShoppingSalePrompt } from "../utils/ShoppingSalePrompt"; import { StopWatch } from "../utils/StopWatch"; -const VENDORS: Array<[string, ILlmSchema.Model]> = [ - ["openai/gpt-4.1", "chatgpt"], - ["anthropic/claude-sonnet-4.5", "claude"], - ["deepseek/deepseek-v3.1-terminus:exacto", "claude"], - ["google/gemini-2.5-pro", "gemini"], - ["meta-llama/llama-3.3-70b-instruct", "claude"], - ["qwen/qwen3-next-80b-a3b-instruct", "claude"], +const VENDORS: string[] = [ + "openai/gpt-4.1", + "anthropic/claude-sonnet-4.5", + "deepseek/deepseek-v3.1-terminus:exacto", + "google/gemini-2.5-pro", + "meta-llama/llama-3.3-70b-instruct", + "qwen/qwen3-next-80b-a3b-instruct", ]; const main = async (): Promise => { for (const title of await ShoppingSalePrompt.documents()) - for (const [vendor, model] of VENDORS) + for (const vendor of VENDORS) try { const application = LlmApplicationFactory.convert({ - model, application: typia.json.application(), }); - await StopWatch.trace(`${title} - ${model}`)(async () => + await StopWatch.trace(`${title} - ${vendor}`)(async () => LlmFunctionCaller.test({ vendor, - model, function: application.functions[0] as any, texts: await ShoppingSalePrompt.texts(title), handleCompletion: async (input) => { await fs.promises.writeFile( - `${TestGlobal.ROOT}/examples/function-calling/arguments/${model}.${title}.input.json`, + `${TestGlobal.ROOT}/examples/function-calling/arguments/llm.${title}.input.json`, JSON.stringify(input, null, 2), "utf8", ); @@ -43,7 +40,7 @@ const main = async (): Promise => { } catch (error) { console.log(title, " -> Error"); await fs.promises.writeFile( - `${TestGlobal.ROOT}/examples/function-calling/arguments/${model}.${title}.error.json`, + `${TestGlobal.ROOT}/examples/function-calling/arguments/llm.${title}.error.json`, JSON.stringify( typia.is(error) ? { ...error } : error, null, diff --git a/test/src/features/issues/test_issue_104_upgrade_v20_allOf.ts b/test/src/features/issues/test_issue_104_upgrade_v20_allOf.ts index 12ba87f2..0da4231a 100644 --- a/test/src/features/issues/test_issue_104_upgrade_v20_allOf.ts +++ b/test/src/features/issues/test_issue_104_upgrade_v20_allOf.ts @@ -36,8 +36,7 @@ export const test_issue_104_upgrade_v20_allOf = async (): Promise => { ); } - const app: IHttpLlmApplication<"claude"> = HttpLlm.application({ - model: "claude", + const app: IHttpLlmApplication = HttpLlm.application({ document, }); TestValidator.equals("errors")(app.errors.length)(0); diff --git a/test/src/features/issues/test_issue_127_enum_description.ts b/test/src/features/issues/test_issue_127_enum_description.ts index cbe4947a..574e0de8 100644 --- a/test/src/features/issues/test_issue_127_enum_description.ts +++ b/test/src/features/issues/test_issue_127_enum_description.ts @@ -5,8 +5,8 @@ import typia, { IJsonSchemaCollection } from "typia"; export const test_issue_127_enum_description = (): void => { const collection: IJsonSchemaCollection = typia.json.schemas<[ISomething]>(); - const chatgpt = LlmSchemaComposer.parameters("chatgpt")({ - config: LlmSchemaComposer.defaultConfig("chatgpt"), + const chatgpt = LlmSchemaComposer.parameters({ + config: LlmSchemaComposer.getConfig(), components: collection.components, schema: collection.schemas[0] as OpenApi.IJsonSchema.IReference, }); @@ -17,8 +17,8 @@ export const test_issue_127_enum_description = (): void => { : "", )("The description."); - const gemini = LlmSchemaComposer.parameters("gemini")({ - config: LlmSchemaComposer.defaultConfig("gemini"), + const gemini = LlmSchemaComposer.parameters({ + config: LlmSchemaComposer.getConfig(), components: collection.components, schema: collection.schemas[0] as OpenApi.IJsonSchema.IReference, }); diff --git a/test/src/features/llm/application/validate_llm_applicationEquals.ts b/test/src/features/llm/application/test_llm_applicationEquals.ts similarity index 56% rename from test/src/features/llm/application/validate_llm_applicationEquals.ts rename to test/src/features/llm/application/test_llm_applicationEquals.ts index f384a128..126333ef 100644 --- a/test/src/features/llm/application/validate_llm_applicationEquals.ts +++ b/test/src/features/llm/application/test_llm_applicationEquals.ts @@ -3,39 +3,18 @@ import { HttpLlm, IHttpLlmApplication, IHttpLlmFunction, - ILlmSchema, IValidation, OpenApi, } from "@samchon/openapi"; -import { LlmSchemaComposer } from "@samchon/openapi/lib/composers/LlmSchemaComposer"; -export const test_chatgpt_applicationEquals = () => - validate_llm_applicationEquals("chatgpt"); - -export const test_claude_applicationEquals = () => - validate_llm_applicationEquals("claude"); - -export const test_gemini_applicationEquals = () => - validate_llm_applicationEquals("gemini"); - -export const test_llm_v30_applicationEquals = () => - validate_llm_applicationEquals("3.0"); - -export const test_llm_v31_applicationEquals = () => - validate_llm_applicationEquals("3.1"); - -const validate_llm_applicationEquals = ( - model: Model, -): void => { - const application: IHttpLlmApplication = HttpLlm.application({ - model, +export const test_llm_applicationEquals = (): void => { + const application: IHttpLlmApplication = HttpLlm.application({ document, - options: { - ...LlmSchemaComposer.defaultConfig(model), + config: { equals: true, - } as any, + }, }); - const func: IHttpLlmFunction = application.functions[0]; + const func: IHttpLlmFunction = application.functions[0]; const result: IValidation = func.validate({ body: { value: 1, diff --git a/test/src/features/llm/application/validate_llm_application_mismatch.ts b/test/src/features/llm/application/test_llm_application_mismatch.ts similarity index 70% rename from test/src/features/llm/application/validate_llm_application_mismatch.ts rename to test/src/features/llm/application/test_llm_application_mismatch.ts index 1ca01442..10be5373 100644 --- a/test/src/features/llm/application/validate_llm_application_mismatch.ts +++ b/test/src/features/llm/application/test_llm_application_mismatch.ts @@ -1,30 +1,8 @@ import { TestValidator } from "@nestia/e2e"; -import { - HttpLlm, - IHttpLlmApplication, - ILlmSchema, - OpenApi, -} from "@samchon/openapi"; +import { HttpLlm, IHttpLlmApplication, OpenApi } from "@samchon/openapi"; import typia, { IJsonSchemaCollection } from "typia"; -export const test_chatgpt_application_mismatch = (): void => - validate_llm_application_mismatch("chatgpt"); - -export const test_claude_application_mismatch = (): void => - validate_llm_application_mismatch("claude"); - -export const test_gemini_application_mismatch = (): void => - validate_llm_application_mismatch("gemini"); - -export const test_llm_v30_application_mismatch = (): void => - validate_llm_application_mismatch("3.0"); - -export const test_llm_v31_application_mismatch = (): void => - validate_llm_application_mismatch("3.1"); - -const validate_llm_application_mismatch = ( - model: Model, -): void => { +export const test_llm_application_mismatch = (): void => { const collection: IJsonSchemaCollection = typia.json.schemas<[IPoint, ICircle, IRectangle]>(); collection.schemas[0] = { $ref: "#/components/schemas/IPoint1" }; @@ -72,8 +50,7 @@ const validate_llm_application_mismatch = ( "x-samchon-emended-v4": true, }; - const app: IHttpLlmApplication = HttpLlm.application({ - model, + const app: IHttpLlmApplication = HttpLlm.application({ document, }); TestValidator.equals("#success")(app.functions.length)(0); diff --git a/test/src/features/llm/application/test_llm_application_separate.ts b/test/src/features/llm/application/test_llm_application_separate.ts new file mode 100644 index 00000000..440af47b --- /dev/null +++ b/test/src/features/llm/application/test_llm_application_separate.ts @@ -0,0 +1,35 @@ +import { TestValidator } from "@nestia/e2e"; +import { + HttpLlm, + IHttpLlmApplication, + LlmTypeChecker, + OpenApi, + OpenApiV3, + OpenApiV3_1, + SwaggerV2, +} from "@samchon/openapi"; +import { Singleton } from "tstl"; +import typia from "typia"; + +export const test_llm_application_separate = async (): Promise => { + const application: IHttpLlmApplication = HttpLlm.application({ + document: await document.get(), + config: { + separate: (schema) => + LlmTypeChecker.isString(schema as any) && + (schema as any)["x-wrtn-secret-key"] !== undefined, + }, + }); + for (const func of application.functions) + TestValidator.equals("separated")(!!func.separated)(true); +}; + +const document = new Singleton(async (): Promise => { + const swagger: + | SwaggerV2.IDocument + | OpenApiV3.IDocument + | OpenApiV3_1.IDocument = await fetch( + "https://wrtnlabs.github.io/connectors/swagger/swagger.json", + ).then((r) => r.json()); + return OpenApi.convert(typia.assert(swagger)); +}); diff --git a/test/src/features/llm/application/validate_llm_application_separateEquals.ts b/test/src/features/llm/application/test_llm_application_separateEquals.ts similarity index 57% rename from test/src/features/llm/application/validate_llm_application_separateEquals.ts rename to test/src/features/llm/application/test_llm_application_separateEquals.ts index b039957a..c8085e9a 100644 --- a/test/src/features/llm/application/validate_llm_application_separateEquals.ts +++ b/test/src/features/llm/application/test_llm_application_separateEquals.ts @@ -3,44 +3,21 @@ import { HttpLlm, IHttpLlmApplication, IHttpLlmFunction, - ILlmSchema, IValidation, OpenApi, OpenApiTypeChecker, } from "@samchon/openapi"; -import { LlmSchemaComposer } from "@samchon/openapi/lib/composers/LlmSchemaComposer"; -export const test_chatgpt_application_separateEquals = () => - validate_llm_application_separateEquals("chatgpt"); - -export const test_claude_application_separateEquals = () => - validate_llm_application_separateEquals("claude"); - -export const test_gemini_application_separateEquals = () => - validate_llm_application_separateEquals("gemini"); - -export const test_llm_v30_application_separateEquals = () => - validate_llm_application_separateEquals("3.0"); - -export const test_llm_v31_application_separateEquals = () => - validate_llm_application_separateEquals("3.1"); - -const validate_llm_application_separateEquals = < - Model extends ILlmSchema.Model, ->( - model: Model, -): void => { - const application: IHttpLlmApplication = HttpLlm.application({ - model, +export const test_llm_application_separateEquals = (): void => { + const application: IHttpLlmApplication = HttpLlm.application({ document, - options: { - ...LlmSchemaComposer.defaultConfig(model), + config: { equals: true, separate: (schema: OpenApi.IJsonSchema) => OpenApiTypeChecker.isNumber(schema), - } as any, + }, }); - const func: IHttpLlmFunction = application.functions[0]; + const func: IHttpLlmFunction = application.functions[0]; const result: IValidation = func.separated!.validate!({ body: { name: "John Doe", diff --git a/test/src/features/llm/application/validate_llm_application_tuple.ts b/test/src/features/llm/application/test_llm_application_tuple.ts similarity index 75% rename from test/src/features/llm/application/validate_llm_application_tuple.ts rename to test/src/features/llm/application/test_llm_application_tuple.ts index 08afdb15..3ae6ea10 100644 --- a/test/src/features/llm/application/validate_llm_application_tuple.ts +++ b/test/src/features/llm/application/test_llm_application_tuple.ts @@ -1,30 +1,8 @@ import { TestValidator } from "@nestia/e2e"; -import { - HttpLlm, - IHttpLlmApplication, - ILlmSchema, - OpenApi, -} from "@samchon/openapi"; +import { HttpLlm, IHttpLlmApplication, OpenApi } from "@samchon/openapi"; import typia, { IJsonSchemaCollection } from "typia"; -export const test_chatgpt_application_mismatch = (): void => - validate_llm_application_tuple("chatgpt"); - -export const test_claude_application_mismatch = (): void => - validate_llm_application_tuple("claude"); - -export const test_gemini_application_mismatch = (): void => - validate_llm_application_tuple("gemini"); - -export const test_llm_v30_application_mismatch = (): void => - validate_llm_application_tuple("3.0"); - -export const test_llm_v31_application_mismatch = (): void => - validate_llm_application_tuple("3.1"); - -const validate_llm_application_tuple = ( - model: Model, -): void => { +export const test_llm_application_tuple = (): void => { const collection: IJsonSchemaCollection = typia.json.schemas< [ [number, number], @@ -97,8 +75,7 @@ const validate_llm_application_tuple = ( }, "x-samchon-emended-v4": true, }; - const app: IHttpLlmApplication = HttpLlm.application({ - model, + const app: IHttpLlmApplication = HttpLlm.application({ document, }); diff --git a/test/src/features/llm/application/test_llm_application_type.ts b/test/src/features/llm/application/test_llm_application_type.ts new file mode 100644 index 00000000..90f00216 --- /dev/null +++ b/test/src/features/llm/application/test_llm_application_type.ts @@ -0,0 +1,30 @@ +import { + HttpLlm, + IHttpLlmApplication, + ILlmApplication, + OpenApi, +} from "@samchon/openapi"; +import fs from "fs"; +import { Singleton } from "tstl"; +import typia from "typia"; + +import { TestGlobal } from "../../../TestGlobal"; + +export const test_llm_application_type = (): void => { + const http: IHttpLlmApplication = application(); + const classic: Omit = http; + typia.assert(classic); +}; + +const application = () => + HttpLlm.application({ + document: document.get(), + }); + +const document = new Singleton(() => + OpenApi.convert( + JSON.parse( + fs.readFileSync(`${TestGlobal.ROOT}/examples/v3.1/shopping.json`, "utf8"), + ), + ), +); diff --git a/test/src/features/llm/application/validate_llm_application_separate.ts b/test/src/features/llm/application/validate_llm_application_separate.ts deleted file mode 100644 index bb477c90..00000000 --- a/test/src/features/llm/application/validate_llm_application_separate.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { TestValidator } from "@nestia/e2e"; -import { - HttpLlm, - IHttpLlmApplication, - ILlmSchema, - OpenApi, - OpenApiV3, - OpenApiV3_1, - SwaggerV2, -} from "@samchon/openapi"; -import { LlmSchemaComposer } from "@samchon/openapi/lib/composers/LlmSchemaComposer"; -import { Singleton } from "tstl"; -import typia from "typia"; - -export const test_chatgpt_application_separate = async (): Promise => { - await validate_llm_application_separate("chatgpt", false); - await validate_llm_application_separate("chatgpt", true); -}; - -export const test_claude_application_separate = async (): Promise => { - await validate_llm_application_separate("claude", false); - await validate_llm_application_separate("claude", true); -}; - -export const test_gemini_application_separate = async (): Promise => { - await validate_llm_application_separate("gemini", false); -}; - -export const test_llm_v30_application_separate = async (): Promise => { - await validate_llm_application_separate("3.0", false); - await validate_llm_application_separate("3.0", true); -}; - -export const test_llm_v31_application_separate = async (): Promise => { - await validate_llm_application_separate("3.1", false); - await validate_llm_application_separate("3.1", true); -}; - -const validate_llm_application_separate = async < - Model extends ILlmSchema.Model, ->( - model: Model, - constraint: boolean, -): Promise => { - const application: IHttpLlmApplication = HttpLlm.application({ - model, - document: await document.get(), - options: { - separate: (schema: any) => - LlmSchemaComposer.typeChecker(model).isString(schema as any) && - (schema as any)["x-wrtn-secret-key"] !== undefined, - constraint: constraint as any, - } as any, - }); - for (const func of application.functions) - TestValidator.equals("separated")(!!func.separated)(true); -}; - -const document = new Singleton(async (): Promise => { - const swagger: - | SwaggerV2.IDocument - | OpenApiV3.IDocument - | OpenApiV3_1.IDocument = await fetch( - "https://wrtnlabs.github.io/connectors/swagger/swagger.json", - ).then((r) => r.json()); - return OpenApi.convert(typia.assert(swagger)); -}); diff --git a/test/src/features/llm/application/validate_llm_application_type.ts b/test/src/features/llm/application/validate_llm_application_type.ts deleted file mode 100644 index 7a9e7d41..00000000 --- a/test/src/features/llm/application/validate_llm_application_type.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { - HttpLlm, - IHttpLlmApplication, - ILlmApplication, - ILlmSchema, - OpenApi, -} from "@samchon/openapi"; -import fs from "fs"; -import { Singleton } from "tstl"; -import typia from "typia"; - -import { TestGlobal } from "../../../TestGlobal"; - -export const test_chatgpt_application_type = (): void => { - const http: IHttpLlmApplication<"chatgpt"> = application("chatgpt"); - const classic: ILlmApplication<"chatgpt"> = http; - typia.assert(classic); -}; - -export const test_claude_application_type = (): void => { - const http: IHttpLlmApplication<"claude"> = application("claude"); - const classic: ILlmApplication<"claude"> = http; - typia.assert(classic); -}; - -export const test_llm_v30_application_type = (): void => { - const http: IHttpLlmApplication<"3.0"> = application("3.0"); - const classic: ILlmApplication<"3.0"> = http; - typia.assert(classic); -}; - -export const test_llm_v31_application_type = (): void => { - const http: IHttpLlmApplication<"3.1"> = application("3.1"); - const classic: ILlmApplication<"3.1"> = http; - typia.assert(classic); -}; - -const application = (model: Model) => - HttpLlm.application({ - model, - document: document.get(), - }); - -const document = new Singleton(() => - OpenApi.convert( - JSON.parse( - fs.readFileSync(`${TestGlobal.ROOT}/examples/v3.1/shopping.json`, "utf8"), - ), - ), -); diff --git a/test/src/features/llm/chatgpt/test_chatgpt_schema_additionalProperties.ts b/test/src/features/llm/chatgpt/test_chatgpt_schema_additionalProperties.ts deleted file mode 100644 index 547e9b70..00000000 --- a/test/src/features/llm/chatgpt/test_chatgpt_schema_additionalProperties.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { TestValidator } from "@nestia/e2e"; -import { ChatGptSchemaComposer } from "@samchon/openapi/lib/composers/llm/ChatGptSchemaComposer"; -import typia from "typia"; - -export const test_chatgpt_schema_additionalProperties = (): void => { - interface IMember { - name: string; - age: number; - hobby: Record; - } - const collection = typia.json.schemas<[IMember]>(); - for (const strict of [false, true]) { - const result = ChatGptSchemaComposer.schema({ - config: { - reference: false, - strict, - }, - $defs: {}, - components: collection.components, - schema: collection.schemas[0], - }); - TestValidator.equals("success")(result.success)(!strict); - } -}; diff --git a/test/src/features/llm/chatgpt/test_chatgpt_schema_optional.ts b/test/src/features/llm/chatgpt/test_chatgpt_schema_optional.ts deleted file mode 100644 index dec27f0a..00000000 --- a/test/src/features/llm/chatgpt/test_chatgpt_schema_optional.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { TestValidator } from "@nestia/e2e"; -import { ChatGptSchemaComposer } from "@samchon/openapi/lib/composers/llm/ChatGptSchemaComposer"; -import typia from "typia"; - -export const test_chatgpt_schema_optional = (): void => { - interface IMember { - name: string; - age: number; - hobby?: string; - } - const collection = typia.json.schemas<[IMember]>(); - for (const strict of [false, true]) { - const result = ChatGptSchemaComposer.schema({ - config: { - reference: false, - strict, - }, - $defs: {}, - components: collection.components, - schema: collection.schemas[0], - }); - TestValidator.equals("success")(result.success)(!strict); - } -}; diff --git a/test/src/features/llm/function_calling/validate_llm_function_calling_additionalProperties.ts b/test/src/features/llm/function_calling/test_llm_function_calling_additionalProperties.ts similarity index 55% rename from test/src/features/llm/function_calling/validate_llm_function_calling_additionalProperties.ts rename to test/src/features/llm/function_calling/test_llm_function_calling_additionalProperties.ts index af5d99a0..072a9685 100644 --- a/test/src/features/llm/function_calling/validate_llm_function_calling_additionalProperties.ts +++ b/test/src/features/llm/function_calling/test_llm_function_calling_additionalProperties.ts @@ -1,4 +1,4 @@ -import { ILlmApplication, ILlmSchema } from "@samchon/openapi"; +import { ILlmApplication } from "@samchon/openapi"; import fs from "fs"; import typia, { tags } from "typia"; @@ -6,56 +6,19 @@ import { TestGlobal } from "../../../TestGlobal"; import { LlmApplicationFactory } from "../../../utils/LlmApplicationFactory"; import { LlmFunctionCaller } from "../../../utils/LlmFunctionCaller"; -export const test_chatgpt_function_calling_additionalProperties = () => - validate_llm_function_calling_additionalProperties({ - vendor: "openai/gpt-4.1", - model: "chatgpt", - }); - -export const test_claude_function_calling_additionalProperties = () => +export const test_llm_function_calling_additionalProperties = () => validate_llm_function_calling_additionalProperties({ vendor: "anthropic/claude-sonnet-4.5", - model: "claude", - }); - -export const test_deepseek_function_calling_additionalProperties = () => - validate_llm_function_calling_additionalProperties({ - vendor: "deepseek/deepseek-v3.1-terminus:exacto", - model: "claude", - }); - -export const test_gemini_function_calling_additionalProperties = () => - validate_llm_function_calling_additionalProperties({ - vendor: "google/gemini-2.5-pro", - model: "gemini", - }); - -export const test_llama_function_calling_additionalProperties = () => - validate_llm_function_calling_additionalProperties({ - vendor: "meta-llama/llama-3.3-70b-instruct", - model: "claude", }); -export const test_qwen_function_calling_additionalProperties = () => - validate_llm_function_calling_additionalProperties({ - vendor: "qwen/qwen3-next-80b-a3b-instruct", - model: "claude", - }); - -const validate_llm_function_calling_additionalProperties = async < - Model extends ILlmSchema.Model, ->(props: { +const validate_llm_function_calling_additionalProperties = async (props: { vendor: string; - model: Model; }) => { - const application: ILlmApplication = - LlmApplicationFactory.convert({ - model: props.model, - application: typia.json.application(), - }); + const application: ILlmApplication = LlmApplicationFactory.convert({ + application: typia.json.application(), + }); return await LlmFunctionCaller.test({ vendor: props.vendor, - model: props.model, function: application.functions[0], texts: [ { @@ -70,7 +33,7 @@ const validate_llm_function_calling_additionalProperties = async < handleParameters: async (parameters) => { if (process.argv.includes("--file")) await fs.promises.writeFile( - `${TestGlobal.ROOT}/examples/function-calling/schemas/chatgpt.additionalProperties.schema.json`, + `${TestGlobal.ROOT}/examples/function-calling/schemas/llm.additionalProperties.schema.json`, JSON.stringify(parameters, null, 2), "utf8", ); @@ -78,7 +41,7 @@ const validate_llm_function_calling_additionalProperties = async < handleCompletion: async (input) => { if (process.argv.includes("--file")) await fs.promises.writeFile( - `${TestGlobal.ROOT}/examples/function-calling/arguments/${props.model}.additionalProperties.input.json`, + `${TestGlobal.ROOT}/examples/function-calling/arguments/llm.additionalProperties.input.json`, JSON.stringify(input, null, 2), "utf8", ); diff --git a/test/src/features/llm/function_calling/validate_llm_function_calling_default.ts b/test/src/features/llm/function_calling/test_llm_function_calling_default.ts similarity index 54% rename from test/src/features/llm/function_calling/validate_llm_function_calling_default.ts rename to test/src/features/llm/function_calling/test_llm_function_calling_default.ts index d9f7bc9f..8a17c967 100644 --- a/test/src/features/llm/function_calling/validate_llm_function_calling_default.ts +++ b/test/src/features/llm/function_calling/test_llm_function_calling_default.ts @@ -1,4 +1,4 @@ -import { ILlmApplication, ILlmSchema } from "@samchon/openapi"; +import { ILlmApplication } from "@samchon/openapi"; import fs from "fs"; import typia, { tags } from "typia"; @@ -6,56 +6,19 @@ import { TestGlobal } from "../../../TestGlobal"; import { LlmApplicationFactory } from "../../../utils/LlmApplicationFactory"; import { LlmFunctionCaller } from "../../../utils/LlmFunctionCaller"; -export const test_chatgpt_function_calling_default = () => - validate_llm_function_calling_default({ - vendor: "openai/gpt-4.1", - model: "chatgpt", - }); - -export const test_claude_function_calling_default = () => +export const test_llm_function_calling_default = () => validate_llm_function_calling_default({ vendor: "anthropic/claude-sonnet-4.5", - model: "claude", - }); - -export const test_deepseek_function_calling_default = () => - validate_llm_function_calling_default({ - vendor: "deepseek/deepseek-v3.1-terminus:exacto", - model: "claude", - }); - -export const test_gemini_function_calling_default = () => - validate_llm_function_calling_default({ - vendor: "google/gemini-2.5-pro", - model: "gemini", - }); - -export const test_llama_function_calling_default = () => - validate_llm_function_calling_default({ - vendor: "meta-llama/llama-3.3-70b-instruct", - model: "claude", }); -export const test_qwen_function_calling_default = () => - validate_llm_function_calling_default({ - vendor: "qwen/qwen3-next-80b-a3b-instruct", - model: "claude", - }); - -const validate_llm_function_calling_default = async < - Model extends ILlmSchema.Model, ->(props: { +const validate_llm_function_calling_default = async (props: { vendor: string; - model: Model; }) => { - const application: ILlmApplication = - LlmApplicationFactory.convert({ - model: props.model, - application: typia.json.application(), - }); + const application: ILlmApplication = LlmApplicationFactory.convert({ + application: typia.json.application(), + }); return await LlmFunctionCaller.test({ vendor: props.vendor, - model: props.model, function: application.functions[0], texts: [ { @@ -70,7 +33,7 @@ const validate_llm_function_calling_default = async < handleParameters: async (parameters) => { if (process.argv.includes("--file")) await fs.promises.writeFile( - `${TestGlobal.ROOT}/examples/function-calling/schemas/${props.model}.default.schema.json`, + `${TestGlobal.ROOT}/examples/function-calling/schemas/llm.default.schema.json`, JSON.stringify(parameters, null, 2), "utf8", ); @@ -78,7 +41,7 @@ const validate_llm_function_calling_default = async < handleCompletion: async (input) => { if (process.argv.includes("--file")) await fs.promises.writeFile( - `${TestGlobal.ROOT}/examples/function-calling/arguments/${props.model}.default.input.json`, + `${TestGlobal.ROOT}/examples/function-calling/arguments/llm.default.input.json`, JSON.stringify(input, null, 2), "utf8", ); diff --git a/test/src/features/llm/function_calling/validate_llm_function_calling_example.ts b/test/src/features/llm/function_calling/test_llm_function_calling_example.ts similarity index 57% rename from test/src/features/llm/function_calling/validate_llm_function_calling_example.ts rename to test/src/features/llm/function_calling/test_llm_function_calling_example.ts index 3d799ba6..6d7dcdc2 100644 --- a/test/src/features/llm/function_calling/validate_llm_function_calling_example.ts +++ b/test/src/features/llm/function_calling/test_llm_function_calling_example.ts @@ -1,4 +1,4 @@ -import { ILlmApplication, ILlmSchema } from "@samchon/openapi"; +import { ILlmApplication } from "@samchon/openapi"; import fs from "fs"; import typia, { tags } from "typia"; @@ -6,56 +6,19 @@ import { TestGlobal } from "../../../TestGlobal"; import { LlmApplicationFactory } from "../../../utils/LlmApplicationFactory"; import { LlmFunctionCaller } from "../../../utils/LlmFunctionCaller"; -export const test_chatgpt_function_calling_example = () => - validate_llm_function_calling_example({ - vendor: "openai/gpt-4.1", - model: "chatgpt", - }); - -export const test_claude_function_calling_example = () => +export const test_llm_function_calling_example = () => validate_llm_function_calling_example({ vendor: "anthropic/claude-sonnet-4.5", - model: "claude", - }); - -export const test_deepseek_function_calling_example = () => - validate_llm_function_calling_example({ - vendor: "deepseek/deepseek-v3.1-terminus:exacto", - model: "claude", - }); - -export const test_gemini_function_calling_example = () => - validate_llm_function_calling_example({ - vendor: "google/gemini-2.5-pro", - model: "gemini", - }); - -export const test_llama_function_calling_example = () => - validate_llm_function_calling_example({ - vendor: "meta-llama/llama-3.3-70b-instruct", - model: "claude", }); -export const test_qwen_function_calling_example = () => - validate_llm_function_calling_example({ - vendor: "qwen/qwen3-next-80b-a3b-instruct", - model: "claude", - }); - -const validate_llm_function_calling_example = async < - Model extends ILlmSchema.Model, ->(props: { +const validate_llm_function_calling_example = async (props: { vendor: string; - model: Model; }) => { - const application: ILlmApplication = - LlmApplicationFactory.convert({ - model: props.model, - application: typia.json.application(), - }); + const application: ILlmApplication = LlmApplicationFactory.convert({ + application: typia.json.application(), + }); return await LlmFunctionCaller.test({ vendor: props.vendor, - model: props.model, function: application.functions[0], texts: [ { @@ -70,7 +33,7 @@ const validate_llm_function_calling_example = async < handleParameters: async (parameters) => { if (process.argv.includes("--file")) await fs.promises.writeFile( - `${TestGlobal.ROOT}/examples/function-calling/schemas/${props.model}.example.schema.json`, + `${TestGlobal.ROOT}/examples/function-calling/schemas/llm.example.schema.json`, JSON.stringify(parameters, null, 2), "utf8", ); @@ -78,7 +41,7 @@ const validate_llm_function_calling_example = async < handleCompletion: async (input) => { if (process.argv.includes("--file")) await fs.promises.writeFile( - `${TestGlobal.ROOT}/examples/function-calling/arguments/${props.model}.example.input.json`, + `${TestGlobal.ROOT}/examples/function-calling/arguments/llm.example.input.json`, JSON.stringify(input, null, 2), "utf8", ); diff --git a/test/src/features/llm/function_calling/validate_llm_function_calling_optional.ts b/test/src/features/llm/function_calling/test_llm_function_calling_optional.ts similarity index 52% rename from test/src/features/llm/function_calling/validate_llm_function_calling_optional.ts rename to test/src/features/llm/function_calling/test_llm_function_calling_optional.ts index a1fdddf1..53ae7cc9 100644 --- a/test/src/features/llm/function_calling/validate_llm_function_calling_optional.ts +++ b/test/src/features/llm/function_calling/test_llm_function_calling_optional.ts @@ -1,4 +1,4 @@ -import { ILlmApplication, ILlmSchema } from "@samchon/openapi"; +import { ILlmApplication } from "@samchon/openapi"; import fs from "fs"; import typia from "typia"; @@ -6,56 +6,19 @@ import { TestGlobal } from "../../../TestGlobal"; import { LlmApplicationFactory } from "../../../utils/LlmApplicationFactory"; import { LlmFunctionCaller } from "../../../utils/LlmFunctionCaller"; -export const test_chatgpt_function_calling_optional = () => - validate_chatgpt_function_calling_optional({ - vendor: "openai/gpt-4.1", - model: "chatgpt", - }); - -export const test_claude_function_calling_optional = () => - validate_chatgpt_function_calling_optional({ +export const test_llm_function_calling_optional = () => + validate_llm_function_calling_optional({ vendor: "anthropic/claude-sonnet-4.5", - model: "claude", - }); - -export const test_deepseek_function_calling_optional = () => - validate_chatgpt_function_calling_optional({ - vendor: "deepseek/deepseek-v3.1-terminus:exacto", - model: "claude", - }); - -export const test_gemini_function_calling_optional = () => - validate_chatgpt_function_calling_optional({ - vendor: "google/gemini-2.5-pro", - model: "gemini", }); -export const test_llama_function_calling_optional = () => - validate_chatgpt_function_calling_optional({ - vendor: "meta-llama/llama-3.3-70b-instruct", - model: "claude", - }); - -export const test_qwen_function_calling_optional = () => - validate_chatgpt_function_calling_optional({ - vendor: "qwen/qwen3-next-80b-a3b-instruct", - model: "claude", - }); - -const validate_chatgpt_function_calling_optional = async < - Model extends ILlmSchema.Model, ->(props: { +const validate_llm_function_calling_optional = async (props: { vendor: string; - model: Model; }) => { - const application: ILlmApplication = - LlmApplicationFactory.convert({ - model: props.model, - application: typia.json.application(), - }); + const application: ILlmApplication = LlmApplicationFactory.convert({ + application: typia.json.application(), + }); return await LlmFunctionCaller.test({ vendor: props.vendor, - model: props.model, function: application.functions[0], texts: [ { @@ -70,7 +33,7 @@ const validate_chatgpt_function_calling_optional = async < handleParameters: async (parameters) => { if (process.argv.includes("--file")) await fs.promises.writeFile( - `${TestGlobal.ROOT}/examples/function-calling/schemas/${props.model}.optional.schema.json`, + `${TestGlobal.ROOT}/examples/function-calling/schemas/llm.optional.schema.json`, JSON.stringify(parameters, null, 2), "utf8", ); @@ -78,7 +41,7 @@ const validate_chatgpt_function_calling_optional = async < handleCompletion: async (input) => { if (process.argv.includes("--file")) await fs.promises.writeFile( - `${TestGlobal.ROOT}/examples/function-calling/arguments/${props.model}.optional.input.json`, + `${TestGlobal.ROOT}/examples/function-calling/arguments/llm.optional.input.json`, JSON.stringify(input, null, 2), "utf8", ); diff --git a/test/src/features/llm/function_calling/test_llm_function_calling_readonly.ts b/test/src/features/llm/function_calling/test_llm_function_calling_readonly.ts new file mode 100644 index 00000000..f2d2da0e --- /dev/null +++ b/test/src/features/llm/function_calling/test_llm_function_calling_readonly.ts @@ -0,0 +1,59 @@ +import typia, { tags } from "typia"; +import { v4 } from "uuid"; + +import { ILlmApplication } from "../../../../../lib"; +import { LlmApplicationFactory } from "../../../utils/LlmApplicationFactory"; +import { LlmFunctionCaller } from "../../../utils/LlmFunctionCaller"; + +export const test_llm_function_calling_readonly = () => + validate_llm_function_calling_readonly({ + vendor: "anthropic/claude-sonnet-4.5", + }); + +const validate_llm_function_calling_readonly = async (props: { + vendor: string; +}) => { + const application: ILlmApplication = LlmApplicationFactory.convert({ + application: typia.json.application(), + }); + for (const p of [ + application.functions[0].parameters.properties.id, + application.functions[0].parameters.properties.created_at, + ]) + (p as any).readOnly = true; + return await LlmFunctionCaller.test({ + vendor: props.vendor, + function: application.functions[0], + texts: [ + { + role: "assistant", + content: SYSTEM_MESSAGE, + }, + { + role: "user", + content: USER_MESSAGE, + }, + ], + handleParameters: async () => {}, + handleCompletion: async () => {}, + }); +}; + +interface IApplication { + participate(member: IMember): void; +} +interface IMember { + readonly id: string & tags.Format<"uuid">; + email: string & tags.Format<"email">; + name: string; + readonly created_at: string & tags.Format<"date-time">; +} + +const SYSTEM_MESSAGE = `You are a helpful assistant for function calling.`; +const USER_MESSAGE = ` + A new member wants to participate. + + The member's id is "${v4()}", and the account's email is "john@doe.com". + The account has been created at "2023-01-01T00:00:00.000Z" + and the member's name is "John Doe". +`; diff --git a/test/src/features/llm/function_calling/validate_llm_function_calling_recursive.ts b/test/src/features/llm/function_calling/test_llm_function_calling_recursive.ts similarity index 65% rename from test/src/features/llm/function_calling/validate_llm_function_calling_recursive.ts rename to test/src/features/llm/function_calling/test_llm_function_calling_recursive.ts index adf9cbc6..599706d3 100644 --- a/test/src/features/llm/function_calling/validate_llm_function_calling_recursive.ts +++ b/test/src/features/llm/function_calling/test_llm_function_calling_recursive.ts @@ -1,4 +1,4 @@ -import { ILlmApplication, ILlmSchema } from "@samchon/openapi"; +import { ILlmApplication } from "@samchon/openapi"; import fs from "fs"; import typia, { tags } from "typia"; @@ -6,50 +6,19 @@ import { TestGlobal } from "../../../TestGlobal"; import { LlmApplicationFactory } from "../../../utils/LlmApplicationFactory"; import { LlmFunctionCaller } from "../../../utils/LlmFunctionCaller"; -export const test_chatgpt_function_calling_recursive = () => - validate_llm_function_calling_recursive({ - vendor: "openai/gpt-4.1", - model: "chatgpt", - }); - -export const test_claude_function_calling_recursive = () => +export const test_llm_function_calling_recursive = () => validate_llm_function_calling_recursive({ vendor: "anthropic/claude-sonnet-4.5", - model: "claude", }); -export const test_deepseek_function_calling_recursive = () => - validate_llm_function_calling_recursive({ - vendor: "deepseek/deepseek-v3.1-terminus:exacto", - model: "claude", - }); - -export const test_gemini_function_calling_recursive = () => - validate_llm_function_calling_recursive({ - vendor: "google/gemini-2.5-pro", - model: "gemini", - }); - -export const test_llama_function_calling_recursive = () => - validate_llm_function_calling_recursive({ - vendor: "meta-llama/llama-3.3-70b-instruct", - model: "claude", - }); - -const validate_llm_function_calling_recursive = async < - Model extends ILlmSchema.Model, ->(props: { +const validate_llm_function_calling_recursive = async (props: { vendor: string; - model: Model; }) => { - const application: ILlmApplication = - LlmApplicationFactory.convert({ - model: props.model, - application: typia.json.application(), - }); + const application: ILlmApplication = LlmApplicationFactory.convert({ + application: typia.json.application(), + }); return await LlmFunctionCaller.test({ vendor: props.vendor, - model: props.model, function: application.functions[0], texts: [ { @@ -64,7 +33,7 @@ const validate_llm_function_calling_recursive = async < handleParameters: async (parameters) => { if (process.argv.includes("--file")) await fs.promises.writeFile( - `${TestGlobal.ROOT}/examples/function-calling/schemas/${props.model}.recursive.schema.json`, + `${TestGlobal.ROOT}/examples/function-calling/schemas/llm.recursive.schema.json`, JSON.stringify(parameters, null, 2), "utf8", ); @@ -72,7 +41,7 @@ const validate_llm_function_calling_recursive = async < handleCompletion: async (input) => { if (process.argv.includes("--file")) await fs.promises.writeFile( - `${TestGlobal.ROOT}/examples/function-calling/arguments/${props.model}.recursive.input.json`, + `${TestGlobal.ROOT}/examples/function-calling/arguments/llm.recursive.input.json`, JSON.stringify(input, null, 2), "utf8", ); diff --git a/test/src/features/llm/function_calling/test_llm_function_calling_sale.ts b/test/src/features/llm/function_calling/test_llm_function_calling_sale.ts new file mode 100644 index 00000000..0695053a --- /dev/null +++ b/test/src/features/llm/function_calling/test_llm_function_calling_sale.ts @@ -0,0 +1,42 @@ +import { ILlmApplication } from "@samchon/openapi"; +import fs from "fs"; +import typia from "typia"; + +import { TestGlobal } from "../../../TestGlobal"; +import { LlmApplicationFactory } from "../../../utils/LlmApplicationFactory"; +import { LlmFunctionCaller } from "../../../utils/LlmFunctionCaller"; +import { ShoppingSalePrompt } from "../../../utils/ShoppingSalePrompt"; + +export const test_llm_function_calling_sale = () => + validate_llm_function_calling_sale({ + vendor: "anthropic/claude-sonnet-4.5", + }); + +const validate_llm_function_calling_sale = async (props: { + vendor: string; +}) => { + const application: ILlmApplication = LlmApplicationFactory.convert({ + application: typia.json.application(), + }); + return await LlmFunctionCaller.test({ + vendor: props.vendor, + function: application.functions[0], + texts: await ShoppingSalePrompt.texts(), + handleParameters: async (parameters) => { + if (process.argv.includes("--file")) + fs.promises.writeFile( + `${TestGlobal.ROOT}/examples/function-calling/schemas/llm.sale.schema.json`, + JSON.stringify(parameters, null, 2), + "utf8", + ); + }, + handleCompletion: async (input) => { + if (process.argv.includes("--file")) + await fs.promises.writeFile( + `${TestGlobal.ROOT}/examples/function-calling/arguments/llm.sale.input.json`, + JSON.stringify(input, null, 2), + "utf8", + ); + }, + }); +}; diff --git a/test/src/features/llm/function_calling/validate_llm_function_calling_tags.ts b/test/src/features/llm/function_calling/test_llm_function_calling_tags.ts similarity index 62% rename from test/src/features/llm/function_calling/validate_llm_function_calling_tags.ts rename to test/src/features/llm/function_calling/test_llm_function_calling_tags.ts index ee5d96d0..0721e127 100644 --- a/test/src/features/llm/function_calling/validate_llm_function_calling_tags.ts +++ b/test/src/features/llm/function_calling/test_llm_function_calling_tags.ts @@ -1,4 +1,4 @@ -import { ILlmApplication, ILlmSchema } from "@samchon/openapi"; +import { ILlmApplication } from "@samchon/openapi"; import fs from "fs"; import typia, { tags } from "typia"; @@ -6,44 +6,19 @@ import { TestGlobal } from "../../../TestGlobal"; import { LlmApplicationFactory } from "../../../utils/LlmApplicationFactory"; import { LlmFunctionCaller } from "../../../utils/LlmFunctionCaller"; -export const test_chatgpt_function_calling_tags = () => - validate_llm_function_calling_tags({ - vendor: "openai/gpt-4.1", - model: "chatgpt", - }); - -export const test_claude_function_calling_tags = () => +export const test_llm_function_calling_tags = () => validate_llm_function_calling_tags({ vendor: "anthropic/claude-sonnet-4.5", - model: "claude", - }); - -export const test_gemini_function_calling_tags = () => - validate_llm_function_calling_tags({ - vendor: "google/gemini-2.5-pro", - model: "gemini", }); -export const test_llama_function_calling_tags = () => - validate_llm_function_calling_tags({ - vendor: "meta-llama/llama-3.3-70b-instruct", - model: "claude", - }); - -const validate_llm_function_calling_tags = async < - Model extends ILlmSchema.Model, ->(props: { +const validate_llm_function_calling_tags = async (props: { vendor: string; - model: Model; }) => { - const application: ILlmApplication = - LlmApplicationFactory.convert({ - model: props.model, - application: typia.json.application(), - }); + const application: ILlmApplication = LlmApplicationFactory.convert({ + application: typia.json.application(), + }); return await LlmFunctionCaller.test({ vendor: props.vendor, - model: props.model, function: application.functions[0], texts: [ { @@ -58,7 +33,7 @@ const validate_llm_function_calling_tags = async < handleParameters: async (parameters) => { if (process.argv.includes("--file")) await fs.promises.writeFile( - `${TestGlobal.ROOT}/examples/function-calling/schemas/${props.model}.tags.schema.json`, + `${TestGlobal.ROOT}/examples/function-calling/schemas/llm.tags.schema.json`, JSON.stringify(parameters, null, 2), "utf8", ); @@ -66,7 +41,7 @@ const validate_llm_function_calling_tags = async < handleCompletion: async (input) => { if (process.argv.includes("--file")) await fs.promises.writeFile( - `${TestGlobal.ROOT}/examples/function-calling/arguments/${props.model}.tags.input.json`, + `${TestGlobal.ROOT}/examples/function-calling/arguments/llm.tags.input.json`, JSON.stringify(input, null, 2), "utf8", ); diff --git a/test/src/features/llm/function_calling/validate_llm_function_calling_union.ts b/test/src/features/llm/function_calling/test_llm_function_calling_union.ts similarity index 58% rename from test/src/features/llm/function_calling/validate_llm_function_calling_union.ts rename to test/src/features/llm/function_calling/test_llm_function_calling_union.ts index 745e6202..e0920b46 100644 --- a/test/src/features/llm/function_calling/validate_llm_function_calling_union.ts +++ b/test/src/features/llm/function_calling/test_llm_function_calling_union.ts @@ -1,4 +1,4 @@ -import { ILlmApplication, ILlmSchema } from "@samchon/openapi"; +import { ILlmApplication } from "@samchon/openapi"; import fs from "fs"; import typia from "typia"; @@ -6,56 +6,19 @@ import { TestGlobal } from "../../../TestGlobal"; import { LlmApplicationFactory } from "../../../utils/LlmApplicationFactory"; import { LlmFunctionCaller } from "../../../utils/LlmFunctionCaller"; -export const test_chatgpt_function_calling_union = () => - validate_llm_function_calling_union({ - vendor: "openai/gpt-4.1", - model: "chatgpt", - }); - -export const test_claude_function_calling_union = () => +export const test_llm_function_calling_union = () => validate_llm_function_calling_union({ vendor: "anthropic/claude-sonnet-4.5", - model: "claude", - }); - -export const test_deepseek_function_calling_union = () => - validate_llm_function_calling_union({ - vendor: "deepseek/deepseek-v3.1-terminus:exacto", - model: "claude", - }); - -export const test_gemini_function_calling_union = () => - validate_llm_function_calling_union({ - vendor: "google/gemini-2.5-pro", - model: "gemini", - }); - -export const test_llama_function_calling_union = () => - validate_llm_function_calling_union({ - vendor: "meta-llama/llama-3.3-70b-instruct", - model: "claude", }); -export const test_qwen_function_calling_union = () => - validate_llm_function_calling_union({ - vendor: "qwen/qwen3-next-80b-a3b-instruct", - model: "claude", - }); - -const validate_llm_function_calling_union = async < - Model extends ILlmSchema.Model, ->(props: { +const validate_llm_function_calling_union = async (props: { vendor: string; - model: Model; }) => { - const application: ILlmApplication = - LlmApplicationFactory.convert({ - model: props.model, - application: typia.json.application(), - }); + const application: ILlmApplication = LlmApplicationFactory.convert({ + application: typia.json.application(), + }); return await LlmFunctionCaller.test({ vendor: props.vendor, - model: props.model, function: application.functions[0], texts: [ { @@ -70,7 +33,7 @@ const validate_llm_function_calling_union = async < handleParameters: async (parameters) => { if (process.argv.includes("--file")) await fs.promises.writeFile( - `${TestGlobal.ROOT}/examples/function-calling/schemas/${props.model}.union.schema.json`, + `${TestGlobal.ROOT}/examples/function-calling/schemas/llm.union.schema.json`, JSON.stringify(parameters, null, 2), "utf8", ); @@ -78,7 +41,7 @@ const validate_llm_function_calling_union = async < handleCompletion: async (input) => { if (process.argv.includes("--file")) await fs.promises.writeFile( - `${TestGlobal.ROOT}/examples/function-calling/arguments/${props.model}.union.input.json`, + `${TestGlobal.ROOT}/examples/function-calling/arguments/llm.union.input.json`, JSON.stringify(input, null, 2), "utf8", ); diff --git a/test/src/features/llm/function_calling/validate_llm_function_calling_readonly.ts b/test/src/features/llm/function_calling/validate_llm_function_calling_readonly.ts deleted file mode 100644 index ad63171a..00000000 --- a/test/src/features/llm/function_calling/validate_llm_function_calling_readonly.ts +++ /dev/null @@ -1,103 +0,0 @@ -import typia, { tags } from "typia"; -import { v4 } from "uuid"; - -import { ILlmApplication, ILlmSchema } from "../../../../../lib"; -import { LlmApplicationFactory } from "../../../utils/LlmApplicationFactory"; -import { LlmFunctionCaller } from "../../../utils/LlmFunctionCaller"; - -export const test_chatgpt_function_calling_strict_readonly = () => - validate_llm_function_calling_readonly({ - vendor: "openai/gpt-4.1", - model: "chatgpt", - config: { - reference: true, - strict: true, - }, - }); - -export const test_chatgpt_function_calling_readonly = () => - validate_llm_function_calling_readonly({ - vendor: "openai/gpt-4.1", - model: "chatgpt", - }); - -export const test_claude_function_calling_readonly = () => - validate_llm_function_calling_readonly({ - vendor: "anthropic/claude-sonnet-4.5", - model: "claude", - }); - -export const test_deepseek_function_calling_readonly = () => - validate_llm_function_calling_readonly({ - vendor: "deepseek/deepseek-v3.1-terminus:exacto", - model: "claude", - }); - -export const test_gemini_function_calling_readonly = () => - validate_llm_function_calling_readonly({ - vendor: "google/gemini-2.5-pro", - model: "gemini", - }); - -export const test_llama_function_calling_readonly = () => - validate_llm_function_calling_readonly({ - vendor: "meta-llama/llama-3.3-70b-instruct", - model: "claude", - }); - -const validate_llm_function_calling_readonly = async < - Model extends ILlmSchema.Model, ->(props: { - vendor: string; - model: Model; - config?: ILlmSchema.IConfig; -}) => { - const application: ILlmApplication = - LlmApplicationFactory.convert({ - model: props.model, - application: typia.json.application(), - config: props.config, - }); - for (const p of [ - application.functions[0].parameters.properties.id, - application.functions[0].parameters.properties.created_at, - ]) - (p as any).readOnly = true; - return await LlmFunctionCaller.test({ - vendor: props.vendor, - model: props.model, - function: application.functions[0], - texts: [ - { - role: "assistant", - content: SYSTEM_MESSAGE, - }, - { - role: "user", - content: USER_MESSAGE, - }, - ], - handleParameters: async () => {}, - handleCompletion: async () => {}, - strict: (props.config as any)?.strict, - }); -}; - -interface IApplication { - participate(member: IMember): void; -} -interface IMember { - readonly id: string & tags.Format<"uuid">; - email: string & tags.Format<"email">; - name: string; - readonly created_at: string & tags.Format<"date-time">; -} - -const SYSTEM_MESSAGE = `You are a helpful assistant for function calling.`; -const USER_MESSAGE = ` - A new member wants to participate. - - The member's id is "${v4()}", and the account's email is "john@doe.com". - The account has been created at "2023-01-01T00:00:00.000Z" - and the member's name is "John Doe". -`; diff --git a/test/src/features/llm/function_calling/validate_llm_function_calling_sale.ts b/test/src/features/llm/function_calling/validate_llm_function_calling_sale.ts deleted file mode 100644 index e8080d6c..00000000 --- a/test/src/features/llm/function_calling/validate_llm_function_calling_sale.ts +++ /dev/null @@ -1,79 +0,0 @@ -import { ILlmApplication, ILlmSchema } from "@samchon/openapi"; -import fs from "fs"; -import typia from "typia"; - -import { TestGlobal } from "../../../TestGlobal"; -import { LlmApplicationFactory } from "../../../utils/LlmApplicationFactory"; -import { LlmFunctionCaller } from "../../../utils/LlmFunctionCaller"; -import { ShoppingSalePrompt } from "../../../utils/ShoppingSalePrompt"; - -export const test_chatgpt_function_calling_sale = () => - validate_llm_function_calling_sale({ - vendor: "openai/gpt-4.1", - model: "chatgpt", - }); - -export const test_claude_function_calling_sale = () => - validate_llm_function_calling_sale({ - vendor: "anthropic/claude-sonnet-4.5", - model: "claude", - }); - -export const test_deepseek_function_calling_sale = () => - validate_llm_function_calling_sale({ - vendor: "deepseek/deepseek-v3.1-terminus:exacto", - model: "claude", - }); - -export const test_gemini_function_calling_sale = () => - validate_llm_function_calling_sale({ - vendor: "google/gemini-2.5-pro", - model: "gemini", - }); - -export const test_llama_function_calling_sale = () => - validate_llm_function_calling_sale({ - vendor: "meta-llama/llama-3.3-70b-instruct", - model: "claude", - }); - -export const test_qwen_function_calling_sale = () => - validate_llm_function_calling_sale({ - vendor: "qwen/qwen3-next-80b-a3b-instruct", - model: "claude", - }); - -const validate_llm_function_calling_sale = async < - Model extends ILlmSchema.Model, ->(props: { - vendor: string; - model: Model; -}) => { - const application: ILlmApplication = - LlmApplicationFactory.convert({ - model: props.model, - application: typia.json.application(), - }); - return await LlmFunctionCaller.test({ - vendor: props.vendor, - model: props.model, - function: application.functions[0], - texts: await ShoppingSalePrompt.texts(), - handleParameters: async (parameters) => { - if (process.argv.includes("--file")) - fs.promises.writeFile( - `${TestGlobal.ROOT}/examples/function-calling/schemas/${props.model}.sale.schema.json`, - JSON.stringify(parameters, null, 2), - "utf8", - ); - }, - handleCompletion: async (input) => { - if (process.argv.includes("--file")) - await fs.promises.writeFile( - `${TestGlobal.ROOT}/examples/function-calling/arguments/${props.model}.sale.input.json`, - JSON.stringify(input, null, 2), - "utf8", - ); - }, - }); -}; diff --git a/test/src/features/llm/http/test_http_llm_application.ts b/test/src/features/llm/http/test_http_llm_application.ts index 970653e8..b851bfd8 100644 --- a/test/src/features/llm/http/test_http_llm_application.ts +++ b/test/src/features/llm/http/test_http_llm_application.ts @@ -15,10 +15,8 @@ export const test_http_llm_application = async (): Promise => { await fs.promises.readFile(`${TestGlobal.ROOT}/swagger.json`, "utf8"), ), ); - const application: IHttpLlmApplication<"3.0"> = HttpLlm.application({ - model: "3.0", + const application: IHttpLlmApplication = HttpLlm.application({ document, - options: {}, }); for (const func of application.functions) { const route: IHttpMigrateRoute = func.route(); diff --git a/test/src/features/llm/http/test_http_llm_application_function_name_length.ts b/test/src/features/llm/http/test_http_llm_application_function_name_length.ts index f7455da6..0d6b2b5b 100644 --- a/test/src/features/llm/http/test_http_llm_application_function_name_length.ts +++ b/test/src/features/llm/http/test_http_llm_application_function_name_length.ts @@ -8,8 +8,7 @@ export const test_http_llm_application_function_name_length = "https://wrtnlabs.github.io/connectors/swagger/swagger.json", ).then((res) => res.json()), ); - const application: IHttpLlmApplication<"chatgpt"> = HttpLlm.application({ - model: "chatgpt", + const application: IHttpLlmApplication = HttpLlm.application({ document, }); diff --git a/test/src/features/llm/http/test_http_llm_application_human.ts b/test/src/features/llm/http/test_http_llm_application_human.ts index 759229ed..8e5d70e5 100644 --- a/test/src/features/llm/http/test_http_llm_application_human.ts +++ b/test/src/features/llm/http/test_http_llm_application_human.ts @@ -10,10 +10,8 @@ export const test_http_llm_application = async (): Promise => { await fs.promises.readFile(`${TestGlobal.ROOT}/swagger.json`, "utf8"), ), ); - const application: IHttpLlmApplication<"3.0"> = HttpLlm.application({ - model: "3.0", + const application: IHttpLlmApplication = HttpLlm.application({ document, - options: {}, }); const humanSwagger: OpenApi.IDocument = JSON.parse(JSON.stringify(document)); @@ -22,10 +20,8 @@ export const test_http_llm_application = async (): Promise => { .post as OpenApi.IOperation )["x-samchon-human"] = true; const humanDocument: OpenApi.IDocument = OpenApi.convert(humanSwagger as any); - const humanApplication: IHttpLlmApplication<"3.0"> = HttpLlm.application({ - model: "3.0", + const humanApplication: IHttpLlmApplication = HttpLlm.application({ document: humanDocument, - options: {}, }); TestValidator.equals("length")(application.functions.length)( diff --git a/test/src/features/llm/http/test_http_llm_fetcher_body.ts b/test/src/features/llm/http/test_http_llm_fetcher_body.ts index 3b57c217..121fb9cb 100644 --- a/test/src/features/llm/http/test_http_llm_fetcher_body.ts +++ b/test/src/features/llm/http/test_http_llm_fetcher_body.ts @@ -5,9 +5,9 @@ import { IHttpLlmApplication, IHttpLlmFunction, IHttpResponse, + LlmTypeChecker, OpenApi, } from "@samchon/openapi"; -import { LlmSchemaComposer } from "@samchon/openapi/lib/composers/LlmSchemaComposer"; import fs from "fs"; import { TestGlobal } from "../../../TestGlobal"; @@ -20,16 +20,14 @@ export const test_http_llm_fetcher_body = async ( await fs.promises.readFile(`${TestGlobal.ROOT}/swagger.json`, "utf8"), ), ); - const application: IHttpLlmApplication<"3.0"> = HttpLlm.application({ - model: "3.0", + const application: IHttpLlmApplication = HttpLlm.application({ document, - options: { + config: { separate: (schema) => - LlmSchemaComposer.typeChecker("3.0").isString(schema) && - !!schema.contentMediaType, + LlmTypeChecker.isString(schema) && !!schema.contentMediaType, }, }); - const func: IHttpLlmFunction<"3.0"> | undefined = application.functions.find( + const func: IHttpLlmFunction | undefined = application.functions.find( (f) => f.path === "/{index}/{level}/{optimal}/body" && f.method === "post", ); if (func === undefined) throw new Error("Function not found"); diff --git a/test/src/features/llm/http/test_http_llm_fetcher_parameters.ts b/test/src/features/llm/http/test_http_llm_fetcher_parameters.ts index b6ffa7a4..2710d842 100644 --- a/test/src/features/llm/http/test_http_llm_fetcher_parameters.ts +++ b/test/src/features/llm/http/test_http_llm_fetcher_parameters.ts @@ -5,9 +5,9 @@ import { IHttpLlmApplication, IHttpLlmFunction, IHttpResponse, + LlmTypeChecker, OpenApi, } from "@samchon/openapi"; -import { LlmSchemaComposer } from "@samchon/openapi/lib/composers/LlmSchemaComposer"; import fs from "fs"; import { TestGlobal } from "../../../TestGlobal"; @@ -20,16 +20,14 @@ export const test_http_llm_fetcher_parameters = async ( await fs.promises.readFile(`${TestGlobal.ROOT}/swagger.json`, "utf8"), ), ); - const application: IHttpLlmApplication<"3.0"> = HttpLlm.application({ - model: "3.0", + const application: IHttpLlmApplication = HttpLlm.application({ document, - options: { + config: { separate: (schema) => - LlmSchemaComposer.typeChecker("3.0").isString(schema) && - !!schema.contentMediaType, + LlmTypeChecker.isString(schema) && !!schema.contentMediaType, }, }); - const func: IHttpLlmFunction<"3.0"> | undefined = application.functions.find( + const func: IHttpLlmFunction | undefined = application.functions.find( (f) => f.path === "/{index}/{level}/{optimal}/parameters" && f.method === "get", ); diff --git a/test/src/features/llm/http/test_http_llm_fetcher_query.ts b/test/src/features/llm/http/test_http_llm_fetcher_query.ts index 0dfbbeca..a5f8d9b6 100644 --- a/test/src/features/llm/http/test_http_llm_fetcher_query.ts +++ b/test/src/features/llm/http/test_http_llm_fetcher_query.ts @@ -5,9 +5,9 @@ import { IHttpLlmApplication, IHttpLlmFunction, IHttpResponse, + LlmTypeChecker, OpenApi, } from "@samchon/openapi"; -import { LlmSchemaComposer } from "@samchon/openapi/lib/composers/LlmSchemaComposer"; import fs from "fs"; import { TestGlobal } from "../../../TestGlobal"; @@ -20,16 +20,14 @@ export const test_http_llm_fetcher_query = async ( await fs.promises.readFile(`${TestGlobal.ROOT}/swagger.json`, "utf8"), ), ); - const application: IHttpLlmApplication<"3.0"> = HttpLlm.application({ - model: "3.0", + const application: IHttpLlmApplication = HttpLlm.application({ document: document, - options: { + config: { separate: (schema) => - LlmSchemaComposer.typeChecker("3.0").isString(schema) && - !!schema.contentMediaType, + LlmTypeChecker.isString(schema) && !!schema.contentMediaType, }, }); - const func: IHttpLlmFunction<"3.0"> | undefined = application.functions.find( + const func: IHttpLlmFunction | undefined = application.functions.find( (f) => f.path === "/{index}/{level}/{optimal}/query" && f.method === "get", ); if (func === undefined) throw new Error("Function not found"); diff --git a/test/src/features/llm/http/test_http_llm_fetcher_query_and_body.ts b/test/src/features/llm/http/test_http_llm_fetcher_query_and_body.ts index 53782746..45bcf2b1 100644 --- a/test/src/features/llm/http/test_http_llm_fetcher_query_and_body.ts +++ b/test/src/features/llm/http/test_http_llm_fetcher_query_and_body.ts @@ -5,9 +5,9 @@ import { IHttpLlmApplication, IHttpLlmFunction, IHttpResponse, + LlmTypeChecker, OpenApi, } from "@samchon/openapi"; -import { LlmSchemaComposer } from "@samchon/openapi/lib/composers/LlmSchemaComposer"; import fs from "fs"; import { TestGlobal } from "../../../TestGlobal"; @@ -20,16 +20,14 @@ export const test_http_llm_fetcher_query_and_body = async ( await fs.promises.readFile(`${TestGlobal.ROOT}/swagger.json`, "utf8"), ), ); - const application: IHttpLlmApplication<"3.0"> = HttpLlm.application({ - model: "3.0", + const application: IHttpLlmApplication = HttpLlm.application({ document, - options: { + config: { separate: (schema) => - LlmSchemaComposer.typeChecker("3.0").isString(schema) && - !!schema.contentMediaType, + LlmTypeChecker.isString(schema) && !!schema.contentMediaType, }, }); - const func: IHttpLlmFunction<"3.0"> | undefined = application.functions.find( + const func: IHttpLlmFunction | undefined = application.functions.find( (f) => f.path === "/{index}/{level}/{optimal}/query/body" && f.method === "post", ); diff --git a/test/src/features/llm/http/test_http_llm_function_deprecated.ts b/test/src/features/llm/http/test_http_llm_function_deprecated.ts index fab26c8c..0a2db9a7 100644 --- a/test/src/features/llm/http/test_http_llm_function_deprecated.ts +++ b/test/src/features/llm/http/test_http_llm_function_deprecated.ts @@ -15,11 +15,10 @@ export const test_http_llm_function_deprecated = async (): Promise => { await fs.promises.readFile(`${TestGlobal.ROOT}/swagger.json`, "utf8"), ), ); - const application: IHttpLlmApplication<"3.0"> = HttpLlm.application({ - model: "3.0", + const application: IHttpLlmApplication = HttpLlm.application({ document, }); - const func: IHttpLlmFunction<"3.0"> | undefined = application.functions.find( + const func: IHttpLlmFunction | undefined = application.functions.find( (f) => f.method === "get" && f.path === "/nothing", ); TestValidator.equals("deprecated")(func?.deprecated)(true); diff --git a/test/src/features/llm/http/test_http_llm_function_multipart.ts b/test/src/features/llm/http/test_http_llm_function_multipart.ts index 707ab1a7..e10c18b3 100644 --- a/test/src/features/llm/http/test_http_llm_function_multipart.ts +++ b/test/src/features/llm/http/test_http_llm_function_multipart.ts @@ -10,8 +10,7 @@ export const test_http_llm_function_multipart = async (): Promise => { await fs.promises.readFile(`${TestGlobal.ROOT}/swagger.json`, "utf8"), ), ); - const application: IHttpLlmApplication<"3.0"> = HttpLlm.application({ - model: "3.0", + const application: IHttpLlmApplication = HttpLlm.application({ document, }); TestValidator.equals("multipart not supported")( diff --git a/test/src/features/llm/http/test_http_llm_function_tags.ts b/test/src/features/llm/http/test_http_llm_function_tags.ts index 21043876..fbef8260 100644 --- a/test/src/features/llm/http/test_http_llm_function_tags.ts +++ b/test/src/features/llm/http/test_http_llm_function_tags.ts @@ -15,11 +15,10 @@ export const test_http_llm_function_deprecated = async (): Promise => { await fs.promises.readFile(`${TestGlobal.ROOT}/swagger.json`, "utf8"), ), ); - const application: IHttpLlmApplication<"3.0"> = HttpLlm.application({ - model: "3.0", + const application: IHttpLlmApplication = HttpLlm.application({ document, }); - const func: IHttpLlmFunction<"3.0"> | undefined = application.functions.find( + const func: IHttpLlmFunction | undefined = application.functions.find( (f) => f.method === "post" && f.path === "/{index}/{level}/{optimal}/body", ); TestValidator.equals("tags")(func?.tags)(["body", "post"]); diff --git a/test/src/features/llm/http/test_http_llm_merge_parameters.ts b/test/src/features/llm/http/test_http_llm_merge_parameters.ts index 8e5e19e5..576ba6d6 100644 --- a/test/src/features/llm/http/test_http_llm_merge_parameters.ts +++ b/test/src/features/llm/http/test_http_llm_merge_parameters.ts @@ -1,5 +1,5 @@ import { TestValidator } from "@nestia/e2e"; -import { HttpLlm } from "@samchon/openapi"; +import { HttpLlm, ILlmSchema } from "@samchon/openapi"; export const test_http_llm_merge_parameters = (): void => { TestValidator.equals("atomics")( @@ -16,7 +16,8 @@ export const test_http_llm_merge_parameters = (): void => { }, additionalProperties: false, required: ["a", "b", "c", "d"], - }, + $defs: {}, + } satisfies ILlmSchema.IParameters, separated: { human: { type: "object", @@ -26,7 +27,8 @@ export const test_http_llm_merge_parameters = (): void => { }, additionalProperties: false, required: ["a", "b"], - }, + $defs: {}, + } satisfies ILlmSchema.IParameters, llm: { type: "object", properties: { @@ -35,7 +37,8 @@ export const test_http_llm_merge_parameters = (): void => { }, additionalProperties: false, required: ["c", "d"], - }, + $defs: {}, + } satisfies ILlmSchema.IParameters, }, validate: null!, }, diff --git a/test/src/features/llm/invert/validate_llm_invert_array.ts b/test/src/features/llm/invert/test_llm_invert_array.ts similarity index 50% rename from test/src/features/llm/invert/validate_llm_invert_array.ts rename to test/src/features/llm/invert/test_llm_invert_array.ts index b1d4d4ec..5f9bb6fb 100644 --- a/test/src/features/llm/invert/validate_llm_invert_array.ts +++ b/test/src/features/llm/invert/test_llm_invert_array.ts @@ -3,37 +3,19 @@ import { ILlmSchema } from "@samchon/openapi"; import { LlmSchemaComposer } from "@samchon/openapi/lib/composers/LlmSchemaComposer"; import typia, { IJsonSchemaCollection, tags } from "typia"; -export const test_chatgpt_invert_array = (): void => - validate_llm_invert_array("chatgpt"); - -export const test_claude_invert_array = (): void => - validate_llm_invert_array("claude"); - -export const test_gemini_invert_array = (): void => - validate_llm_invert_array("gemini"); - -export const test_llm_v30_invert_array = (): void => - validate_llm_invert_array("3.0"); - -export const test_llm_v31_invert_array = (): void => - validate_llm_invert_array("3.1"); - -const validate_llm_invert_array = ( - model: Model, -): void => { +export const test_llm_invert_array = (): void => { const collection: IJsonSchemaCollection = typia.json.schemas< [Array & tags.MinItems<1> & tags.MaxItems<10> & tags.UniqueItems] >(); - const $defs: Record> = {}; - const converted = LlmSchemaComposer.schema(model)({ + const $defs: Record = {}; + const converted = LlmSchemaComposer.schema({ components: collection.components, schema: collection.schemas[0], - config: LlmSchemaComposer.defaultConfig(model) as any, $defs: $defs as any, }); if (converted.success === false) throw new Error(converted.error.message); - const inverted = LlmSchemaComposer.invert(model)({ + const inverted = LlmSchemaComposer.invert({ $defs, components: collection.components, schema: converted.value, diff --git a/test/src/features/llm/invert/validate_llm_invert_enum.ts b/test/src/features/llm/invert/test_llm_invert_enum.ts similarity index 52% rename from test/src/features/llm/invert/validate_llm_invert_enum.ts rename to test/src/features/llm/invert/test_llm_invert_enum.ts index ed9a32ef..e298e551 100644 --- a/test/src/features/llm/invert/validate_llm_invert_enum.ts +++ b/test/src/features/llm/invert/test_llm_invert_enum.ts @@ -3,34 +3,16 @@ import { ILlmSchema } from "@samchon/openapi"; import { LlmSchemaComposer } from "@samchon/openapi/lib/composers/LlmSchemaComposer"; import typia, { IJsonSchemaCollection } from "typia"; -export const test_chatgpt_invert_enum = (): void => - validate_llm_invert_enum("chatgpt"); - -export const test_claude_invert_enum = (): void => - validate_llm_invert_enum("claude"); - -export const test_gemini_invert_enum = (): void => - validate_llm_invert_enum("gemini"); - -export const test_llm_v30_invert_enum = (): void => - validate_llm_invert_enum("3.0"); - -export const test_llm_v31_invert_enum = (): void => - validate_llm_invert_enum("3.1"); - -const validate_llm_invert_enum = ( - model: Model, -): void => { +export const test_llm_invert_enum = (): void => { const validate = (collection: IJsonSchemaCollection) => { - const $defs: Record> = {}; - const converted = LlmSchemaComposer.schema(model)({ + const $defs: Record = {}; + const converted = LlmSchemaComposer.schema({ components: collection.components, schema: collection.schemas[0], - config: LlmSchemaComposer.defaultConfig(model) as any, $defs: $defs as any, }); if (converted.success === false) throw new Error(converted.error.message); - const inverted = LlmSchemaComposer.invert(model)({ + const inverted = LlmSchemaComposer.invert({ $defs, components: collection.components, schema: collection.schemas[0], diff --git a/test/src/features/llm/invert/validate_llm_invert_integer.ts b/test/src/features/llm/invert/test_llm_invert_integer.ts similarity index 59% rename from test/src/features/llm/invert/validate_llm_invert_integer.ts rename to test/src/features/llm/invert/test_llm_invert_integer.ts index 7c0fd4f5..d6e2c0b8 100644 --- a/test/src/features/llm/invert/validate_llm_invert_integer.ts +++ b/test/src/features/llm/invert/test_llm_invert_integer.ts @@ -3,34 +3,16 @@ import { ILlmSchema } from "@samchon/openapi"; import { LlmSchemaComposer } from "@samchon/openapi/lib/composers/LlmSchemaComposer"; import typia, { IJsonSchemaCollection, tags } from "typia"; -export const test_chatgpt_invert_integer = (): void => - validate_llm_invert_integer("chatgpt"); - -export const test_claude_invert_integer = (): void => - validate_llm_invert_integer("claude"); - -export const test_gemini_invert_integer = (): void => - validate_llm_invert_integer("gemini"); - -export const test_llm_v30_invert_integer = (): void => - validate_llm_invert_integer("3.0"); - -export const test_llm_v31_invert_integer = (): void => - validate_llm_invert_integer("3.1"); - -const validate_llm_invert_integer = ( - model: Model, -): void => { +export const test_llm_invert_integer = (): void => { const validate = (collection: IJsonSchemaCollection) => { - const $defs: Record> = {}; - const converted = LlmSchemaComposer.schema(model)({ + const $defs: Record = {}; + const converted = LlmSchemaComposer.schema({ components: collection.components, schema: collection.schemas[0], - config: LlmSchemaComposer.defaultConfig(model) as any, $defs: $defs as any, }); if (converted.success === false) throw new Error(converted.error.message); - const inverted = LlmSchemaComposer.invert(model)({ + const inverted = LlmSchemaComposer.invert({ $defs, components: collection.components, schema: converted.value, diff --git a/test/src/features/llm/invert/validate_llm_invert_nullable.ts b/test/src/features/llm/invert/test_llm_invert_nullable.ts similarity index 68% rename from test/src/features/llm/invert/validate_llm_invert_nullable.ts rename to test/src/features/llm/invert/test_llm_invert_nullable.ts index 3cec1359..934b5881 100644 --- a/test/src/features/llm/invert/validate_llm_invert_nullable.ts +++ b/test/src/features/llm/invert/test_llm_invert_nullable.ts @@ -3,34 +3,16 @@ import { ILlmSchema } from "@samchon/openapi"; import { LlmSchemaComposer } from "@samchon/openapi/lib/composers/LlmSchemaComposer"; import typia, { IJsonSchemaCollection, tags } from "typia"; -export const test_chatgpt_invert_nullable = (): void => - validate_llm_invert_nullable("chatgpt"); - -export const test_claude_invert_nullable = (): void => - validate_llm_invert_nullable("claude"); - -export const test_gemini_invert_nullable = (): void => - validate_llm_invert_nullable("gemini"); - -export const test_llm_v30_invert_nullable = (): void => - validate_llm_invert_nullable("3.0"); - -export const test_llm_v31_invert_nullable = (): void => - validate_llm_invert_nullable("3.1"); - -const validate_llm_invert_nullable = ( - model: Model, -): void => { +export const test_llm_invert_nullable = (): void => { const validate = (collection: IJsonSchemaCollection) => { - const $defs: Record> = {}; - const converted = LlmSchemaComposer.schema(model)({ + const $defs: Record = {}; + const converted = LlmSchemaComposer.schema({ components: collection.components, schema: collection.schemas[0], - config: LlmSchemaComposer.defaultConfig(model) as any, $defs: $defs as any, }); if (converted.success === false) throw new Error(converted.error.message); - const inverted = LlmSchemaComposer.invert(model)({ + const inverted = LlmSchemaComposer.invert({ $defs, components: collection.components, schema: converted.value, diff --git a/test/src/features/llm/invert/validate_llm_invert_number.ts b/test/src/features/llm/invert/test_llm_invert_number.ts similarity index 55% rename from test/src/features/llm/invert/validate_llm_invert_number.ts rename to test/src/features/llm/invert/test_llm_invert_number.ts index df1601d0..cd6a7506 100644 --- a/test/src/features/llm/invert/validate_llm_invert_number.ts +++ b/test/src/features/llm/invert/test_llm_invert_number.ts @@ -3,34 +3,16 @@ import { ILlmSchema } from "@samchon/openapi"; import { LlmSchemaComposer } from "@samchon/openapi/lib/composers/LlmSchemaComposer"; import typia, { IJsonSchemaCollection, tags } from "typia"; -export const test_chatgpt_invert_number = (): void => - validate_llm_invert_number("chatgpt"); - -export const test_claude_invert_number = (): void => - validate_llm_invert_number("claude"); - -export const test_gemini_invert_number = (): void => - validate_llm_invert_number("gemini"); - -export const test_llm_v30_invert_number = (): void => - validate_llm_invert_number("3.0"); - -export const test_llm_v31_invert_number = (): void => - validate_llm_invert_number("3.1"); - -const validate_llm_invert_number = ( - model: Model, -): void => { +export const test_llm_invert_number = (): void => { const validate = (collection: IJsonSchemaCollection) => { - const $defs: Record> = {}; - const converted = LlmSchemaComposer.schema(model)({ + const $defs: Record = {}; + const converted = LlmSchemaComposer.schema({ components: collection.components, schema: collection.schemas[0], - config: LlmSchemaComposer.defaultConfig(model) as any, $defs: $defs as any, }); if (converted.success === false) throw new Error(converted.error.message); - const inverted = LlmSchemaComposer.invert(model)({ + const inverted = LlmSchemaComposer.invert({ $defs, components: collection.components, schema: converted.value, diff --git a/test/src/features/llm/invert/validate_llm_invert_object.ts b/test/src/features/llm/invert/test_llm_invert_object.ts similarity index 57% rename from test/src/features/llm/invert/validate_llm_invert_object.ts rename to test/src/features/llm/invert/test_llm_invert_object.ts index 3953982b..60136d69 100644 --- a/test/src/features/llm/invert/validate_llm_invert_object.ts +++ b/test/src/features/llm/invert/test_llm_invert_object.ts @@ -1,26 +1,9 @@ import { TestValidator } from "@nestia/e2e"; -import { ILlmSchema, OpenApi } from "@samchon/openapi"; +import { OpenApi } from "@samchon/openapi"; import { LlmSchemaComposer } from "@samchon/openapi/lib/composers/LlmSchemaComposer"; import typia, { IJsonSchemaCollection, tags } from "typia"; -export const test_chatgpt_invert_object = (): void => - validate_llm_invert_object("chatgpt"); - -export const test_claude_invert_object = (): void => - validate_llm_invert_object("claude"); - -export const test_gemini_invert_object = (): void => - validate_llm_invert_object("gemini"); - -export const test_llm_v30_invert_object = (): void => - validate_llm_invert_object("3.0"); - -export const test_llm_v31_invert_object = (): void => - validate_llm_invert_object("3.1"); - -const validate_llm_invert_object = ( - model: Model, -): void => { +export const test_llm_invert_object = (): void => { const collection: IJsonSchemaCollection = typia.json.schemas< [ { @@ -38,15 +21,12 @@ const validate_llm_invert_object = ( }, ] >(); - const converted = LlmSchemaComposer.parameters(model)({ - config: { - reference: true, - } as any, + const converted = LlmSchemaComposer.parameters({ components: collection.components, schema: collection.schemas[0] as OpenApi.IJsonSchema.IReference, }); if (converted.success === false) throw new Error(converted.error.message); - const inverted = LlmSchemaComposer.invert(model)({ + const inverted = LlmSchemaComposer.invert({ $defs: (converted.value as any).$defs, components: collection.components, schema: converted.value, diff --git a/test/src/features/llm/invert/validate_llm_invert_oneof.ts b/test/src/features/llm/invert/test_llm_invert_oneof.ts similarity index 60% rename from test/src/features/llm/invert/validate_llm_invert_oneof.ts rename to test/src/features/llm/invert/test_llm_invert_oneof.ts index a66c5aba..78f29af7 100644 --- a/test/src/features/llm/invert/validate_llm_invert_oneof.ts +++ b/test/src/features/llm/invert/test_llm_invert_oneof.ts @@ -3,31 +3,16 @@ import { ILlmSchema } from "@samchon/openapi"; import { LlmSchemaComposer } from "@samchon/openapi/lib/composers/LlmSchemaComposer"; import typia, { IJsonSchemaCollection } from "typia"; -export const test_chatgpt_invert_oneof = (): void => - validate_llm_invert_oneof("chatgpt"); - -export const test_claude_invert_oneof = (): void => - validate_llm_invert_oneof("claude"); - -export const test_llm_v30_invert_oneof = (): void => - validate_llm_invert_oneof("3.0"); - -export const test_llm_v31_invert_oneof = (): void => - validate_llm_invert_oneof("3.1"); - -const validate_llm_invert_oneof = ( - model: Model, -): void => { +export const test_llm_invert_oneof = (): void => { const validate = (collection: IJsonSchemaCollection) => { - const $defs: Record> = {}; - const converted = LlmSchemaComposer.schema(model)({ + const $defs: Record = {}; + const converted = LlmSchemaComposer.schema({ components: collection.components, schema: collection.schemas[0], - config: LlmSchemaComposer.defaultConfig(model) as any, $defs: $defs as any, }); if (converted.success === false) throw new Error(converted.error.message); - const inverted = LlmSchemaComposer.invert(model)({ + const inverted = LlmSchemaComposer.invert({ $defs, components: collection.components, schema: converted.value, diff --git a/test/src/features/llm/invert/validate_llm_invert_ref.ts b/test/src/features/llm/invert/test_llm_invert_ref.ts similarity index 58% rename from test/src/features/llm/invert/validate_llm_invert_ref.ts rename to test/src/features/llm/invert/test_llm_invert_ref.ts index 1fcdb4f8..fbf0b44e 100644 --- a/test/src/features/llm/invert/validate_llm_invert_ref.ts +++ b/test/src/features/llm/invert/test_llm_invert_ref.ts @@ -1,34 +1,28 @@ import { TestValidator } from "@nestia/e2e"; -import { ILlmSchema, OpenApi, OpenApiTypeChecker } from "@samchon/openapi"; +import { + ILlmSchema, + IOpenApiSchemaError, + IResult, + OpenApi, + OpenApiTypeChecker, +} from "@samchon/openapi"; import { LlmSchemaComposer } from "@samchon/openapi/lib/composers/LlmSchemaComposer"; import typia, { IJsonSchemaCollection, tags } from "typia"; -export const test_chatgpt_invert_ref = (): void => - validate_llm_invert_ref("chatgpt"); - -export const test_claude_invert_ref = (): void => - validate_llm_invert_ref("claude"); - -export const test_llm_v31_invert_ref = (): void => - validate_llm_invert_ref("3.1"); - -const validate_llm_invert_ref = ( - model: Model, -): void => { +export const test_llm_invert_ref = (): void => { const collection: IJsonSchemaCollection = typia.json.schemas<[IMember]>(); - const converted = LlmSchemaComposer.parameters(model)({ - config: { - reference: true, - } as any, - components: collection.components, - schema: collection.schemas[0] as OpenApi.IJsonSchema.IReference, - }); + const converted: IResult = + LlmSchemaComposer.parameters({ + components: collection.components, + schema: collection.schemas[0] as OpenApi.IJsonSchema.IReference, + }); if (converted.success === false) throw new Error(converted.error.message); - const inverted = LlmSchemaComposer.invert(model)({ - $defs: (converted.value as any).$defs, + + const inverted: OpenApi.IJsonSchema = LlmSchemaComposer.invert({ + $defs: converted.value.$defs, components: collection.components, schema: converted.value, - } as any); + }); TestValidator.predicate("inverted")( OpenApiTypeChecker.isObject(inverted) && inverted.properties !== undefined && diff --git a/test/src/features/llm/invert/validate_llm_invert_string.ts b/test/src/features/llm/invert/test_llm_invert_string.ts similarity index 59% rename from test/src/features/llm/invert/validate_llm_invert_string.ts rename to test/src/features/llm/invert/test_llm_invert_string.ts index b54f9cf6..49966f95 100644 --- a/test/src/features/llm/invert/validate_llm_invert_string.ts +++ b/test/src/features/llm/invert/test_llm_invert_string.ts @@ -3,31 +3,16 @@ import { ILlmSchema } from "@samchon/openapi"; import { LlmSchemaComposer } from "@samchon/openapi/lib/composers/LlmSchemaComposer"; import typia, { IJsonSchemaCollection, tags } from "typia"; -export const test_chatgpt_invert_string = (): void => - validate_llm_invert_string("chatgpt"); - -export const test_claude_invert_string = (): void => - validate_llm_invert_string("claude"); - -export const test_llm_v30_invert_string = (): void => - validate_llm_invert_string("3.0"); - -export const test_llm_v31_invert_string = (): void => - validate_llm_invert_string("3.1"); - -const validate_llm_invert_string = ( - model: Model, -): void => { +export const test_llm_invert_string = (): void => { const validate = (collection: IJsonSchemaCollection) => { - const $defs: Record> = {}; - const converted = LlmSchemaComposer.schema(model)({ + const $defs: Record = {}; + const converted = LlmSchemaComposer.schema({ components: collection.components, schema: collection.schemas[0], - config: LlmSchemaComposer.defaultConfig(model) as any, $defs: $defs as any, }); if (converted.success === false) throw new Error(converted.error.message); - const inverted = LlmSchemaComposer.invert(model)({ + const inverted = LlmSchemaComposer.invert({ $defs, components: collection.components, schema: converted.value, diff --git a/test/src/features/llm/schema/validate_llm_schema_mismatch.ts b/test/src/features/llm/parameters/test_llm_parameters_mismatch.ts similarity index 65% rename from test/src/features/llm/schema/validate_llm_schema_mismatch.ts rename to test/src/features/llm/parameters/test_llm_parameters_mismatch.ts index 74391f8b..7d8d59df 100644 --- a/test/src/features/llm/schema/validate_llm_schema_mismatch.ts +++ b/test/src/features/llm/parameters/test_llm_parameters_mismatch.ts @@ -8,24 +8,7 @@ import { import { LlmSchemaComposer } from "@samchon/openapi/lib/composers/LlmSchemaComposer"; import typia, { IJsonSchemaCollection } from "typia"; -export const test_chatgpt_schema_mismatch = (): void => - validate_llm_schema_mismatch("chatgpt"); - -export const test_claude_schema_mismatch = (): void => - validate_llm_schema_mismatch("claude"); - -export const test_gemini_schema_mismatch = (): void => - validate_llm_schema_mismatch("gemini"); - -export const test_llm_v30_schema_mismatch = (): void => - validate_llm_schema_mismatch("3.0"); - -export const test_llm_v31_schema_mismatch = (): void => - validate_llm_schema_mismatch("3.1"); - -const validate_llm_schema_mismatch = ( - model: Model, -): void => { +export const test_llm_parameters_mismatch = (): void => { const collection: IJsonSchemaCollection = typia.json.schemas< [ { @@ -46,19 +29,15 @@ const validate_llm_schema_mismatch = ( p.third.items.properties.nested.$ref = "#/components/schemas/IRectangle1"; const result: IResult< - ILlmSchema, + ILlmSchema.IParameters, IOpenApiSchemaError - > = LlmSchemaComposer.schema(model)({ + > = LlmSchemaComposer.parameters({ accessor: "$input", - config: LlmSchemaComposer.defaultConfig( - model, - ) satisfies ILlmSchema.IConfig as any, components: collection.components, schema: typia.assert< OpenApi.IJsonSchema.IReference | OpenApi.IJsonSchema.IObject >(collection.schemas[0]), - $defs: {}, - } as any) as IResult, IOpenApiSchemaError>; + }); TestValidator.equals("success")(result.success)(false); TestValidator.equals("errors")( result.success ? [] : result.error.reasons.map((r) => r.accessor).sort(), diff --git a/test/src/features/llm/parameters/test_llm_parameters_reference_escaped_description_of_name.ts b/test/src/features/llm/parameters/test_llm_parameters_reference_escaped_description_of_name.ts new file mode 100644 index 00000000..0dea5636 --- /dev/null +++ b/test/src/features/llm/parameters/test_llm_parameters_reference_escaped_description_of_name.ts @@ -0,0 +1,49 @@ +import { TestValidator } from "@nestia/e2e"; +import { + ILlmSchema, + IOpenApiSchemaError, + IResult, + OpenApi, +} from "@samchon/openapi"; +import { LlmSchemaComposer } from "@samchon/openapi/lib/composers/LlmSchemaComposer"; +import typia, { IJsonSchemaCollection } from "typia"; + +export const test_llm_parameters_reference_escaped_description_of_name = + (): void => { + const collection: IJsonSchemaCollection = + typia.json.schemas<[Something.INested.IDeep]>(); + const deep: ILlmSchema.IParameters = composeSchema(collection); + TestValidator.predicate("description")( + () => !!deep.description?.includes("Something.INested.IDeep"), + ); + }; + +interface Something { + x: number; +} +namespace Something { + export interface INested { + y: number; + } + export namespace INested { + export interface IDeep { + z: number; + } + } +} + +const composeSchema = ( + collection: IJsonSchemaCollection, +): ILlmSchema.IParameters => { + const result: IResult< + ILlmSchema.IParameters, + IOpenApiSchemaError + > = LlmSchemaComposer.parameters({ + components: collection.components, + schema: typia.assert< + OpenApi.IJsonSchema.IObject | OpenApi.IJsonSchema.IReference + >(collection.schemas[0]), + }); + if (result.success === false) throw new Error("Invalid schema"); + return result.value; +}; diff --git a/test/src/features/llm/parameters/test_llm_parameters_separate_array.ts b/test/src/features/llm/parameters/test_llm_parameters_separate_array.ts new file mode 100644 index 00000000..6c8066f6 --- /dev/null +++ b/test/src/features/llm/parameters/test_llm_parameters_separate_array.ts @@ -0,0 +1,84 @@ +import { TestValidator } from "@nestia/e2e"; +import { + ILlmSchema, + IOpenApiSchemaError, + IResult, + LlmTypeChecker, + OpenApi, +} from "@samchon/openapi"; +import { LlmSchemaComposer } from "@samchon/openapi/lib/composers/LlmSchemaComposer"; +import typia, { IJsonSchemaCollection, tags } from "typia"; + +export const test_llm_parameters_separate_array = (): void => { + const separator = (schema: ILlmSchema.IParameters) => + LlmSchemaComposer.separate({ + predicate: (s) => + LlmTypeChecker.isString(s as OpenApi.IJsonSchema.IString) && + (s as OpenApi.IJsonSchema.IString).contentMediaType !== undefined, + parameters: schema, + }); + const member: ILlmSchema.IParameters = schema( + typia.json.schemas<[IManagement]>(), + ); + const upload: ILlmSchema.IParameters = schema( + typia.json.schemas<[IManagement]>(), + ); + const combined: ILlmSchema.IParameters = schema( + typia.json.schemas<[IManagement]>(), + ); + + TestValidator.equals( + "member", + (key) => key !== "description", + )(separator(member))({ + llm: member, + human: null, + }); + TestValidator.equals( + "upload", + (key) => key !== "description", + )(separator(upload))({ + llm: { + type: "object", + properties: {}, + additionalProperties: false, + required: [], + $defs: {}, + }, + human: upload, + }); + TestValidator.equals( + "combined", + (key) => key !== "description", + )(separator(combined))({ + llm: member, + human: upload, + }); +}; + +interface IManagement { + profiles: T[]; +} +interface IMember { + id: number; + name: string; +} +interface IFileUpload { + file: string & tags.ContentMediaType<"image/png">; +} +interface ICombined extends IMember, IFileUpload {} + +const schema = (collection: IJsonSchemaCollection): ILlmSchema.IParameters => { + const result: IResult = + LlmSchemaComposer.parameters({ + components: collection.components, + schema: typia.assert< + OpenApi.IJsonSchema.IObject | OpenApi.IJsonSchema.IReference + >(collection.schemas[0]), + }); + if (result.success === false) { + console.log(result.error); + throw new Error("Invalid schema"); + } + return result.value; +}; diff --git a/test/src/features/llm/parameters/test_llm_parameters_separate_nested.ts b/test/src/features/llm/parameters/test_llm_parameters_separate_nested.ts new file mode 100644 index 00000000..00f96051 --- /dev/null +++ b/test/src/features/llm/parameters/test_llm_parameters_separate_nested.ts @@ -0,0 +1,92 @@ +import { TestValidator } from "@nestia/e2e"; +import { + ILlmSchema, + IOpenApiSchemaError, + IResult, + LlmTypeChecker, + OpenApi, +} from "@samchon/openapi"; +import { LlmSchemaComposer } from "@samchon/openapi/lib/composers/LlmSchemaComposer"; +import typia, { IJsonSchemaCollection, tags } from "typia"; + +export const test_llm_parameters_separate_nested = (): void => { + const separator = (schema: ILlmSchema.IParameters) => + LlmSchemaComposer.separate({ + predicate: (s) => + LlmTypeChecker.isString(s as OpenApi.IJsonSchema.IString) && + (s as OpenApi.IJsonSchema.IString).description?.includes( + "@contentMediaType", + ) === true, + parameters: schema as any, + }); + const member: ILlmSchema.IParameters = schema()( + typia.json.schemas<[INested]>(), + ); + const upload: ILlmSchema.IParameters = schema()( + typia.json.schemas<[INested]>(), + ); + const combined: ILlmSchema.IParameters = schema()( + typia.json.schemas<[INested]>(), + ); + + TestValidator.equals( + "member", + (key) => key !== "description", + )(separator(member))({ + llm: member, + human: null, + }); + TestValidator.equals( + "upload", + (key) => key !== "description", + )(separator(upload))({ + llm: { + type: "object", + properties: {}, + additionalProperties: false, + required: [], + $defs: {}, + }, + human: upload, + }); + TestValidator.equals( + "combined", + (key) => key !== "description", + )(separator(combined))({ + llm: member, + human: upload, + }); +}; + +interface INested { + first: { + second: { + third: { + fourth: T; + }; + array: T[]; + }; + }; +} +interface IMember { + id: number; + name: string; +} +interface IFileUpload { + file: string & tags.Format<"uri"> & tags.ContentMediaType<"image/png">; +} +interface ICombined extends IMember, IFileUpload {} + +const schema = + () => + (collection: IJsonSchemaCollection): ILlmSchema.IParameters => { + const result: IResult = + LlmSchemaComposer.parameters({ + components: collection.components, + schema: typia.assert< + OpenApi.IJsonSchema.IObject | OpenApi.IJsonSchema.IReference + >(collection.schemas[0]), + }) as IResult; + if (result.success === false) throw new Error("Invalid schema"); + return result.value; + }; diff --git a/test/src/features/llm/parameters/test_llm_parameters_separate_object.ts b/test/src/features/llm/parameters/test_llm_parameters_separate_object.ts new file mode 100644 index 00000000..877f3e59 --- /dev/null +++ b/test/src/features/llm/parameters/test_llm_parameters_separate_object.ts @@ -0,0 +1,78 @@ +import { TestValidator } from "@nestia/e2e"; +import { + ILlmSchema, + IOpenApiSchemaError, + IResult, + LlmTypeChecker, + OpenApi, +} from "@samchon/openapi"; +import { LlmSchemaComposer } from "@samchon/openapi/lib/composers/LlmSchemaComposer"; +import typia, { IJsonSchemaCollection, tags } from "typia"; + +export const test_llm_parameters_separate_object = (): void => { + const separator = (schema: ILlmSchema.IParameters) => + LlmSchemaComposer.separate({ + predicate: (s) => + LlmTypeChecker.isString(s as OpenApi.IJsonSchema.IString) && + (s as OpenApi.IJsonSchema.IString).contentMediaType !== undefined, + parameters: schema, + }); + const member: ILlmSchema.IParameters = schema( + typia.json.schemas<[IMember]>(), + ); + const upload: ILlmSchema.IParameters = schema( + typia.json.schemas<[IFileUpload]>(), + ); + const combined: ILlmSchema.IParameters = schema( + typia.json.schemas<[ICombined]>(), + ); + + TestValidator.equals( + "member", + (key) => key !== "description", + )(separator(member))({ + llm: member, + human: null, + }); + TestValidator.equals( + "upload", + (key) => key !== "description", + )(separator(upload))({ + llm: { + type: "object", + properties: {}, + additionalProperties: false, + required: [], + $defs: {}, + }, + human: upload, + }); + TestValidator.equals( + "combined", + (key) => key !== "description", + )(separator(combined))({ + llm: member, + human: upload, + }); +}; + +interface IMember { + id: number; + name: string; +} +interface IFileUpload { + file: string & tags.Format<"uri"> & tags.ContentMediaType<"image/png">; +} +interface ICombined extends IMember, IFileUpload {} + +const schema = (collection: IJsonSchemaCollection): ILlmSchema.IParameters => { + const result: IResult = + LlmSchemaComposer.parameters({ + components: collection.components, + schema: typia.assert< + OpenApi.IJsonSchema.IObject | OpenApi.IJsonSchema.IReference + >(collection.schemas[0]), + }); + if (result.success === false) throw new Error("Invalid schema"); + return result.value; +}; diff --git a/test/src/features/llm/parameters/test_llm_parameters_separate_object_additionalProperties.ts b/test/src/features/llm/parameters/test_llm_parameters_separate_object_additionalProperties.ts new file mode 100644 index 00000000..7fbb1f2c --- /dev/null +++ b/test/src/features/llm/parameters/test_llm_parameters_separate_object_additionalProperties.ts @@ -0,0 +1,94 @@ +import { TestValidator } from "@nestia/e2e"; +import { + ILlmSchema, + IOpenApiSchemaError, + IResult, + LlmTypeChecker, + OpenApi, +} from "@samchon/openapi"; +import { LlmSchemaComposer } from "@samchon/openapi/lib/composers/LlmSchemaComposer"; +import typia, { IJsonSchemaCollection, tags } from "typia"; + +export const test_llm_parameters_separate_object_additionalProperties = + (): void => { + const separator = (schema: ILlmSchema.IParameters) => + LlmSchemaComposer.separate({ + predicate: (s) => + LlmTypeChecker.isString(s as OpenApi.IJsonSchema.IString) && + (s as OpenApi.IJsonSchema.IString).description?.includes( + "@contentMediaType", + ) === true, + parameters: schema as any, + }); + const params: ILlmSchema.IParameters = schema()( + typia.json.schemas<[IParameters]>(), + ); + TestValidator.equals( + "separated", + (key) => key !== "description", + )(separator(params))({ + llm: schema()( + typia.json.schemas< + [ + { + input: { + email: string; + hobbies: Record< + string, + { + id: string; + name: string; + } + >; + }; + }, + ] + >(), + ), + human: schema()( + typia.json.schemas< + [ + { + input: { + hobbies: Record< + string, + { + thumbnail: string & + tags.Format<"uri"> & + tags.ContentMediaType<"image/*">; + } + >; + }; + }, + ] + >(), + ), + }); + }; + +interface IParameters { + input: IMember; +} +interface IMember { + email: string; + hobbies: Record; +} +interface IHobby { + id: string; + name: string; + thumbnail: string & tags.Format<"uri"> & tags.ContentMediaType<"image/*">; +} + +const schema = + () => + (collection: IJsonSchemaCollection): ILlmSchema.IParameters => { + const result: IResult = + LlmSchemaComposer.parameters({ + components: collection.components, + schema: typia.assert< + OpenApi.IJsonSchema.IObject | OpenApi.IJsonSchema.IReference + >(collection.schemas[0]), + }) as IResult; + if (result.success === false) throw new Error("Invalid schema"); + return result.value; + }; diff --git a/test/src/features/llm/parameters/test_llm_parameters_separate_ref.ts b/test/src/features/llm/parameters/test_llm_parameters_separate_ref.ts new file mode 100644 index 00000000..d0a55388 --- /dev/null +++ b/test/src/features/llm/parameters/test_llm_parameters_separate_ref.ts @@ -0,0 +1,112 @@ +import { TestValidator } from "@nestia/e2e"; +import { + ILlmSchema, + IOpenApiSchemaError, + IResult, + LlmTypeChecker, + OpenApi, +} from "@samchon/openapi"; +import { LlmSchemaComposer } from "@samchon/openapi/lib/composers/LlmSchemaComposer"; +import typia, { IJsonSchemaCollection, tags } from "typia"; + +export const test_llm_parameters_separate_ref = (): void => { + const separator = (schema: ILlmSchema.IParameters) => + LlmSchemaComposer.separate({ + predicate: (s) => + LlmTypeChecker.isString(s as OpenApi.IJsonSchema.IString) && + (s as OpenApi.IJsonSchema.IString).description?.includes( + "@contentMediaType", + ) === true, + parameters: schema as any, + }); + const member: ILlmSchema.IParameters = schema()( + typia.json.schemas<[IWrapper]>(), + ); + const upload: ILlmSchema.IParameters = schema()( + typia.json.schemas<[IWrapper]>(), + ); + const combined: ILlmSchema.IParameters = schema()( + typia.json.schemas<[IWrapper]>(), + ); + + TestValidator.equals( + "member", + (key) => key !== "description", + )(separator(member))({ + llm: member, + human: null, + }); + TestValidator.equals( + "upload", + (key) => key !== "description", + )(separator(upload))({ + llm: { + type: "object", + properties: {}, + additionalProperties: false, + required: [], + $defs: {}, + }, + human: upload, + }); + TestValidator.equals( + "combined", + (key) => key !== "description", + )({ + llm: separator(combined).llm + ? { ...separator(combined).llm, $defs: {} } + : null, + human: separator(combined).human + ? { ...separator(combined).human, $defs: {} } + : null, + })({ + llm: { + $defs: {}, + type: "object", + properties: { + value: { + $ref: "#/$defs/ICombined.Llm", + }, + }, + required: ["value"], + additionalProperties: false, + } satisfies ILlmSchema.IParameters, + human: { + $defs: {}, + type: "object", + properties: { + value: { + $ref: "#/$defs/ICombined.Human", + }, + }, + required: ["value"], + additionalProperties: false, + } satisfies ILlmSchema.IParameters, + }); +}; + +interface IWrapper { + value: T; +} +interface IMember { + id: number; + name: string; +} +interface IFileUpload { + file: string & tags.Format<"uri"> & tags.ContentMediaType<"image/png">; +} +interface ICombined extends IMember, IFileUpload {} + +const schema = + () => + (collection: IJsonSchemaCollection): ILlmSchema.IParameters => { + const result: IResult = + LlmSchemaComposer.parameters({ + components: collection.components, + schema: typia.assert< + OpenApi.IJsonSchema.IObject | OpenApi.IJsonSchema.IReference + >(collection.schemas[0]), + }) as IResult; + if (result.success === false) throw new Error("Invalid schema"); + return result.value; + }; diff --git a/test/src/features/llm/parameters/test_llm_parameters_separate_validate.ts b/test/src/features/llm/parameters/test_llm_parameters_separate_validate.ts new file mode 100644 index 00000000..bae92af3 --- /dev/null +++ b/test/src/features/llm/parameters/test_llm_parameters_separate_validate.ts @@ -0,0 +1,43 @@ +import { TestValidator } from "@nestia/e2e"; +import { + ILlmFunction, + ILlmSchema, + IOpenApiSchemaError, + IResult, + OpenApi, + OpenApiTypeChecker, +} from "@samchon/openapi"; +import { LlmSchemaComposer } from "@samchon/openapi/lib/composers/LlmSchemaComposer"; +import typia from "typia"; + +export const test_llm_parameters_separate_validate = (): void => { + const collection = typia.json.schemas<[ISeparatable, IHumanOnly]>(); + const validate = (schema: OpenApi.IJsonSchema, exists: boolean) => { + const result: IResult = + LlmSchemaComposer.parameters({ + $defs: {}, + components: collection.components, + schema: schema as OpenApi.IJsonSchema.IReference, + } as any) as IResult; + if (result.success === false) throw new Error("Failed to convert"); + + const separated: ILlmFunction.ISeparated = LlmSchemaComposer.separate({ + parameters: result.value as ILlmSchema.IParameters, + predicate: (s: OpenApi.IJsonSchema) => OpenApiTypeChecker.isNumber(s), + } as any) as ILlmFunction.ISeparated; + TestValidator.equals( + "validate", + (key) => key !== "description", + )(!!separated.validate)(exists); + }; + validate(collection.schemas[0], true); + validate(collection.schemas[1], false); +}; + +interface ISeparatable { + title: string; + value: number; +} +interface IHumanOnly { + value: number; +} diff --git a/test/src/features/llm/parameters/validate_llm_parameters_tuple.ts b/test/src/features/llm/parameters/test_llm_parameters_tuple.ts similarity index 59% rename from test/src/features/llm/parameters/validate_llm_parameters_tuple.ts rename to test/src/features/llm/parameters/test_llm_parameters_tuple.ts index 43fead9e..96b46a01 100644 --- a/test/src/features/llm/parameters/validate_llm_parameters_tuple.ts +++ b/test/src/features/llm/parameters/test_llm_parameters_tuple.ts @@ -8,24 +8,7 @@ import { import { LlmSchemaComposer } from "@samchon/openapi/lib/composers/LlmSchemaComposer"; import typia, { IJsonSchemaCollection } from "typia"; -export const test_chatgpt_parameters_tuple = (): void => - validate_llm_parameters_tuple("chatgpt"); - -export const test_claude_parameters_tuple = (): void => - validate_llm_parameters_tuple("claude"); - -export const test_gemini_parameters_tuple = (): void => - validate_llm_parameters_tuple("gemini"); - -export const test_llm_v30_parameters_tuple = (): void => - validate_llm_parameters_tuple("3.0"); - -export const test_llm_v31_parameters_tuple = (): void => - validate_llm_parameters_tuple("3.1"); - -const validate_llm_parameters_tuple = ( - model: Model, -): void => { +export const test_llm_parameters_tuple = (): void => { const collection: IJsonSchemaCollection = typia.json.schemas< [ { @@ -41,18 +24,15 @@ const validate_llm_parameters_tuple = ( ] >(); const result: IResult< - ILlmSchema.IParameters, + ILlmSchema.IParameters, IOpenApiSchemaError - > = LlmSchemaComposer.parameters(model)({ + > = LlmSchemaComposer.parameters({ accessor: "$input", - config: LlmSchemaComposer.defaultConfig( - model, - ) satisfies ILlmSchema.IConfig as any, components: collection.components, schema: typia.assert< OpenApi.IJsonSchema.IReference | OpenApi.IJsonSchema.IObject >(collection.schemas[0]), - }) as IResult, IOpenApiSchemaError>; + }); TestValidator.equals("parameters")(result.success)(false); TestValidator.equals("errors")( result.success ? [] : result.error.reasons.map((r) => r.accessor).sort(), diff --git a/test/src/features/llm/parameters/validate_llm_parameters_reference_escaped_description_of_name.ts b/test/src/features/llm/parameters/validate_llm_parameters_reference_escaped_description_of_name.ts deleted file mode 100644 index 484cd984..00000000 --- a/test/src/features/llm/parameters/validate_llm_parameters_reference_escaped_description_of_name.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { TestValidator } from "@nestia/e2e"; -import { - ILlmSchema, - IOpenApiSchemaError, - IResult, - OpenApi, -} from "@samchon/openapi"; -import { LlmSchemaComposer } from "@samchon/openapi/lib/composers/LlmSchemaComposer"; -import typia, { IJsonSchemaCollection } from "typia"; - -export const test_chatgpt_parameters_reference_escaped_description_of_name = - (): void => - validate_llm_parameters_reference_escaped_description_of_name("chatgpt"); - -export const test_claude_parameters_reference_escaped_description_of_name = - (): void => - validate_llm_parameters_reference_escaped_description_of_name("claude"); - -export const test_gemini_parameters_reference_escaped_description_of_name = - (): void => - validate_llm_parameters_reference_escaped_description_of_name("gemini"); - -export const test_llm_v30_parameters_reference_escaped_description_of_name = - (): void => - validate_llm_parameters_reference_escaped_description_of_name("3.0"); - -export const test_llm_v31_parameters_reference_escaped_description_of_name = - (): void => - validate_llm_parameters_reference_escaped_description_of_name("3.1"); - -const validate_llm_parameters_reference_escaped_description_of_name = < - Model extends ILlmSchema.Model, ->( - model: Model, -): void => { - const collection: IJsonSchemaCollection = - typia.json.schemas<[Something.INested.IDeep]>(); - const deep: ILlmSchema.IParameters = composeSchema(model)(collection); - TestValidator.predicate("description")( - () => !!deep.description?.includes("Something.INested.IDeep"), - ); -}; - -interface Something { - x: number; -} -namespace Something { - export interface INested { - y: number; - } - export namespace INested { - export interface IDeep { - z: number; - } - } -} - -const composeSchema = - (model: Model) => - (collection: IJsonSchemaCollection): ILlmSchema.IParameters => { - const result: IResult< - ILlmSchema.IParameters, - IOpenApiSchemaError - > = LlmSchemaComposer.parameters(model)({ - components: collection.components, - schema: typia.assert< - OpenApi.IJsonSchema.IObject | OpenApi.IJsonSchema.IReference - >(collection.schemas[0]), - config: { - ...LlmSchemaComposer.defaultConfig(model), - reference: false, - } satisfies ILlmSchema.IConfig as any, - }) as IResult, IOpenApiSchemaError>; - if (result.success === false) throw new Error("Invalid schema"); - return result.value; - }; diff --git a/test/src/features/llm/parameters/validate_llm_parameters_separate_array.ts b/test/src/features/llm/parameters/validate_llm_parameters_separate_array.ts deleted file mode 100644 index 12be9f80..00000000 --- a/test/src/features/llm/parameters/validate_llm_parameters_separate_array.ts +++ /dev/null @@ -1,123 +0,0 @@ -import { TestValidator } from "@nestia/e2e"; -import { - ILlmSchema, - IOpenApiSchemaError, - IResult, - OpenApi, -} from "@samchon/openapi"; -import { LlmSchemaComposer } from "@samchon/openapi/lib/composers/LlmSchemaComposer"; -import typia, { IJsonSchemaCollection, tags } from "typia"; - -export const test_chatgpt_parameters_separate_array = (): void => - validate_llm_parameters_separate_array("chatgpt", false); - -export const test_claude_parameters_separate_array = (): void => - validate_llm_parameters_separate_array("claude", true); - -export const test_gemini_parameters_separate_array = (): void => - validate_llm_parameters_separate_array("gemini", false); - -export const test_llm_v30_parameters_separate_array = (): void => { - validate_llm_parameters_separate_array("3.0", false); - validate_llm_parameters_separate_array("3.0", true); -}; - -export const test_llm_v31_parameters_separate_array = (): void => { - validate_llm_parameters_separate_array("3.1", false); - validate_llm_parameters_separate_array("3.1", true); -}; - -const validate_llm_parameters_separate_array = ( - model: Model, - constraint: boolean, -): void => { - const separator = (schema: ILlmSchema.IParameters) => - LlmSchemaComposer.separateParameters(model)({ - predicate: (s) => - LlmSchemaComposer.typeChecker(model).isString( - s as OpenApi.IJsonSchema.IString, - ) && - (constraint - ? (s as OpenApi.IJsonSchema.IString).contentMediaType !== undefined - : (s as OpenApi.IJsonSchema.IString).description?.includes( - "@contentMediaType", - ) === true), - parameters: schema as any, - }); - const member: ILlmSchema.IParameters = schema( - model, - constraint, - )(typia.json.schemas<[IManagement]>()); - const upload: ILlmSchema.IParameters = schema( - model, - constraint, - )(typia.json.schemas<[IManagement]>()); - const combined: ILlmSchema.IParameters = schema( - model, - constraint, - )(typia.json.schemas<[IManagement]>()); - - TestValidator.equals( - "member", - (key) => key !== "description", - )(separator(member))({ - llm: member, - human: null, - }); - TestValidator.equals( - "upload", - (key) => key !== "description", - )(separator(upload))({ - llm: { - type: "object", - properties: {}, - additionalProperties: false, - required: [], - $defs: {}, - }, - human: upload, - }); - TestValidator.equals( - "combined", - (key) => key !== "description", - )(separator(combined))({ - llm: member, - human: upload, - }); -}; - -interface IManagement { - profiles: T[]; -} -interface IMember { - id: number; - name: string; -} -interface IFileUpload { - file: string & tags.ContentMediaType<"image/png">; -} -interface ICombined extends IMember, IFileUpload {} - -const schema = - (model: Model, constraint: boolean) => - (collection: IJsonSchemaCollection): ILlmSchema.IParameters => { - const result: IResult< - ILlmSchema.IParameters, - IOpenApiSchemaError - > = LlmSchemaComposer.parameters(model)({ - components: collection.components, - schema: typia.assert< - OpenApi.IJsonSchema.IObject | OpenApi.IJsonSchema.IReference - >(collection.schemas[0]), - config: { - ...LlmSchemaComposer.defaultConfig(model), - reference: false, - constraint, - } satisfies ILlmSchema.IConfig as any, - }) as IResult, IOpenApiSchemaError>; - if (result.success === false) { - console.log(result.error); - throw new Error("Invalid schema"); - } - return result.value; - }; diff --git a/test/src/features/llm/parameters/validate_llm_parameters_separate_nested.ts b/test/src/features/llm/parameters/validate_llm_parameters_separate_nested.ts deleted file mode 100644 index 5933a288..00000000 --- a/test/src/features/llm/parameters/validate_llm_parameters_separate_nested.ts +++ /dev/null @@ -1,129 +0,0 @@ -import { TestValidator } from "@nestia/e2e"; -import { - ILlmSchema, - IOpenApiSchemaError, - IResult, - OpenApi, -} from "@samchon/openapi"; -import { LlmSchemaComposer } from "@samchon/openapi/lib/composers/LlmSchemaComposer"; -import typia, { IJsonSchemaCollection, tags } from "typia"; - -export const test_chatgpt_parameters_separate_nested = (): void => - validate_llm_parameters_separate_nested("chatgpt", false); - -export const test_claude_parameters_separate_nested = (): void => - validate_llm_parameters_separate_nested("claude", true); - -export const test_gemini_parameters_separate_nested = (): void => - validate_llm_parameters_separate_nested("gemini", false); - -export const test_llm_v30_parameters_separate_nested = (): void => { - validate_llm_parameters_separate_nested("3.0", false); - validate_llm_parameters_separate_nested("3.0", true); -}; - -export const test_llm_v31_parameters_separate_nested = (): void => { - validate_llm_parameters_separate_nested("3.1", false); - validate_llm_parameters_separate_nested("3.1", true); -}; - -const validate_llm_parameters_separate_nested = < - Model extends ILlmSchema.Model, ->( - model: Model, - constraint: boolean, -): void => { - const separator = (schema: ILlmSchema.IParameters) => - LlmSchemaComposer.separateParameters(model)({ - predicate: (s) => - LlmSchemaComposer.typeChecker(model).isString( - s as OpenApi.IJsonSchema.IString, - ) && - (constraint - ? (s as OpenApi.IJsonSchema.IString).contentMediaType !== undefined - : (s as OpenApi.IJsonSchema.IString).description?.includes( - "@contentMediaType", - ) === true), - parameters: schema as any, - }); - const member: ILlmSchema.IParameters = schema( - model, - constraint, - )(typia.json.schemas<[INested]>()); - const upload: ILlmSchema.IParameters = schema( - model, - constraint, - )(typia.json.schemas<[INested]>()); - const combined: ILlmSchema.IParameters = schema( - model, - constraint, - )(typia.json.schemas<[INested]>()); - - TestValidator.equals( - "member", - (key) => key !== "description", - )(separator(member))({ - llm: member, - human: null, - }); - TestValidator.equals( - "upload", - (key) => key !== "description", - )(separator(upload))({ - llm: { - type: "object", - properties: {}, - additionalProperties: false, - required: [], - $defs: {}, - }, - human: upload, - }); - TestValidator.equals( - "combined", - (key) => key !== "description", - )(separator(combined))({ - llm: member, - human: upload, - }); -}; - -interface INested { - first: { - second: { - third: { - fourth: T; - }; - array: T[]; - }; - }; -} -interface IMember { - id: number; - name: string; -} -interface IFileUpload { - file: string & tags.Format<"uri"> & tags.ContentMediaType<"image/png">; -} -interface ICombined extends IMember, IFileUpload {} - -const schema = - (model: Model, constraint: boolean) => - (collection: IJsonSchemaCollection): ILlmSchema.IParameters => { - const result: IResult< - ILlmSchema.IParameters, - IOpenApiSchemaError - > = LlmSchemaComposer.parameters(model)({ - components: collection.components, - schema: typia.assert< - OpenApi.IJsonSchema.IObject | OpenApi.IJsonSchema.IReference - >(collection.schemas[0]), - config: { - ...LlmSchemaComposer.defaultConfig(model), - reference: false, - constraint, - } satisfies ILlmSchema.IConfig as any, - }) as IResult, IOpenApiSchemaError>; - if (result.success === false) throw new Error("Invalid schema"); - return result.value; - }; diff --git a/test/src/features/llm/parameters/validate_llm_parameters_separate_object.ts b/test/src/features/llm/parameters/validate_llm_parameters_separate_object.ts deleted file mode 100644 index 7fc65085..00000000 --- a/test/src/features/llm/parameters/validate_llm_parameters_separate_object.ts +++ /dev/null @@ -1,119 +0,0 @@ -import { TestValidator } from "@nestia/e2e"; -import { - ILlmSchema, - IOpenApiSchemaError, - IResult, - OpenApi, -} from "@samchon/openapi"; -import { LlmSchemaComposer } from "@samchon/openapi/lib/composers/LlmSchemaComposer"; -import typia, { IJsonSchemaCollection, tags } from "typia"; - -export const test_chatgpt_parameters_separate_object = (): void => - validate_llm_parameters_separate_object("chatgpt", false); - -export const test_claude_parameters_separate_object = (): void => - validate_llm_parameters_separate_object("claude", true); - -export const test_gemini_parameters_separate_object = (): void => - validate_llm_parameters_separate_object("gemini", false); - -export const test_llm_v30_parameters_separate_object = (): void => { - validate_llm_parameters_separate_object("3.0", false); - validate_llm_parameters_separate_object("3.1", false); -}; - -export const test_llm_v31_parameters_separate_object = (): void => { - validate_llm_parameters_separate_object("3.0", true); - validate_llm_parameters_separate_object("3.1", true); -}; - -const validate_llm_parameters_separate_object = < - Model extends ILlmSchema.Model, ->( - model: Model, - constraint: boolean, -): void => { - const separator = (schema: ILlmSchema.IParameters) => - LlmSchemaComposer.separateParameters(model)({ - predicate: (s) => - LlmSchemaComposer.typeChecker(model).isString( - s as OpenApi.IJsonSchema.IString, - ) && - (constraint - ? (s as OpenApi.IJsonSchema.IString).contentMediaType !== undefined - : (s as OpenApi.IJsonSchema.IString).description?.includes( - "@contentMediaType", - ) === true), - parameters: schema as any, - }); - const member: ILlmSchema.IParameters = schema( - model, - constraint, - )(typia.json.schemas<[IMember]>()); - const upload: ILlmSchema.IParameters = schema( - model, - constraint, - )(typia.json.schemas<[IFileUpload]>()); - const combined: ILlmSchema.IParameters = schema( - model, - constraint, - )(typia.json.schemas<[ICombined]>()); - - TestValidator.equals( - "member", - (key) => key !== "description", - )(separator(member))({ - llm: member, - human: null, - }); - TestValidator.equals( - "upload", - (key) => key !== "description", - )(separator(upload))({ - llm: { - type: "object", - properties: {}, - additionalProperties: false, - required: [], - $defs: {}, - }, - human: upload, - }); - TestValidator.equals( - "combined", - (key) => key !== "description", - )(separator(combined))({ - llm: member, - human: upload, - }); -}; - -interface IMember { - id: number; - name: string; -} -interface IFileUpload { - file: string & tags.Format<"uri"> & tags.ContentMediaType<"image/png">; -} -interface ICombined extends IMember, IFileUpload {} - -const schema = - (model: Model, constraint: boolean) => - (collection: IJsonSchemaCollection): ILlmSchema.IParameters => { - const result: IResult< - ILlmSchema.IParameters, - IOpenApiSchemaError - > = LlmSchemaComposer.parameters(model)({ - components: collection.components, - schema: typia.assert< - OpenApi.IJsonSchema.IObject | OpenApi.IJsonSchema.IReference - >(collection.schemas[0]), - config: { - ...LlmSchemaComposer.defaultConfig(model), - reference: false, - constraint, - } satisfies ILlmSchema.IConfig as any, - }) as IResult, IOpenApiSchemaError>; - if (result.success === false) throw new Error("Invalid schema"); - return result.value; - }; diff --git a/test/src/features/llm/parameters/validate_llm_parameters_separate_object_additionalProperties.ts b/test/src/features/llm/parameters/validate_llm_parameters_separate_object_additionalProperties.ts deleted file mode 100644 index de44d67a..00000000 --- a/test/src/features/llm/parameters/validate_llm_parameters_separate_object_additionalProperties.ts +++ /dev/null @@ -1,150 +0,0 @@ -import { TestValidator } from "@nestia/e2e"; -import { - ILlmSchema, - IOpenApiSchemaError, - IResult, - OpenApi, -} from "@samchon/openapi"; -import { LlmSchemaComposer } from "@samchon/openapi/lib/composers/LlmSchemaComposer"; -import typia, { IJsonSchemaCollection, tags } from "typia"; - -export const test_chatgpt_parameters_separate_object_additionalProperties = - (): void => - validate_llm_parameters_separate_object_additionalProperties( - "chatgpt", - false, - ); - -export const test_claude_parameters_separate_object_additionalProperties = - (): void => - validate_llm_parameters_separate_object_additionalProperties( - "claude", - true, - ); - -export const test_gemini_parameters_separate_object_additionalProperties = - (): void => - TestValidator.error("Geimini does not support additionalProperties")(() => - validate_llm_parameters_separate_object_additionalProperties( - "gemini", - false, - ), - ); - -export const test_llm_v30_parameters_separate_object_additionalProperties = - (): void => { - validate_llm_parameters_separate_object_additionalProperties("3.0", false); - validate_llm_parameters_separate_object_additionalProperties("3.0", true); - }; - -export const test_llm_v31_parameters_separate_object_additionalProperties = - (): void => { - validate_llm_parameters_separate_object_additionalProperties("3.1", false); - validate_llm_parameters_separate_object_additionalProperties("3.1", true); - }; - -const validate_llm_parameters_separate_object_additionalProperties = < - Model extends ILlmSchema.Model, ->( - model: Model, - constraint: boolean, -): void => { - const separator = (schema: ILlmSchema.IParameters) => - LlmSchemaComposer.separateParameters(model)({ - predicate: (s) => - LlmSchemaComposer.typeChecker(model).isString( - s as OpenApi.IJsonSchema.IString, - ) && - (constraint - ? (s as OpenApi.IJsonSchema.IString).contentMediaType !== undefined - : (s as OpenApi.IJsonSchema.IString).description?.includes( - "@contentMediaType", - ) === true), - parameters: schema as any, - }); - const params: ILlmSchema.IParameters = schema( - model, - constraint, - )(typia.json.schemas<[IParameters]>()); - TestValidator.equals( - model, - (key) => key !== "description", - )(separator(params))({ - llm: schema( - model, - constraint, - )( - typia.json.schemas< - [ - { - input: { - email: string; - hobbies: Record< - string, - { - id: string; - name: string; - } - >; - }; - }, - ] - >(), - ), - human: schema( - model, - constraint, - )( - typia.json.schemas< - [ - { - input: { - hobbies: Record< - string, - { - thumbnail: string & - tags.Format<"uri"> & - tags.ContentMediaType<"image/*">; - } - >; - }; - }, - ] - >(), - ), - }); -}; - -interface IParameters { - input: IMember; -} -interface IMember { - email: string; - hobbies: Record; -} -interface IHobby { - id: string; - name: string; - thumbnail: string & tags.Format<"uri"> & tags.ContentMediaType<"image/*">; -} - -const schema = - (model: Model, constraint: boolean) => - (collection: IJsonSchemaCollection): ILlmSchema.IParameters => { - const result: IResult< - ILlmSchema.IParameters, - IOpenApiSchemaError - > = LlmSchemaComposer.parameters(model)({ - components: collection.components, - schema: typia.assert< - OpenApi.IJsonSchema.IObject | OpenApi.IJsonSchema.IReference - >(collection.schemas[0]), - config: { - ...LlmSchemaComposer.defaultConfig(model), - reference: false, - constraint, - } satisfies ILlmSchema.IConfig as any, - }) as IResult, IOpenApiSchemaError>; - if (result.success === false) throw new Error("Invalid schema"); - return result.value; - }; diff --git a/test/src/features/llm/parameters/validate_llm_parameters_separate_ref.ts b/test/src/features/llm/parameters/validate_llm_parameters_separate_ref.ts deleted file mode 100644 index 09e06061..00000000 --- a/test/src/features/llm/parameters/validate_llm_parameters_separate_ref.ts +++ /dev/null @@ -1,142 +0,0 @@ -import { TestValidator } from "@nestia/e2e"; -import { - IChatGptSchema, - ILlmSchema, - IOpenApiSchemaError, - IResult, - OpenApi, -} from "@samchon/openapi"; -import { LlmSchemaComposer } from "@samchon/openapi/lib/composers/LlmSchemaComposer"; -import typia, { IJsonSchemaCollection, tags } from "typia"; - -export const test_chatgpt_parameters_separate_ref = (): void => - validate_llm_parameters_separate_ref("chatgpt", false); - -export const test_claude_parameters_separate_ref = (): void => - validate_llm_parameters_separate_ref("claude", true); - -export const test_llm_v31_parameters_separate_ref = (): void => { - validate_llm_parameters_separate_ref("3.1", false); - validate_llm_parameters_separate_ref("3.1", true); -}; - -const validate_llm_parameters_separate_ref = < - Model extends Exclude, ->( - model: Model, - constraint: boolean, -): void => { - const separator = (schema: ILlmSchema.IParameters) => - LlmSchemaComposer.separateParameters(model)({ - predicate: (s) => - LlmSchemaComposer.typeChecker(model).isString( - s as OpenApi.IJsonSchema.IString, - ) && - (constraint - ? (s as OpenApi.IJsonSchema.IString).contentMediaType !== undefined - : (s as OpenApi.IJsonSchema.IString).description?.includes( - "@contentMediaType", - ) === true), - parameters: schema as any, - }); - const member: ILlmSchema.IParameters = schema( - model, - constraint, - )(typia.json.schemas<[IWrapper]>()); - const upload: ILlmSchema.IParameters = schema( - model, - constraint, - )(typia.json.schemas<[IWrapper]>()); - const combined: ILlmSchema.IParameters = schema( - model, - constraint, - )(typia.json.schemas<[IWrapper]>()); - - TestValidator.equals( - "member", - (key) => key !== "description", - )(separator(member))({ - llm: member, - human: null, - }); - TestValidator.equals( - "upload", - (key) => key !== "description", - )(separator(upload))({ - llm: { - type: "object", - properties: {}, - additionalProperties: false, - required: [], - $defs: {}, - }, - human: upload, - }); - TestValidator.equals( - "combined", - (key) => key !== "description", - )({ - llm: separator(combined).llm - ? { ...separator(combined).llm, $defs: {} } - : null, - human: separator(combined).human - ? { ...separator(combined).human, $defs: {} } - : null, - })({ - llm: { - $defs: {}, - type: "object", - properties: { - value: { - $ref: "#/$defs/ICombined.Llm", - }, - }, - required: ["value"], - additionalProperties: false, - } satisfies IChatGptSchema.IParameters, - human: { - $defs: {}, - type: "object", - properties: { - value: { - $ref: "#/$defs/ICombined.Human", - }, - }, - required: ["value"], - additionalProperties: false, - } satisfies IChatGptSchema.IParameters, - }); -}; - -interface IWrapper { - value: T; -} -interface IMember { - id: number; - name: string; -} -interface IFileUpload { - file: string & tags.Format<"uri"> & tags.ContentMediaType<"image/png">; -} -interface ICombined extends IMember, IFileUpload {} - -const schema = - (model: Model, constraint: boolean) => - (collection: IJsonSchemaCollection): ILlmSchema.IParameters => { - const result: IResult< - ILlmSchema.IParameters, - IOpenApiSchemaError - > = LlmSchemaComposer.parameters(model)({ - components: collection.components, - schema: typia.assert< - OpenApi.IJsonSchema.IObject | OpenApi.IJsonSchema.IReference - >(collection.schemas[0]), - config: { - ...LlmSchemaComposer.defaultConfig(model), - reference: true, - constraint, - } satisfies ILlmSchema.IConfig as any, - }) as IResult, IOpenApiSchemaError>; - if (result.success === false) throw new Error("Invalid schema"); - return result.value; - }; diff --git a/test/src/features/llm/parameters/validate_llm_parameters_separate_validate.ts b/test/src/features/llm/parameters/validate_llm_parameters_separate_validate.ts deleted file mode 100644 index 715e392f..00000000 --- a/test/src/features/llm/parameters/validate_llm_parameters_separate_validate.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { TestValidator } from "@nestia/e2e"; -import { - ILlmFunction, - ILlmSchema, - IOpenApiSchemaError, - IResult, - OpenApi, - OpenApiTypeChecker, -} from "@samchon/openapi"; -import { LlmSchemaComposer } from "@samchon/openapi/lib/composers/LlmSchemaComposer"; -import typia from "typia"; - -export const test_chatgpt_parameters_separate_validate = (): void => - validate_llm_parameters_separate_validate("chatgpt"); - -export const test_claude_parameters_separate_validate = (): void => - validate_llm_parameters_separate_validate("claude"); - -export const test_gemini_parameters_separate_validate = (): void => - validate_llm_parameters_separate_validate("gemini"); - -export const test_llm_v30_parameters_separate_validate = (): void => - validate_llm_parameters_separate_validate("3.0"); - -export const test_llm_v31_parameters_separate_validate = (): void => - validate_llm_parameters_separate_validate("3.1"); - -const validate_llm_parameters_separate_validate = < - Model extends ILlmSchema.Model, ->( - model: Model, -): void => { - const collection = typia.json.schemas<[ISeparatable, IHumanOnly]>(); - const validate = (schema: OpenApi.IJsonSchema, exists: boolean) => { - const result: IResult< - ILlmSchema.IParameters, - IOpenApiSchemaError - > = LlmSchemaComposer.parameters(model)({ - $defs: {}, - components: collection.components, - schema: schema as OpenApi.IJsonSchema.IReference, - config: LlmSchemaComposer.defaultConfig(model), - } as any) as IResult, IOpenApiSchemaError>; - if (result.success === false) throw new Error("Failed to convert"); - - const separated: ILlmFunction.ISeparated = - LlmSchemaComposer.separateParameters(model)({ - parameters: result.value as ILlmSchema.IParameters, - predicate: (s: OpenApi.IJsonSchema) => OpenApiTypeChecker.isNumber(s), - } as any) as ILlmFunction.ISeparated; - TestValidator.equals( - "validate", - (key) => key !== "description", - )(!!separated.validate)(exists); - }; - validate(collection.schemas[0], true); - validate(collection.schemas[1], false); -}; - -interface ISeparatable { - title: string; - value: number; -} -interface IHumanOnly { - value: number; -} diff --git a/test/src/features/llm/chatgpt/test_chatgpt_schema_discriminator.ts b/test/src/features/llm/schema/test_llm_schema_discriminator.ts similarity index 69% rename from test/src/features/llm/chatgpt/test_chatgpt_schema_discriminator.ts rename to test/src/features/llm/schema/test_llm_schema_discriminator.ts index eec35b61..d0e7460d 100644 --- a/test/src/features/llm/chatgpt/test_chatgpt_schema_discriminator.ts +++ b/test/src/features/llm/schema/test_llm_schema_discriminator.ts @@ -1,31 +1,28 @@ import { TestValidator } from "@nestia/e2e"; import { - ChatGptTypeChecker, - IChatGptSchema, + ILlmSchema, IOpenApiSchemaError, IResult, + LlmTypeChecker, OpenApi, OpenApiTypeChecker, } from "@samchon/openapi"; -import { ChatGptSchemaComposer } from "@samchon/openapi/lib/composers/llm/ChatGptSchemaComposer"; +import { LlmSchemaComposer } from "@samchon/openapi/lib/composers/LlmSchemaComposer"; import typia, { IJsonSchemaUnit } from "typia"; -export const test_chatgpt_schema_discriminator = (): void => { - const $defs: Record = {}; +export const test_llm_schema_discriminator = (): void => { + const $defs: Record = {}; const unit: IJsonSchemaUnit = typia.json.schema(); - const result: IResult = - ChatGptSchemaComposer.schema({ - config: { - reference: true, - }, - $defs: {}, + const result: IResult = + LlmSchemaComposer.schema({ + $defs, components: unit.components, schema: unit.schema, }); if (result.success === false) throw new Error("Failed to transform"); TestValidator.predicate("discriminator")( () => - ChatGptTypeChecker.isAnyOf(result.value) && + LlmTypeChecker.isAnyOf(result.value) && result.value["x-discriminator"] !== undefined && result.value["x-discriminator"].mapping !== undefined && Object.values(result.value["x-discriminator"].mapping).every((k) => @@ -33,7 +30,7 @@ export const test_chatgpt_schema_discriminator = (): void => { ), ); - const invert: OpenApi.IJsonSchema = ChatGptSchemaComposer.invert({ + const invert: OpenApi.IJsonSchema = LlmSchemaComposer.invert({ components: {}, $defs, schema: result.value, diff --git a/test/src/features/llm/schema/test_llm_schema_enum.ts b/test/src/features/llm/schema/test_llm_schema_enum.ts new file mode 100644 index 00000000..f91f30dc --- /dev/null +++ b/test/src/features/llm/schema/test_llm_schema_enum.ts @@ -0,0 +1,38 @@ +import { TestValidator } from "@nestia/e2e"; +import { + ILlmSchema, + IOpenApiSchemaError, + IResult, + OpenApi, +} from "@samchon/openapi"; +import { LlmSchemaComposer } from "@samchon/openapi/lib/composers/LlmSchemaComposer"; +import typia, { IJsonSchemaCollection } from "typia"; + +export const test_llm_schema_enum = (): void => { + const collection: IJsonSchemaCollection = typia.json.schemas<[IBbsArticle]>(); + const result: IResult = + LlmSchemaComposer.parameters({ + components: collection.components, + schema: collection.schemas[0] as + | OpenApi.IJsonSchema.IObject + | OpenApi.IJsonSchema.IReference, + config: { + reference: false, + }, + }); + TestValidator.equals("success")(result.success)(true); + if (result.success === false) return; + + const formatted: ILlmSchema.IParameters = result.value; + const formatProp = formatted.properties.format as ILlmSchema.IString; + TestValidator.equals("enum")(formatProp.enum)(["html", "md", "txt"]); +}; + +interface IBbsArticle { + format: IBbsArticle.Format; + // title: string; + // body: string; +} +namespace IBbsArticle { + export type Format = "html" | "md" | "txt"; +} diff --git a/test/src/features/llm/schema/validate_llm_schema_enum_reference.ts b/test/src/features/llm/schema/test_llm_schema_enum_reference.ts similarity index 53% rename from test/src/features/llm/schema/validate_llm_schema_enum_reference.ts rename to test/src/features/llm/schema/test_llm_schema_enum_reference.ts index 7c774f88..3c2e315d 100644 --- a/test/src/features/llm/schema/validate_llm_schema_enum_reference.ts +++ b/test/src/features/llm/schema/test_llm_schema_enum_reference.ts @@ -7,20 +7,7 @@ import { } from "@samchon/openapi"; import { LlmSchemaComposer } from "@samchon/openapi/lib/composers/LlmSchemaComposer"; -export const test_chatgpt_schema_enum_reference = (): void => - validate_llm_schema_enum_reference("chatgpt"); - -export const test_gemini_schema_enum_reference = (): void => - validate_llm_schema_enum_reference("gemini"); - -export const test_llm_v30_schema_enum_reference = (): void => - validate_llm_schema_enum_reference("3.0"); - -const validate_llm_schema_enum_reference = < - Model extends "chatgpt" | "gemini" | "3.0", ->( - model: Model, -): void => { +export const test_llm_schema_enum_reference = (): void => { const components: OpenApi.IComponents = { schemas: { named: { @@ -46,18 +33,15 @@ const validate_llm_schema_enum_reference = < ], }; - const result: IResult< - ILlmSchema, - IOpenApiSchemaError - > = LlmSchemaComposer.schema(model)({ - components, - schema, - config: { - ...LlmSchemaComposer.defaultConfig(model), - reference: false, - } as any, - $defs: {}, - }) as IResult, IOpenApiSchemaError>; + const result: IResult = + LlmSchemaComposer.schema({ + components, + schema, + $defs: {}, + config: { + reference: false, + }, + }); TestValidator.equals( "success", (key) => key === "description", diff --git a/test/src/features/llm/schema/validate_llm_schema_invert.ts b/test/src/features/llm/schema/test_llm_schema_invert.ts similarity index 64% rename from test/src/features/llm/schema/validate_llm_schema_invert.ts rename to test/src/features/llm/schema/test_llm_schema_invert.ts index 896f1855..2406984d 100644 --- a/test/src/features/llm/schema/validate_llm_schema_invert.ts +++ b/test/src/features/llm/schema/test_llm_schema_invert.ts @@ -1,40 +1,23 @@ import { TestValidator } from "@nestia/e2e"; -import { ILlmSchema } from "@samchon/openapi"; +import { ILlmSchema, IOpenApiSchemaError, IResult } from "@samchon/openapi"; import { LlmSchemaComposer } from "@samchon/openapi/lib/composers/LlmSchemaComposer"; import typia, { IJsonSchemaUnit, tags } from "typia"; -export const test_chatgpt_schema_invert = () => - validate_llm_schema_invert("chatgpt"); - -export const test_claude_schema_invert = () => - validate_llm_schema_invert("claude"); - -export const test_gemini_schema_invert = () => - validate_llm_schema_invert("gemini"); - -export const test_llm_v30_schema_invert = () => - validate_llm_schema_invert("3.0"); - -export const test_llm_v31_schema_invert = () => - validate_llm_schema_invert("3.1"); - -const validate_llm_schema_invert = ( - model: Model, -) => { +export const test_llm_schema_invert = (): void => { const assert = (title: string, unit: IJsonSchemaUnit): void => { - const result = LlmSchemaComposer.schema(model)({ - config: LlmSchemaComposer.defaultConfig(model) as any, - components: unit.components, - schema: unit.schema, - $defs: {}, - }); + const result: IResult = + LlmSchemaComposer.schema({ + components: unit.components, + schema: unit.schema, + $defs: {}, + }); if (result.success === false) throw new Error("Failed to compose LLM schema."); - const inverted = LlmSchemaComposer.invert(model)({ + const inverted = LlmSchemaComposer.invert({ components: {}, $defs: {}, schema: result.value, - } as any); + }); TestValidator.equals(title, (key) => key === "description")(inverted)( unit.schema, ); diff --git a/test/src/features/llm/parameters/validate_llm_parameters_mismatch.ts b/test/src/features/llm/schema/test_llm_schema_mismatch.ts similarity index 54% rename from test/src/features/llm/parameters/validate_llm_parameters_mismatch.ts rename to test/src/features/llm/schema/test_llm_schema_mismatch.ts index fe12e982..3718de77 100644 --- a/test/src/features/llm/parameters/validate_llm_parameters_mismatch.ts +++ b/test/src/features/llm/schema/test_llm_schema_mismatch.ts @@ -8,24 +8,7 @@ import { import { LlmSchemaComposer } from "@samchon/openapi/lib/composers/LlmSchemaComposer"; import typia, { IJsonSchemaCollection } from "typia"; -export const test_chatgpt_parameters_mismatch = (): void => - validate_llm_parameters_mismatch("chatgpt"); - -export const test_claude_parameters_mismatch = (): void => - validate_llm_parameters_mismatch("claude"); - -export const test_gemini_parameters_mismatch = (): void => - validate_llm_parameters_mismatch("gemini"); - -export const test_llm_v30_parameters_mismatch = (): void => - validate_llm_parameters_mismatch("3.0"); - -export const test_llm_v31_parameters_mismatch = (): void => - validate_llm_parameters_mismatch("3.1"); - -const validate_llm_parameters_mismatch = ( - model: Model, -): void => { +export const test_llm_schema_mismatch = (): void => { const collection: IJsonSchemaCollection = typia.json.schemas< [ { @@ -45,19 +28,15 @@ const validate_llm_parameters_mismatch = ( p.second.properties.input.$ref = "#/components/schemas/ICircle1"; p.third.items.properties.nested.$ref = "#/components/schemas/IRectangle1"; - const result: IResult< - ILlmSchema.IParameters, - IOpenApiSchemaError - > = LlmSchemaComposer.parameters(model)({ - accessor: "$input", - config: LlmSchemaComposer.defaultConfig( - model, - ) satisfies ILlmSchema.IConfig as any, - components: collection.components, - schema: typia.assert< - OpenApi.IJsonSchema.IReference | OpenApi.IJsonSchema.IObject - >(collection.schemas[0]), - }) as IResult, IOpenApiSchemaError>; + const result: IResult = + LlmSchemaComposer.schema({ + accessor: "$input", + components: collection.components, + schema: typia.assert< + OpenApi.IJsonSchema.IReference | OpenApi.IJsonSchema.IObject + >(collection.schemas[0]), + $defs: {}, + }); TestValidator.equals("success")(result.success)(false); TestValidator.equals("errors")( result.success ? [] : result.error.reasons.map((r) => r.accessor).sort(), diff --git a/test/src/features/llm/schema/test_llm_schema_nullable.ts b/test/src/features/llm/schema/test_llm_schema_nullable.ts new file mode 100644 index 00000000..5dddb259 --- /dev/null +++ b/test/src/features/llm/schema/test_llm_schema_nullable.ts @@ -0,0 +1,28 @@ +import { TestValidator } from "@nestia/e2e"; +import { ILlmSchema, IOpenApiSchemaError, IResult } from "@samchon/openapi"; +import { LlmSchemaComposer } from "@samchon/openapi/lib/composers/LlmSchemaComposer"; +import typia, { IJsonSchemaCollection } from "typia"; + +export const test_llm_schema_nullable = (): void => { + const collection: IJsonSchemaCollection = + typia.json.schemas<[number | null]>(); + const result: IResult< + ILlmSchema, + IOpenApiSchemaError + > = LlmSchemaComposer.schema({ + components: collection.components, + schema: typia.assert(collection.schemas[0]), + $defs: {}, + }); + TestValidator.equals("success")(result.success)(true); + TestValidator.equals("nullable")(result.success ? result.value : {})({ + anyOf: [ + { + type: "null", + }, + { + type: "number", + }, + ], + }); +}; diff --git a/test/src/features/llm/schema/test_llm_schema_oneof.ts b/test/src/features/llm/schema/test_llm_schema_oneof.ts new file mode 100644 index 00000000..7e9f9ccd --- /dev/null +++ b/test/src/features/llm/schema/test_llm_schema_oneof.ts @@ -0,0 +1,50 @@ +import { TestValidator } from "@nestia/e2e"; +import { ILlmSchema, IOpenApiSchemaError, IResult } from "@samchon/openapi"; +import { LlmSchemaComposer } from "@samchon/openapi/lib/composers/LlmSchemaComposer"; +import typia, { IJsonSchemaCollection } from "typia"; + +export const test_llm_schema_oneof = (): void => { + const collection: IJsonSchemaCollection = + typia.json.schemas<[IPoint | ILine | ITriangle | IRectangle]>(); + + const $defs: Record = {}; + const result: IResult = + LlmSchemaComposer.schema({ + $defs, + components: collection.components, + schema: collection.schemas[0], + config: { + reference: false, + }, + }); + TestValidator.equals("success")(result.success); + TestValidator.equals("anyOf")(["point", "line", "triangle", "rectangle"])( + (result as any)?.value?.anyOf?.map( + (e: any) => e.properties?.type?.enum?.[0], + ), + ); +}; + +interface IPoint { + type: "point"; + x: number; + y: number; +} +interface ILine { + type: "line"; + p1: IPoint; + p2: IPoint; +} +interface ITriangle { + type: "triangle"; + p1: IPoint; + p2: IPoint; + p3: IPoint; +} +interface IRectangle { + type: "rectangle"; + p1: IPoint; + p2: IPoint; + p3: IPoint; + p4: IPoint; +} diff --git a/test/src/features/llm/schema/test_llm_schema_recursive_ref.ts b/test/src/features/llm/schema/test_llm_schema_recursive_ref.ts new file mode 100644 index 00000000..8f129e88 --- /dev/null +++ b/test/src/features/llm/schema/test_llm_schema_recursive_ref.ts @@ -0,0 +1,54 @@ +import { TestValidator } from "@nestia/e2e"; +import { ILlmSchema, IOpenApiSchemaError, IResult } from "@samchon/openapi"; +import { LlmSchemaComposer } from "@samchon/openapi/lib/composers/LlmSchemaComposer"; + +export const test_llm_schema_recursive_ref = (): void => { + const $defs: Record = {}; + const result: IResult = + LlmSchemaComposer.schema({ + $defs, + components: { + schemas: { + Department: { + type: "object", + properties: { + name: { + type: "string", + }, + children: { + type: "array", + items: { + $ref: "#/components/schemas/Department", + }, + }, + }, + required: ["name", "children"], + }, + }, + }, + schema: { + $ref: "#/components/schemas/Department", + }, + }); + TestValidator.equals("success")(result.success)(true); + TestValidator.equals("$defs")({ + Department: { + type: "object", + properties: { + name: { + type: "string", + }, + children: { + type: "array", + items: { + $ref: "#/$defs/Department", + }, + }, + }, + required: ["name", "children"], + }, + } satisfies Record as Record)($defs); + TestValidator.equals("schema")(result.success ? result.value : {})({ + $ref: "#/$defs/Department", + }); +}; diff --git a/test/src/features/llm/schema/test_llm_schema_reference_escaped_description_of_name.ts b/test/src/features/llm/schema/test_llm_schema_reference_escaped_description_of_name.ts new file mode 100644 index 00000000..71910b8c --- /dev/null +++ b/test/src/features/llm/schema/test_llm_schema_reference_escaped_description_of_name.ts @@ -0,0 +1,59 @@ +import { TestValidator } from "@nestia/e2e"; +import { + ILlmSchema, + IOpenApiSchemaError, + IResult, + OpenApi, +} from "@samchon/openapi"; +import { LlmSchemaComposer } from "@samchon/openapi/lib/composers/LlmSchemaComposer"; +import typia, { IJsonSchemaCollection } from "typia"; + +export const test_llm_schema_reference_escaped_description_of_name = + (): void => { + const collection: IJsonSchemaCollection = typia.json.schemas< + [ + { + deep: Something.INested.IDeep; + nested: Something.INested; + something: Something; + }, + ] + >(); + const schema: ILlmSchema.IParameters = composeSchema(collection); + const deep: ILlmSchema.IObject = schema.properties + .deep as ILlmSchema.IObject; + TestValidator.predicate("description")( + () => !!deep.description?.includes("Something.INested.IDeep"), + ); + }; + +interface Something { + x: number; +} +namespace Something { + export interface INested { + y: number; + } + export namespace INested { + export interface IDeep { + z: number; + } + } +} + +const composeSchema = ( + collection: IJsonSchemaCollection, +): ILlmSchema.IParameters => { + const result: IResult = + LlmSchemaComposer.parameters({ + components: collection.components, + schema: typia.assert< + OpenApi.IJsonSchema.IObject | OpenApi.IJsonSchema.IReference + >(collection.schemas[0]), + config: { + reference: false, + }, + }); + if (result.success === false) throw new Error("Invalid schema"); + return result.value; +}; diff --git a/test/src/features/llm/schema/test_llm_schema_reference_escaped_description_of_namespace.ts b/test/src/features/llm/schema/test_llm_schema_reference_escaped_description_of_namespace.ts new file mode 100644 index 00000000..25e7f97d --- /dev/null +++ b/test/src/features/llm/schema/test_llm_schema_reference_escaped_description_of_namespace.ts @@ -0,0 +1,69 @@ +import { TestValidator } from "@nestia/e2e"; +import { + ILlmSchema, + IOpenApiSchemaError, + IResult, + OpenApi, +} from "@samchon/openapi"; +import { LlmSchemaComposer } from "@samchon/openapi/lib/composers/LlmSchemaComposer"; +import typia, { IJsonSchemaCollection } from "typia"; + +export const test_llm_schema_reference_escaped_description_of_namespace = + (): void => { + const collection: IJsonSchemaCollection = typia.json.schemas< + [ + { + deep: Something.INested.IDeep; + nested: Something.INested; + something: Something; + }, + ] + >(); + const schema: ILlmSchema.IParameters = composeSchema(collection); + const deep: ILlmSchema = schema.properties.deep as ILlmSchema; + TestValidator.predicate("description")(() => { + const description: string | undefined = ( + deep as OpenApi.IJsonSchema.IObject + ).description; + return ( + !!description && + description.includes("Something interface") && + description.includes("Something nested interface") && + description.includes("Something nested and deep interface") + ); + }); + }; + +/** Something interface. */ +interface Something { + x: number; +} +namespace Something { + /** Something nested interface. */ + export interface INested { + y: number; + } + export namespace INested { + /** Something nested and deep interface. */ + export interface IDeep { + z: number; + } + } +} + +const composeSchema = ( + collection: IJsonSchemaCollection, +): ILlmSchema.IParameters => { + const result: IResult = + LlmSchemaComposer.parameters({ + components: collection.components, + schema: typia.assert< + OpenApi.IJsonSchema.IObject | OpenApi.IJsonSchema.IReference + >(collection.schemas[0]), + config: { + reference: false, + }, + }); + if (result.success === false) throw new Error("Invalid schema"); + return result.value; +}; diff --git a/test/src/features/llm/schema/test_llm_schema_reference_escaped_description_of_property.ts b/test/src/features/llm/schema/test_llm_schema_reference_escaped_description_of_property.ts new file mode 100644 index 00000000..9233410e --- /dev/null +++ b/test/src/features/llm/schema/test_llm_schema_reference_escaped_description_of_property.ts @@ -0,0 +1,42 @@ +import { TestValidator } from "@nestia/e2e"; +import { OpenApi } from "@samchon/openapi"; +import { LlmSchemaComposer } from "@samchon/openapi/lib/composers/LlmSchemaComposer"; +import typia, { IJsonSchemaCollection, tags } from "typia"; + +export const test_llm_schema_reference_escaped_description_of_property = + (): void => { + const collection: IJsonSchemaCollection = typia.json.schemas<[IMember]>(); + const result = LlmSchemaComposer.parameters({ + components: collection.components, + schema: collection.schemas[0]! as OpenApi.IJsonSchema.IReference, + config: { + strict: true, + }, + }); + if (result.success === false) + throw new Error("Failed to compose LLM schema."); + + TestValidator.equals("property description")( + result.value.properties.hobby.description, + )(undefined); + }; + +interface IMember { + id: string & tags.Format<"uuid">; + name: string; + age: number & + tags.Type<"uint32"> & + tags.Minimum<20> & + tags.ExclusiveMaximum<100>; + /** + * A hobby. + * + * The main hobby. + */ + hobby: IHobby; +} + +/** The hobby type. */ +interface IHobby { + name: string; +} diff --git a/test/src/features/llm/schema/test_llm_schema_separate_object_empty.ts b/test/src/features/llm/schema/test_llm_schema_separate_object_empty.ts new file mode 100644 index 00000000..b9fc7974 --- /dev/null +++ b/test/src/features/llm/schema/test_llm_schema_separate_object_empty.ts @@ -0,0 +1,35 @@ +import { TestValidator } from "@nestia/e2e"; +import { + ILlmSchema, + IOpenApiSchemaError, + IResult, + OpenApi, + OpenApiTypeChecker, +} from "@samchon/openapi"; +import { LlmSchemaComposer } from "@samchon/openapi/lib/composers/LlmSchemaComposer"; +import typia, { IJsonSchemaCollection } from "typia"; + +export const test_llm_schema_separate_object_empty = (): void => { + TestValidator.equals("separated")( + LlmSchemaComposer.separate({ + predicate: (schema: OpenApi.IJsonSchema) => + OpenApiTypeChecker.isInteger(schema), + parameters: schema(typia.json.schemas<[{}]>()), + }), + )({ + llm: schema(typia.json.schemas<[{}]>()), + human: null, + }); +}; + +const schema = (collection: IJsonSchemaCollection): ILlmSchema.IParameters => { + const result: IResult = + LlmSchemaComposer.parameters({ + components: collection.components, + schema: typia.assert< + OpenApi.IJsonSchema.IObject | OpenApi.IJsonSchema.IReference + >(collection.schemas[0]), + }); + if (result.success === false) throw new Error("Invalid schema"); + return result.value; +}; diff --git a/test/src/features/llm/schema/test_llm_schema_separate_string.ts b/test/src/features/llm/schema/test_llm_schema_separate_string.ts new file mode 100644 index 00000000..bcaf8980 --- /dev/null +++ b/test/src/features/llm/schema/test_llm_schema_separate_string.ts @@ -0,0 +1,63 @@ +import { TestValidator } from "@nestia/e2e"; +import { + ILlmSchema, + IOpenApiSchemaError, + IResult, + LlmTypeChecker, + OpenApi, +} from "@samchon/openapi"; +import { LlmSchemaComposer } from "@samchon/openapi/lib/composers/LlmSchemaComposer"; +import typia, { IJsonSchemaCollection, tags } from "typia"; + +export const test_llm_schema_separate_string = (): void => { + const separator = (schema: ILlmSchema.IParameters) => + LlmSchemaComposer.separate({ + predicate: (s) => + LlmTypeChecker.isString(s) && s.contentMediaType !== undefined, + parameters: schema, + }); + const plain: ILlmSchema.IParameters = schema( + typia.json.schemas< + [ + { + name: string; + }, + ] + >(), + ); + const upload: ILlmSchema.IParameters = schema( + typia.json.schemas< + [ + { + file: string & tags.ContentMediaType<"image/*">; + }, + ] + >(), + ); + TestValidator.equals("plain")(separator(plain))({ + llm: plain, + human: null, + }); + TestValidator.equals("upload")(separator(upload))({ + llm: { + type: "object", + properties: {}, + additionalProperties: false, + required: [], + $defs: {}, + }, + human: upload, + }); +}; + +const schema = (collection: IJsonSchemaCollection): ILlmSchema.IParameters => { + const result: IResult = + LlmSchemaComposer.parameters({ + components: collection.components, + schema: typia.assert< + OpenApi.IJsonSchema.IObject | OpenApi.IJsonSchema.IReference + >(collection.schemas[0]), + }); + if (result.success === false) throw new Error("Invalid schema"); + return result.value; +}; diff --git a/test/src/features/llm/chatgpt/test_chatgpt_schema_strict.ts b/test/src/features/llm/schema/test_llm_schema_strict_additionalProperties.ts similarity index 85% rename from test/src/features/llm/chatgpt/test_chatgpt_schema_strict.ts rename to test/src/features/llm/schema/test_llm_schema_strict_additionalProperties.ts index 000eb0b7..0f7f387a 100644 --- a/test/src/features/llm/chatgpt/test_chatgpt_schema_strict.ts +++ b/test/src/features/llm/schema/test_llm_schema_strict_additionalProperties.ts @@ -3,7 +3,7 @@ import { ILlmSchema, IOpenApiSchemaError, IResult } from "@samchon/openapi"; import { LlmSchemaComposer } from "@samchon/openapi/lib/composers/LlmSchemaComposer"; import typia, { IJsonSchemaCollection } from "typia"; -export const test_chatgpt_schema_strict = (): void => { +export const test_llm_schema_strict_additionalProperties = (): void => { const collection: IJsonSchemaCollection = typia.json.schemas< [ { @@ -17,15 +17,14 @@ export const test_chatgpt_schema_strict = (): void => { ] >(); const res: IResult = - LlmSchemaComposer.schema("chatgpt")({ - config: { - ...LlmSchemaComposer.defaultConfig("chatgpt"), - strict: true, - }, + LlmSchemaComposer.schema({ components: collection.components, schema: collection.schemas[0], $defs: {}, - } as any); + config: { + strict: true, + }, + }); TestValidator.equals("strict")({ type: "object", additionalProperties: false, diff --git a/test/src/features/llm/chatgpt/test_chatgpt_schema_reference_description.ts b/test/src/features/llm/schema/test_llm_schema_strict_description.ts similarity index 69% rename from test/src/features/llm/chatgpt/test_chatgpt_schema_reference_description.ts rename to test/src/features/llm/schema/test_llm_schema_strict_description.ts index b0528c67..c9c25b19 100644 --- a/test/src/features/llm/chatgpt/test_chatgpt_schema_reference_description.ts +++ b/test/src/features/llm/schema/test_llm_schema_strict_description.ts @@ -3,15 +3,14 @@ import { OpenApi } from "@samchon/openapi"; import { LlmSchemaComposer } from "@samchon/openapi/lib/composers/LlmSchemaComposer"; import typia, { IJsonSchemaCollection, tags } from "typia"; -export const test_chatgpt_schema_reference_description = () => { +export const test_llm_schema_strict_description = () => { const collection: IJsonSchemaCollection = typia.json.schemas<[IMember]>(); - const result = LlmSchemaComposer.parameters("chatgpt")({ + const result = LlmSchemaComposer.parameters({ + components: collection.components, + schema: collection.schemas[0]! as OpenApi.IJsonSchema.IReference, config: { - reference: true, strict: true, }, - components: collection.components, - schema: collection.schemas[0]! as OpenApi.IJsonSchema.IReference, }); TestValidator.predicate("type description")( result.success === true && @@ -24,19 +23,6 @@ export const test_chatgpt_schema_reference_description = () => { result.success === true && result.value.properties.hobby.description === undefined, ); - - const nonStrict = LlmSchemaComposer.parameters("chatgpt")({ - config: { - reference: true, - strict: false, - }, - components: collection.components, - schema: collection.schemas[0]! as OpenApi.IJsonSchema.IReference, - }); - TestValidator.equals("non-strict $ref description")( - nonStrict.success === true && - nonStrict.value.properties.hobby.description === "The hobby type.", - ); }; interface IMember { diff --git a/test/src/features/llm/schema/validate_llm_schema_tuple.ts b/test/src/features/llm/schema/test_llm_schema_tuple.ts similarity index 53% rename from test/src/features/llm/schema/validate_llm_schema_tuple.ts rename to test/src/features/llm/schema/test_llm_schema_tuple.ts index 3bf113bd..1ff5586d 100644 --- a/test/src/features/llm/schema/validate_llm_schema_tuple.ts +++ b/test/src/features/llm/schema/test_llm_schema_tuple.ts @@ -8,24 +8,7 @@ import { import { LlmSchemaComposer } from "@samchon/openapi/lib/composers/LlmSchemaComposer"; import typia, { IJsonSchemaCollection } from "typia"; -export const test_chatgpt_schema_tuple = (): void => - validate_llm_schema_tuple("chatgpt"); - -export const test_claude_schema_tuple = (): void => - validate_llm_schema_tuple("claude"); - -export const test_gemini_schema_tuple = (): void => - validate_llm_schema_tuple("gemini"); - -export const test_llm_v30_schema_tuple = (): void => - validate_llm_schema_tuple("3.0"); - -export const test_llm_v31_schema_tuple = (): void => - validate_llm_schema_tuple("3.1"); - -const validate_llm_schema_tuple = ( - model: Model, -): void => { +export const test_llm_schema_tuple = (): void => { const collection: IJsonSchemaCollection = typia.json.schemas< [ [string, number], @@ -44,7 +27,7 @@ const validate_llm_schema_tuple = ( }>, ] >(); - const v = validate(model)(collection.components); + const v = validate(collection.components); v(collection.schemas[0])(["$input"]); v(collection.schemas[1])([ `$input.properties["input"]`, @@ -56,22 +39,16 @@ const validate_llm_schema_tuple = ( }; const validate = - (model: Model) => (components: OpenApi.IComponents) => (schema: OpenApi.IJsonSchema) => (expected: string[]): void => { - const result: IResult< - ILlmSchema.IParameters, - IOpenApiSchemaError - > = LlmSchemaComposer.schema(model)({ - config: LlmSchemaComposer.defaultConfig( - model, - ) satisfies ILlmSchema.IConfig as any, - accessor: "$input", - components, - schema, - $defs: {}, - } as any) as IResult, IOpenApiSchemaError>; + const result: IResult = + LlmSchemaComposer.schema({ + accessor: "$input", + components, + schema, + $defs: {}, + }); TestValidator.equals("success")(result.success)(false); TestValidator.equals("errors")( result.success ? [] : result.error.reasons.map((r) => r.accessor).sort(), diff --git a/test/src/features/llm/schema/test_llm_type_checker_cover_any.ts b/test/src/features/llm/schema/test_llm_type_checker_cover_any.ts new file mode 100644 index 00000000..7c4cc749 --- /dev/null +++ b/test/src/features/llm/schema/test_llm_type_checker_cover_any.ts @@ -0,0 +1,34 @@ +import { TestValidator } from "@nestia/e2e"; +import { ILlmSchema, LlmTypeChecker, OpenApi } from "@samchon/openapi"; +import { LlmSchemaComposer } from "@samchon/openapi/lib/composers/LlmSchemaComposer"; +import typia, { IJsonSchemaCollection } from "typia"; + +export const test_llm_type_checker_cover_any = () => { + const collection: IJsonSchemaCollection = typia.json.schemas<[IBasic]>(); + const result = LlmSchemaComposer.parameters({ + components: collection.components, + schema: collection.schemas[0] as OpenApi.IJsonSchema.IReference, + }); + if (result.success === false) + throw new Error(`Failed to compose parameters.`); + + const parameters = result.value; + const check = (x: ILlmSchema, y: ILlmSchema): boolean => + LlmTypeChecker.covers({ + x, + y, + $defs: parameters.$defs, + }); + TestValidator.equals("any covers (string | null)")(true)( + check(parameters.properties.any, parameters.properties.string_or_null), + ); + TestValidator.equals("any covers (string | undefined)")(true)( + check(parameters.properties.any, parameters.properties.string_or_undefined), + ); +}; + +interface IBasic { + any: any; + string_or_null: null | string; + string_or_undefined: string | undefined; +} diff --git a/test/src/features/llm/schema/test_llm_type_checker_cover_array.ts b/test/src/features/llm/schema/test_llm_type_checker_cover_array.ts new file mode 100644 index 00000000..213d1cc0 --- /dev/null +++ b/test/src/features/llm/schema/test_llm_type_checker_cover_array.ts @@ -0,0 +1,173 @@ +import { TestValidator } from "@nestia/e2e"; +import { LlmTypeChecker, OpenApi } from "@samchon/openapi"; +import { LlmSchemaComposer } from "@samchon/openapi/lib/composers/LlmSchemaComposer"; +import typia, { IJsonSchemaCollection } from "typia"; + +export const test_llm_type_checker_cover_array = () => { + const collection: IJsonSchemaCollection = + typia.json.schemas<[Plan2D, Plan3D, Box2D, Box3D]>(); + const components: OpenApi.IComponents = collection.components as any; + const plan2D: OpenApi.IJsonSchema = components.schemas!.Plan2D; + const plan3D: OpenApi.IJsonSchema = components.schemas!.Plan3D; + const box2D: OpenApi.IJsonSchema = components.schemas!.Box2D; + const box3D: OpenApi.IJsonSchema = components.schemas!.Box3D; + + const $defs = {}; + const check = (x: OpenApi.IJsonSchema, y: OpenApi.IJsonSchema): boolean => { + const [a, b] = [x, y].map((schema) => { + const result = LlmSchemaComposer.schema({ + components: collection.components, + schema: schema, + $defs, + }); + if (result.success === false) + throw new Error(`Failed to compose schema.`); + return result.value; + }); + return LlmTypeChecker.covers({ + x: a, + y: b, + $defs, + }); + }; + + TestValidator.equals("Plan3D[] covers Plan2D[]")(true)( + check({ type: "array", items: plan3D }, { type: "array", items: plan2D }), + ); + TestValidator.equals("Box3D[] covers Box2D[]")(true)( + check({ type: "array", items: box3D }, { type: "array", items: box2D }), + ); + TestValidator.equals("Array covers Array")(true)( + check( + { + type: "array", + items: { + oneOf: [plan3D, box3D], + }, + }, + { + type: "array", + items: { + oneOf: [plan2D, box2D], + }, + }, + ), + ); + TestValidator.equals("(Plan3D|Box3D)[] covers (Plan2D|Box2D)[]")(true)( + check( + { + oneOf: [ + { type: "array", items: plan3D }, + { type: "array", items: box3D }, + ], + }, + { + oneOf: [ + { type: "array", items: plan2D }, + { type: "array", items: box2D }, + ], + }, + ), + ); + + TestValidator.equals("Plan2D[] can't cover Plan3D[]")(false)( + check({ type: "array", items: plan2D }, { type: "array", items: plan3D }), + ); + TestValidator.equals("Box2D[] can't cover Box3D[]")(false)( + check({ type: "array", items: box2D }, { type: "array", items: box3D }), + ); + TestValidator.equals("Array can't cover Array")( + false, + )( + check( + { + type: "array", + items: { + oneOf: [plan2D, box2D], + }, + }, + { + type: "array", + items: { + oneOf: [plan3D, box3D], + }, + }, + ), + ); + TestValidator.equals("(Plan2D[]|Box2D[]) can't cover (Plan3D[]|Box3D[])")( + false, + )( + check( + { + oneOf: [ + { type: "array", items: plan2D }, + { type: "array", items: box2D }, + ], + }, + { + oneOf: [ + { type: "array", items: plan3D }, + { type: "array", items: box3D }, + ], + }, + ), + ); + TestValidator.equals("Plan3D[] can't cover (Plan2D|Box2D)[]")(false)( + check( + { type: "array", items: plan3D }, + { + oneOf: [ + { type: "array", items: plan2D }, + { type: "array", items: box2D }, + ], + }, + ), + ); + TestValidator.equals("Box3D[] can't cover Array")(false)( + check( + { type: "array", items: box3D }, + { + type: "array", + items: { + oneOf: [plan2D, box2D], + }, + }, + ), + ); +}; + +type Plan2D = { + center: Point2D; + size: Point2D; + geometries: Geometry2D[]; +}; +type Plan3D = { + center: Point3D; + size: Point3D; + geometries: Geometry3D[]; +}; +type Geometry3D = { + position: Point3D; + scale: Point3D; +}; +type Geometry2D = { + position: Point2D; + scale: Point2D; +}; +type Point2D = { + x: number; + y: number; +}; +type Point3D = { + x: number; + y: number; + z: number; +}; +type Box2D = { + size: Point2D; + nested: Point2D[]; +}; +type Box3D = { + size: Point3D; + nested: Point3D[]; +}; diff --git a/test/src/features/llm/schema/validate_llm_schema_discriminator.ts b/test/src/features/llm/schema/validate_llm_schema_discriminator.ts deleted file mode 100644 index 8d017ae6..00000000 --- a/test/src/features/llm/schema/validate_llm_schema_discriminator.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { TestValidator } from "@nestia/e2e"; -import { - ILlmSchemaV3_1, - IOpenApiSchemaError, - IResult, - LlmTypeCheckerV3_1, - OpenApi, - OpenApiTypeChecker, -} from "@samchon/openapi"; -import { LlmSchemaComposer } from "@samchon/openapi/lib/composers/LlmSchemaComposer"; -import typia, { IJsonSchemaUnit } from "typia"; - -export const test_claude_schema_discriminator = (): void => - validate_llm_schema_discriminator("claude"); - -export const test_llama_v31_schema_discriminator = (): void => - validate_llm_schema_discriminator("3.1"); - -const validate_llm_schema_discriminator = (vendor: "claude" | "3.1"): void => { - const $defs: Record = {}; - const unit: IJsonSchemaUnit = typia.json.schema(); - const result: IResult = - LlmSchemaComposer.schema(vendor)({ - config: { - reference: true, - constraint: true, - }, - $defs, - components: unit.components, - schema: unit.schema, - }); - if (result.success === false) throw new Error("Failed to transform"); - TestValidator.predicate("discriminator")( - () => - LlmTypeCheckerV3_1.isOneOf(result.value) && - result.value.discriminator !== undefined && - result.value.discriminator.mapping !== undefined && - Object.values(result.value.discriminator.mapping).every((k) => - k.startsWith("#/$defs/"), - ), - ); - - const invert: OpenApi.IJsonSchema = LlmSchemaComposer.invert(vendor)({ - components: {}, - $defs, - schema: result.value, - }); - TestValidator.predicate("invert")( - () => - OpenApiTypeChecker.isOneOf(invert) && - invert.discriminator !== undefined && - invert.discriminator.mapping !== undefined && - Object.values(invert.discriminator.mapping).every((k) => - k.startsWith("#/components/schemas/"), - ), - ); -}; - -interface ICat { - type: "cat"; - name: string; - ribbon: boolean; -} -interface IAnt { - type: "ant"; - name: string; - role: "queen" | "soldier" | "worker"; -} diff --git a/test/src/features/llm/schema/validate_llm_schema_enum.ts b/test/src/features/llm/schema/validate_llm_schema_enum.ts deleted file mode 100644 index f8dc5c42..00000000 --- a/test/src/features/llm/schema/validate_llm_schema_enum.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { TestValidator } from "@nestia/e2e"; -import { - IGeminiSchema, - ILlmSchema, - IOpenApiSchemaError, - IResult, -} from "@samchon/openapi"; -import { LlmSchemaComposer } from "@samchon/openapi/lib/composers/LlmSchemaComposer"; -import typia, { IJsonSchemaCollection } from "typia"; - -export const test_chatgpt_schema_enum = (): void => - validate_llm_schema_enum("chatgpt"); - -export const test_gemini_schema_enum = (): void => - validate_llm_schema_enum("gemini"); - -export const test_llm_v30_schema_enum = (): void => - validate_llm_schema_enum("3.0"); - -const validate_llm_schema_enum = ( - model: Model, -): void => { - const collection: IJsonSchemaCollection = typia.json.schemas<[IBbsArticle]>(); - const result: IResult< - ILlmSchema, - IOpenApiSchemaError - > = LlmSchemaComposer.schema(model)({ - components: collection.components, - schema: collection.schemas[0], - config: { - ...LlmSchemaComposer.defaultConfig(model), - reference: false, - } as any, - $defs: {}, - }) as IResult, IOpenApiSchemaError>; - TestValidator.equals("success")(result.success); - TestValidator.equals("enum")( - typia.assert( - typia.assert(result.success ? result.value : {}) - .properties.format, - ).enum, - )(["html", "md", "txt"]); -}; - -interface IBbsArticle { - format: IBbsArticle.Format; - // title: string; - // body: string; -} -namespace IBbsArticle { - export type Format = "html" | "md" | "txt"; -} diff --git a/test/src/features/llm/schema/validate_llm_schema_nullable.ts b/test/src/features/llm/schema/validate_llm_schema_nullable.ts deleted file mode 100644 index 11674aa0..00000000 --- a/test/src/features/llm/schema/validate_llm_schema_nullable.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { TestValidator } from "@nestia/e2e"; -import { ILlmSchema, IOpenApiSchemaError, IResult } from "@samchon/openapi"; -import { LlmSchemaComposer } from "@samchon/openapi/lib/composers/LlmSchemaComposer"; -import typia, { IJsonSchemaCollection } from "typia"; - -export const test_chatgpt_schema_nullable = (): void => - validate_llm_schema_nullable("chatgpt", "anyOf"); - -export const test_claude_schema_nullable = (): void => - validate_llm_schema_nullable("claude", "oneOf"); - -export const test_gemini_schema_nullable = (): void => - validate_llm_schema_nullable("gemini", "anyOf"); - -export const test_llm_v30_schema_nullable = (): void => - validate_llm_schema_nullable("3.0", "nullable"); - -export const test_llm_v31_schema_nullable = (): void => - validate_llm_schema_nullable("3.1", "oneOf"); - -const validate_llm_schema_nullable = ( - model: Model, - expected: "nullable" | "oneOf" | "anyOf", -): void => { - const collection: IJsonSchemaCollection = - typia.json.schemas<[number | null]>(); - const result: IResult< - ILlmSchema, - IOpenApiSchemaError - > = LlmSchemaComposer.schema(model)({ - config: LlmSchemaComposer.defaultConfig(model) as any, - components: collection.components, - schema: typia.assert(collection.schemas[0]), - $defs: {}, - } as any) as IResult, IOpenApiSchemaError>; - TestValidator.equals("success")(result.success)(true); - TestValidator.equals("nullable")(result.success ? result.value : {})( - expected === "nullable" - ? ({ - type: "number", - nullable: true, - } as any) - : ({ - [expected]: [ - { - type: "null", - }, - { - type: "number", - }, - ], - } as any), - ); -}; diff --git a/test/src/features/llm/schema/validate_llm_schema_oneof.ts b/test/src/features/llm/schema/validate_llm_schema_oneof.ts deleted file mode 100644 index 3201f899..00000000 --- a/test/src/features/llm/schema/validate_llm_schema_oneof.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { TestValidator } from "@nestia/e2e"; -import { ILlmSchema, IOpenApiSchemaError, IResult } from "@samchon/openapi"; -import { LlmSchemaComposer } from "@samchon/openapi/lib/composers/LlmSchemaComposer"; -import typia, { IJsonSchemaCollection } from "typia"; - -export const test_chatgpt_schema_anyof = (): void => - validate_llm_schema_oneof("chatgpt", "anyOf", false); - -export const test_claude_schema_oneof = (): void => - validate_llm_schema_oneof("claude", "oneOf", true); - -export const test_llm_v30_schema_oneof = (): void => - validate_llm_schema_oneof("3.0", "oneOf", false); - -export const test_llm_v31_schema_oneof = (): void => - validate_llm_schema_oneof("3.1", "oneOf", true); - -const validate_llm_schema_oneof = ( - model: Model, - field: "oneOf" | "anyOf", - constant: boolean, -): void => { - const collection: IJsonSchemaCollection = - typia.json.schemas<[IPoint | ILine | ITriangle | IRectangle]>(); - - const $defs: Record> = {}; - const result: IResult< - ILlmSchema, - IOpenApiSchemaError - > = LlmSchemaComposer.schema(model)({ - $defs: $defs as any, - components: collection.components, - schema: collection.schemas[0], - config: { - ...LlmSchemaComposer.defaultConfig(model), - reference: false, - } as any, - }) as IResult, IOpenApiSchemaError>; - TestValidator.equals("success")(result.success); - TestValidator.equals(field)(["point", "line", "triangle", "rectangle"])( - (result as any)?.value?.[field]?.map((e: any) => - constant ? e.properties?.type?.const : e.properties?.type?.enum?.[0], - ), - ); -}; - -interface IPoint { - type: "point"; - x: number; - y: number; -} -interface ILine { - type: "line"; - p1: IPoint; - p2: IPoint; -} -interface ITriangle { - type: "triangle"; - p1: IPoint; - p2: IPoint; - p3: IPoint; -} -interface IRectangle { - type: "rectangle"; - p1: IPoint; - p2: IPoint; - p3: IPoint; - p4: IPoint; -} diff --git a/test/src/features/llm/schema/validate_llm_schema_recursive_ref.ts b/test/src/features/llm/schema/validate_llm_schema_recursive_ref.ts deleted file mode 100644 index 2c3a0613..00000000 --- a/test/src/features/llm/schema/validate_llm_schema_recursive_ref.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { TestValidator } from "@nestia/e2e"; -import { ILlmSchema, IOpenApiSchemaError, IResult } from "@samchon/openapi"; -import { LlmSchemaComposer } from "@samchon/openapi/lib/composers/LlmSchemaComposer"; - -export const test_chatgpt_schema_recursive_ref = (): void => - validate_llm_schema_recursive_ref("chatgpt"); - -export const test_claude_schema_recursive_ref = (): void => - validate_llm_schema_recursive_ref("claude"); - -export const test_llm_v31_schema_recursive_ref = (): void => - validate_llm_schema_recursive_ref("3.1"); - -const validate_llm_schema_recursive_ref = < - Model extends Exclude, ->( - model: Model, -): void => { - const $defs: Record> = {}; - const result: IResult< - ILlmSchema, - IOpenApiSchemaError - > = LlmSchemaComposer.schema(model)({ - $defs: $defs as any, - components: { - schemas: { - Department: { - type: "object", - properties: { - name: { - type: "string", - }, - children: { - type: "array", - items: { - $ref: "#/components/schemas/Department", - }, - }, - }, - required: ["name", "children"], - }, - }, - }, - schema: { - $ref: "#/components/schemas/Department", - }, - config: { - ...(LlmSchemaComposer.defaultConfig(model) as any), - reference: false, - }, - }) as IResult, IOpenApiSchemaError>; - TestValidator.equals("success")(result.success)(true); - TestValidator.equals("$defs")({ - Department: { - type: "object", - properties: { - name: { - type: "string", - }, - children: { - type: "array", - items: { - $ref: "#/$defs/Department", - }, - }, - }, - required: ["name", "children"], - }, - })($defs as any); - TestValidator.equals("schema")(result.success ? result.value : {})({ - $ref: "#/$defs/Department", - }); -}; diff --git a/test/src/features/llm/schema/validate_llm_schema_reference_escaped_description_of_name.ts b/test/src/features/llm/schema/validate_llm_schema_reference_escaped_description_of_name.ts deleted file mode 100644 index 7ced29fa..00000000 --- a/test/src/features/llm/schema/validate_llm_schema_reference_escaped_description_of_name.ts +++ /dev/null @@ -1,86 +0,0 @@ -import { TestValidator } from "@nestia/e2e"; -import { - ILlmSchema, - IOpenApiSchemaError, - IResult, - OpenApi, -} from "@samchon/openapi"; -import { LlmSchemaComposer } from "@samchon/openapi/lib/composers/LlmSchemaComposer"; -import typia, { IJsonSchemaCollection } from "typia"; - -export const test_chatgpt_schema_reference_escaped_description_of_name = - (): void => - validate_llm_schema_reference_escaped_description_of_name("chatgpt"); - -export const test_claude_schema_reference_escaped_description_of_name = - (): void => - validate_llm_schema_reference_escaped_description_of_name("claude"); - -export const test_gemini_schema_reference_escaped_description_of_name = - (): void => - validate_llm_schema_reference_escaped_description_of_name("gemini"); - -export const test_llm_v30_schema_reference_escaped_description_of_name = - (): void => validate_llm_schema_reference_escaped_description_of_name("3.0"); - -export const test_llm_v31_schema_reference_escaped_description_of_name = - (): void => validate_llm_schema_reference_escaped_description_of_name("3.1"); - -const validate_llm_schema_reference_escaped_description_of_name = < - Model extends ILlmSchema.Model, ->( - model: Model, -): void => { - const collection: IJsonSchemaCollection = typia.json.schemas< - [ - { - deep: Something.INested.IDeep; - nested: Something.INested; - something: Something; - }, - ] - >(); - const schema: ILlmSchema.IParameters = - composeSchema(model)(collection); - const deep: ILlmSchema = schema.properties.deep as ILlmSchema; - TestValidator.predicate("description")( - () => - !!(deep as OpenApi.IJsonSchema.IObject).description?.includes( - "Something.INested.IDeep", - ), - ); -}; - -interface Something { - x: number; -} -namespace Something { - export interface INested { - y: number; - } - export namespace INested { - export interface IDeep { - z: number; - } - } -} - -const composeSchema = - (model: Model) => - (collection: IJsonSchemaCollection): ILlmSchema.IParameters => { - const result: IResult< - ILlmSchema.IParameters, - IOpenApiSchemaError - > = LlmSchemaComposer.parameters(model)({ - components: collection.components, - schema: typia.assert< - OpenApi.IJsonSchema.IObject | OpenApi.IJsonSchema.IReference - >(collection.schemas[0]), - config: { - ...LlmSchemaComposer.defaultConfig(model), - reference: false, - } satisfies ILlmSchema.IConfig as any, - }) as IResult, IOpenApiSchemaError>; - if (result.success === false) throw new Error("Invalid schema"); - return result.value; - }; diff --git a/test/src/features/llm/schema/validate_llm_schema_reference_escaped_description_of_namespace.ts b/test/src/features/llm/schema/validate_llm_schema_reference_escaped_description_of_namespace.ts deleted file mode 100644 index 481a2b77..00000000 --- a/test/src/features/llm/schema/validate_llm_schema_reference_escaped_description_of_namespace.ts +++ /dev/null @@ -1,96 +0,0 @@ -import { TestValidator } from "@nestia/e2e"; -import { - ILlmSchema, - IOpenApiSchemaError, - IResult, - OpenApi, -} from "@samchon/openapi"; -import { LlmSchemaComposer } from "@samchon/openapi/lib/composers/LlmSchemaComposer"; -import typia, { IJsonSchemaCollection } from "typia"; - -export const test_chatgpt_schema_reference_escaped_description_of_namespace = - (): void => - validate_llm_schema_reference_escaped_description_of_namespace("chatgpt"); - -export const test_claude_schema_reference_escaped_description_of_namespace = - (): void => - validate_llm_schema_reference_escaped_description_of_namespace("claude"); - -export const test_gemini_schema_reference_escaped_description_of_namespace = - (): void => - validate_llm_schema_reference_escaped_description_of_namespace("gemini"); - -export const test_llm_v30_schema_reference_escaped_description_of_namespace = - (): void => - validate_llm_schema_reference_escaped_description_of_namespace("3.0"); - -export const test_llm_v31_schema_reference_escaped_description_of_namespace = - (): void => - validate_llm_schema_reference_escaped_description_of_namespace("3.1"); - -const validate_llm_schema_reference_escaped_description_of_namespace = < - Model extends ILlmSchema.Model, ->( - model: Model, -): void => { - const collection: IJsonSchemaCollection = typia.json.schemas< - [ - { - deep: Something.INested.IDeep; - nested: Something.INested; - something: Something; - }, - ] - >(); - const schema: ILlmSchema.IParameters = - composeSchema(model)(collection); - const deep: ILlmSchema = schema.properties.deep as ILlmSchema; - TestValidator.predicate("description")(() => { - const description: string | undefined = ( - deep as OpenApi.IJsonSchema.IObject - ).description; - return ( - !!description && - description.includes("Something interface") && - description.includes("Something nested interface") && - description.includes("Something nested and deep interface") - ); - }); -}; - -/** Something interface. */ -interface Something { - x: number; -} -namespace Something { - /** Something nested interface. */ - export interface INested { - y: number; - } - export namespace INested { - /** Something nested and deep interface. */ - export interface IDeep { - z: number; - } - } -} - -const composeSchema = - (model: Model) => - (collection: IJsonSchemaCollection): ILlmSchema.IParameters => { - const result: IResult< - ILlmSchema.IParameters, - IOpenApiSchemaError - > = LlmSchemaComposer.parameters(model)({ - components: collection.components, - schema: typia.assert< - OpenApi.IJsonSchema.IObject | OpenApi.IJsonSchema.IReference - >(collection.schemas[0]), - config: { - ...LlmSchemaComposer.defaultConfig(model), - reference: false, - } satisfies ILlmSchema.IConfig as any, - }) as IResult, IOpenApiSchemaError>; - if (result.success === false) throw new Error("Invalid schema"); - return result.value; - }; diff --git a/test/src/features/llm/schema/validate_llm_schema_reference_escaped_description_of_property.ts b/test/src/features/llm/schema/validate_llm_schema_reference_escaped_description_of_property.ts deleted file mode 100644 index 80b5aff6..00000000 --- a/test/src/features/llm/schema/validate_llm_schema_reference_escaped_description_of_property.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { TestValidator } from "@nestia/e2e"; -import { ILlmSchema, OpenApi } from "@samchon/openapi"; -import { LlmSchemaComposer } from "@samchon/openapi/lib/composers/LlmSchemaComposer"; -import typia, { IJsonSchemaCollection, tags } from "typia"; - -export const test_chatgpt_schema_reference_escaped_description_of_property = - (): void => - validate_llm_schema_reference_escaped_description_of_property("chatgpt"); - -export const test_claude_schema_reference_escaped_description_of_property = - (): void => - validate_llm_schema_reference_escaped_description_of_property("claude"); - -export const test_gemini_schema_reference_escaped_description_of_property = - (): void => - validate_llm_schema_reference_escaped_description_of_property("gemini"); - -export const test_llm_v30_schema_reference_escaped_description_of_property = - (): void => - validate_llm_schema_reference_escaped_description_of_property("3.0"); - -export const test_llm_v31_schema_reference_escaped_description_of_property = - (): void => - validate_llm_schema_reference_escaped_description_of_property("3.1"); - -const validate_llm_schema_reference_escaped_description_of_property = < - Model extends ILlmSchema.Model, ->( - model: Model, -): void => { - const collection: IJsonSchemaCollection = typia.json.schemas<[IMember]>(); - const result = LlmSchemaComposer.parameters(model)({ - config: { - reference: false, - } as any, - components: collection.components, - schema: collection.schemas[0]! as OpenApi.IJsonSchema.IReference, - }); - TestValidator.predicate("description")(() => { - if (result.success === false) return false; - const description: string | undefined = ( - result.value.properties.hobby as OpenApi.IJsonSchema.IObject - ).description; - return ( - !!description?.includes("A hobby") && - !!description?.includes("The main hobby") && - !!description?.includes("The hobby type") - ); - }); -}; - -interface IMember { - id: string & tags.Format<"uuid">; - name: string; - age: number & - tags.Type<"uint32"> & - tags.Minimum<20> & - tags.ExclusiveMaximum<100>; - /** - * A hobby. - * - * The main hobby. - */ - hobby: IHobby; -} - -/** The hobby type. */ -interface IHobby { - name: string; -} diff --git a/test/src/features/llm/schema/validate_llm_schema_separate_object_empty.ts b/test/src/features/llm/schema/validate_llm_schema_separate_object_empty.ts deleted file mode 100644 index f7c2ead3..00000000 --- a/test/src/features/llm/schema/validate_llm_schema_separate_object_empty.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { TestValidator } from "@nestia/e2e"; -import { - ILlmSchema, - IOpenApiSchemaError, - IResult, - OpenApi, - OpenApiTypeChecker, -} from "@samchon/openapi"; -import { LlmSchemaComposer } from "@samchon/openapi/lib/composers/LlmSchemaComposer"; -import typia, { IJsonSchemaCollection } from "typia"; - -export const test_chatgpt_schema_separate_object_empty = (): void => - validate_llm_schema_separate_object_empty("chatgpt"); - -export const test_claude_schema_separate_object_empty = (): void => - validate_llm_schema_separate_object_empty("claude"); - -export const test_gemini_schema_separate_object_empty = (): void => - validate_llm_schema_separate_object_empty("gemini"); - -export const test_llm_v30_schema_separate_object_empty = (): void => - validate_llm_schema_separate_object_empty("3.0"); - -export const test_llm_v31_schema_separate_object_empty = (): void => - validate_llm_schema_separate_object_empty("3.1"); - -const validate_llm_schema_separate_object_empty = < - Model extends ILlmSchema.Model, ->( - model: Model, -): void => { - TestValidator.equals("separated")( - LlmSchemaComposer.separateParameters(model)({ - predicate: ((schema: OpenApi.IJsonSchema) => - OpenApiTypeChecker.isInteger(schema)) as any, - parameters: schema(model)(typia.json.schemas<[{}]>()) as any, - }), - )({ - llm: schema(model)(typia.json.schemas<[{}]>()) as any, - human: null, - }); -}; - -const schema = - (model: Model) => - (collection: IJsonSchemaCollection): ILlmSchema.IParameters => { - const result: IResult< - ILlmSchema.IParameters, - IOpenApiSchemaError - > = LlmSchemaComposer.parameters(model)({ - components: collection.components, - schema: typia.assert< - OpenApi.IJsonSchema.IObject | OpenApi.IJsonSchema.IReference - >(collection.schemas[0]), - config: LlmSchemaComposer.defaultConfig( - model, - ) satisfies ILlmSchema.IConfig as any, - }) as IResult, IOpenApiSchemaError>; - if (result.success === false) throw new Error("Invalid schema"); - return result.value; - }; diff --git a/test/src/features/llm/schema/validate_llm_schema_separate_string.ts b/test/src/features/llm/schema/validate_llm_schema_separate_string.ts deleted file mode 100644 index 43f8d18e..00000000 --- a/test/src/features/llm/schema/validate_llm_schema_separate_string.ts +++ /dev/null @@ -1,106 +0,0 @@ -import { TestValidator } from "@nestia/e2e"; -import { - ILlmSchema, - IOpenApiSchemaError, - IResult, - OpenApi, -} from "@samchon/openapi"; -import { LlmSchemaComposer } from "@samchon/openapi/lib/composers/LlmSchemaComposer"; -import typia, { IJsonSchemaCollection, tags } from "typia"; - -export const test_chatgpt_schema_separate_string = (): void => - validate_llm_schema_separate_string("chatgpt", false); - -export const test_claude_schema_separate_string = (): void => - validate_llm_schema_separate_string("claude", true); - -export const test_gemini_schema_separate_string = (): void => - validate_llm_schema_separate_string("gemini", true); - -export const test_llm_v30_schema_separate_string = (): void => { - validate_llm_schema_separate_string("3.0", false); - validate_llm_schema_separate_string("3.0", true); -}; - -export const test_llm_v31_schema_separate_string = (): void => { - validate_llm_schema_separate_string("3.1", false); - validate_llm_schema_separate_string("3.1", true); -}; - -const validate_llm_schema_separate_string = ( - model: Model, - constraint: boolean, -): void => { - const separator = (schema: ILlmSchema.IParameters) => - LlmSchemaComposer.separateParameters(model)({ - predicate: (s) => - LlmSchemaComposer.typeChecker(model).isString( - s as OpenApi.IJsonSchema.IString, - ) && - (constraint - ? (s as OpenApi.IJsonSchema.IString).contentMediaType !== undefined - : (s as OpenApi.IJsonSchema.IString).description?.includes( - "@contentMediaType", - ) === true), - parameters: schema as any, - }); - const plain: ILlmSchema.IParameters = schema( - model, - constraint, - )( - typia.json.schemas< - [ - { - name: string; - }, - ] - >(), - ); - const upload: ILlmSchema.IParameters = schema( - model, - constraint, - )( - typia.json.schemas< - [ - { - file: string & tags.ContentMediaType<"image/*">; - }, - ] - >(), - ); - TestValidator.equals("plain")(separator(plain))({ - llm: plain, - human: null, - }); - TestValidator.equals("upload")(separator(upload))({ - llm: { - type: "object", - properties: {}, - additionalProperties: false, - required: [], - $defs: {}, - }, - human: upload, - }); -}; - -const schema = - (model: Model, constraint: boolean) => - (collection: IJsonSchemaCollection): ILlmSchema.IParameters => { - const result: IResult< - ILlmSchema.IParameters, - IOpenApiSchemaError - > = LlmSchemaComposer.parameters(model)({ - components: collection.components, - schema: typia.assert< - OpenApi.IJsonSchema.IObject | OpenApi.IJsonSchema.IReference - >(collection.schemas[0]), - config: { - ...LlmSchemaComposer.defaultConfig(model), - reference: true, - constraint, - } satisfies ILlmSchema.IConfig as any, - }) as IResult, IOpenApiSchemaError>; - if (result.success === false) throw new Error("Invalid schema"); - return result.value; - }; diff --git a/test/src/features/llm/schema/validate_llm_type_checker_cover_any.ts b/test/src/features/llm/schema/validate_llm_type_checker_cover_any.ts deleted file mode 100644 index 3685f223..00000000 --- a/test/src/features/llm/schema/validate_llm_type_checker_cover_any.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { TestValidator } from "@nestia/e2e"; -import { ILlmSchema, OpenApi } from "@samchon/openapi"; -import { LlmSchemaComposer } from "@samchon/openapi/lib/composers/LlmSchemaComposer"; -import typia, { IJsonSchemaCollection } from "typia"; - -export const test_chatgpt_type_checker_cover_any = (): void => - validate_llm_type_checker_cover_any("chatgpt"); - -export const test_claude_type_checker_cover_any = (): void => - validate_llm_type_checker_cover_any("claude"); - -export const test_gemini_type_checker_cover_any = (): void => - validate_llm_type_checker_cover_any("gemini"); - -export const test_llm_v30_type_checker_cover_any = (): void => - validate_llm_type_checker_cover_any("3.0"); - -export const test_llm_v31_type_checker_cover_any = (): void => - validate_llm_type_checker_cover_any("3.1"); - -const validate_llm_type_checker_cover_any = ( - model: Model, -) => { - const collection: IJsonSchemaCollection = typia.json.schemas<[IBasic]>(); - const result = LlmSchemaComposer.parameters(model)({ - config: LlmSchemaComposer.defaultConfig(model) as any, - components: collection.components, - schema: collection.schemas[0] as OpenApi.IJsonSchema.IReference, - }); - if (result.success === false) - throw new Error(`Failed to compose ${model} parameters.`); - - const parameters = result.value; - const check = (x: ILlmSchema, y: ILlmSchema): boolean => - model === "3.0" || model === "gemini" - ? (LlmSchemaComposer.typeChecker(model).covers as any)(x, y) - : (LlmSchemaComposer.typeChecker(model).covers as any)({ - x, - y, - $defs: (parameters as any).$defs, - }); - TestValidator.equals("any covers (string | null)")(true)( - check( - parameters.properties.any as ILlmSchema, - parameters.properties.string_or_null as ILlmSchema, - ), - ); - TestValidator.equals("any covers (string | undefined)")(true)( - check( - parameters.properties.any as ILlmSchema, - parameters.properties.string_or_undefined as ILlmSchema, - ), - ); -}; - -interface IBasic { - any: any; - string_or_null: null | string; - string_or_undefined: string | undefined; -} diff --git a/test/src/features/llm/schema/validate_llm_type_checker_cover_array.ts b/test/src/features/llm/schema/validate_llm_type_checker_cover_array.ts deleted file mode 100644 index f007e503..00000000 --- a/test/src/features/llm/schema/validate_llm_type_checker_cover_array.ts +++ /dev/null @@ -1,208 +0,0 @@ -import { TestValidator } from "@nestia/e2e"; -import { ILlmSchema, OpenApi } from "@samchon/openapi"; -import { LlmSchemaComposer } from "@samchon/openapi/lib/composers/LlmSchemaComposer"; -import typia, { IJsonSchemaCollection } from "typia"; - -export const test_chatgpt_type_checker_cover_array = (): void => - validate_llm_type_checker_cover_array("chatgpt"); - -export const test_claude_type_checker_cover_array = (): void => - validate_llm_type_checker_cover_array("claude"); - -export const test_gemini_type_checker_cover_array = (): void => - validate_llm_type_checker_cover_array("gemini"); - -export const test_llm_v30_type_checker_cover_array = (): void => - validate_llm_type_checker_cover_array("3.0"); - -export const test_llm_v31_type_checker_cover_array = (): void => - validate_llm_type_checker_cover_array("3.1"); - -const validate_llm_type_checker_cover_array = ( - model: Model, -) => { - const collection: IJsonSchemaCollection = - typia.json.schemas<[Plan2D, Plan3D, Box2D, Box3D]>(); - const components: OpenApi.IComponents = collection.components as any; - const plan2D: OpenApi.IJsonSchema = components.schemas!.Plan2D; - const plan3D: OpenApi.IJsonSchema = components.schemas!.Plan3D; - const box2D: OpenApi.IJsonSchema = components.schemas!.Box2D; - const box3D: OpenApi.IJsonSchema = components.schemas!.Box3D; - - const $defs = {}; - const check = (x: OpenApi.IJsonSchema, y: OpenApi.IJsonSchema): boolean => { - const [a, b] = [x, y].map((schema) => { - const result = LlmSchemaComposer.schema(model)({ - config: LlmSchemaComposer.defaultConfig(model) as any, - components: collection.components, - schema: schema, - $defs, - }); - if (result.success === false) - throw new Error(`Failed to compose ${model} schema.`); - return result.value; - }); - return model === "3.0" - ? (LlmSchemaComposer.typeChecker(model).covers as any)(a, b) - : (LlmSchemaComposer.typeChecker(model).covers as any)({ - x: a, - y: b, - $defs, - }); - }; - - TestValidator.equals(model + " Plan3D[] covers Plan2D[]")(true)( - check({ type: "array", items: plan3D }, { type: "array", items: plan2D }), - ); - TestValidator.equals(model + " Box3D[] covers Box2D[]")(true)( - check({ type: "array", items: box3D }, { type: "array", items: box2D }), - ); - if (model !== "gemini") - TestValidator.equals( - model + " Array covers Array", - )(true)( - check( - { - type: "array", - items: { - oneOf: [plan3D, box3D], - }, - }, - { - type: "array", - items: { - oneOf: [plan2D, box2D], - }, - }, - ), - ); - if (model !== "gemini") - TestValidator.equals(model + " (Plan3D|Box3D)[] covers (Plan2D|Box2D)[]")( - true, - )( - check( - { - oneOf: [ - { type: "array", items: plan3D }, - { type: "array", items: box3D }, - ], - }, - { - oneOf: [ - { type: "array", items: plan2D }, - { type: "array", items: box2D }, - ], - }, - ), - ); - - TestValidator.equals(model + " Plan2D[] can't cover Plan3D[]")(false)( - check({ type: "array", items: plan2D }, { type: "array", items: plan3D }), - ); - TestValidator.equals(model + " Box2D[] can't cover Box3D[]")(false)( - check({ type: "array", items: box2D }, { type: "array", items: box3D }), - ); - if (model !== "gemini") - if (model !== "gemini") - TestValidator.equals( - "Array can't cover Array", - )(false)( - check( - { - type: "array", - items: { - oneOf: [plan2D, box2D], - }, - }, - { - type: "array", - items: { - oneOf: [plan3D, box3D], - }, - }, - ), - ); - if (model !== "gemini") - TestValidator.equals( - model + " (Plan2D[]|Box2D[]) can't cover (Plan3D[]|Box3D[])", - )(false)( - check( - { - oneOf: [ - { type: "array", items: plan2D }, - { type: "array", items: box2D }, - ], - }, - { - oneOf: [ - { type: "array", items: plan3D }, - { type: "array", items: box3D }, - ], - }, - ), - ); - if (model !== "gemini") - TestValidator.equals(model + " Plan3D[] can't cover (Plan2D|Box2D)[]")( - false, - )( - check( - { type: "array", items: plan3D }, - { - oneOf: [ - { type: "array", items: plan2D }, - { type: "array", items: box2D }, - ], - }, - ), - ); - if (model !== "gemini") - TestValidator.equals(model + " Box3D[] can't cover Array")( - false, - )( - check( - { type: "array", items: box3D }, - { - type: "array", - items: { - oneOf: [plan2D, box2D], - }, - }, - ), - ); -}; - -type Plan2D = { - center: Point2D; - size: Point2D; - geometries: Geometry2D[]; -}; -type Plan3D = { - center: Point3D; - size: Point3D; - geometries: Geometry3D[]; -}; -type Geometry3D = { - position: Point3D; - scale: Point3D; -}; -type Geometry2D = { - position: Point2D; - scale: Point2D; -}; -type Point2D = { - x: number; - y: number; -}; -type Point3D = { - x: number; - y: number; - z: number; -}; -type Box2D = { - size: Point2D; - nested: Point2D[]; -}; -type Box3D = { - size: Point3D; - nested: Point3D[]; -}; diff --git a/test/src/features/mcp/test_mcp_application.ts b/test/src/features/mcp/test_mcp_application.ts new file mode 100644 index 00000000..d2fdc1b2 --- /dev/null +++ b/test/src/features/mcp/test_mcp_application.ts @@ -0,0 +1,70 @@ +import { TestValidator } from "@nestia/e2e"; +import { + HttpLlm, + IHttpLlmApplication, + ILlmSchema, + IMcpLlmApplication, + IMcpLlmFunction, + LlmTypeChecker, + McpLlm, + OpenApi, +} from "@samchon/openapi"; +import fs from "fs"; + +import { TestGlobal } from "../../TestGlobal"; + +export const test_mcp_application = async (): Promise => { + const http: IHttpLlmApplication = HttpLlm.application({ + document: OpenApi.convert( + await fetch( + "https://raw.githubusercontent.com/samchon/shopping-backend/refs/heads/master/packages/api/swagger.json", + ).then((r) => r.json()), + ), + }); + const mcp: IMcpLlmApplication = McpLlm.application({ + tools: http.functions.map((f) => ({ + name: f.name, + description: f.description, + inputSchema: f.parameters, + })), + }); + TestValidator.equals("functions")(http.functions.length)( + mcp.functions.length, + ); + TestValidator.equals("errors")(0)(mcp.errors.length); + + http.functions.forEach((x) => { + const parameters: ILlmSchema.IParameters = { ...x.parameters }; + const visited: Set = new Set(); + LlmTypeChecker.visit({ + closure: (schema: any) => { + if (typeof schema.$ref === "string") + visited.add(schema.$ref.split("/").pop()!); + }, + schema: parameters, + $defs: parameters.$defs, + }); + (parameters as any).$defs = Object.fromEntries( + Object.entries((parameters as any).$defs).filter(([key]) => + visited.has(key), + ), + ); + const y: IMcpLlmFunction | undefined = mcp.functions.find( + (y) => y.name === x.name, + ); + TestValidator.equals( + `parameters: ${x.name}`, + (key) => + key === "description" || + key === "discriminator" || + key === "x-discriminator", + )(parameters)((y?.parameters as any) ?? {}); + }); + + if (process.argv.includes("--file")) + await fs.promises.writeFile( + `${TestGlobal.ROOT}/examples/mcp/application.json`, + JSON.stringify(mcp, null, 2), + "utf8", + ); +}; diff --git a/test/src/features/mcp/test_mcp_schema_ref.ts b/test/src/features/mcp/test_mcp_schema_ref.ts index 2ed35066..16807ded 100644 --- a/test/src/features/mcp/test_mcp_schema_ref.ts +++ b/test/src/features/mcp/test_mcp_schema_ref.ts @@ -1,25 +1,20 @@ import { TestValidator } from "@nestia/e2e"; import { - ChatGptTypeChecker, HttpLlm, IHttpLlmApplication, IHttpLlmFunction, IMcpLlmApplication, - LlamaTypeChecker, + LlmTypeChecker, McpLlm, } from "@samchon/openapi"; export const test_mcp_schema_ref = async (): Promise => { - const http: IHttpLlmApplication<"chatgpt"> = HttpLlm.application({ - model: "chatgpt", + const http: IHttpLlmApplication = HttpLlm.application({ document: await fetch( "https://raw.githubusercontent.com/samchon/shopping-backend/refs/heads/master/packages/api/swagger.json", ).then((r) => r.json()), - options: { - reference: true, - }, }); - const func: IHttpLlmFunction<"chatgpt"> | undefined = http.functions.find( + const func: IHttpLlmFunction | undefined = http.functions.find( (x) => Object.keys(x.parameters.$defs).length !== 0 && Object.keys(x.parameters.properties).length !== 0 && @@ -30,17 +25,16 @@ export const test_mcp_schema_ref = async (): Promise => { if (func === undefined) throw new Error("Function not found"); const visited: Set = new Set(); - ChatGptTypeChecker.visit({ + LlmTypeChecker.visit({ closure: (schema) => { - if (ChatGptTypeChecker.isReference(schema)) + if (LlmTypeChecker.isReference(schema)) visited.add(schema.$ref.split("/").pop()!); }, $defs: func.parameters.$defs, schema: func.parameters, }); - const mcp: IMcpLlmApplication<"chatgpt"> = McpLlm.application({ - model: "chatgpt", + const mcp: IMcpLlmApplication = McpLlm.application({ tools: [ { name: func.name, @@ -48,9 +42,6 @@ export const test_mcp_schema_ref = async (): Promise => { inputSchema: func.parameters, }, ], - options: { - reference: true, - }, }); TestValidator.equals( "schema", @@ -64,12 +55,12 @@ export const test_mcp_schema_ref = async (): Promise => { }; const isEmptyBody = ($defs: Record, input: any): boolean => { - if (LlamaTypeChecker.isReference(input)) { + if (LlmTypeChecker.isReference(input)) { const name: string = input.$ref.split("/").pop()!; return $defs[name] && isEmptyBody($defs, $defs[name]); } return ( - LlamaTypeChecker.isObject(input) && + LlmTypeChecker.isObject(input) && Object.keys(input.properties ?? {}).length === 0 ); }; diff --git a/test/src/features/mcp/validate_mcp_application.ts b/test/src/features/mcp/validate_mcp_application.ts deleted file mode 100644 index bd1a77ad..00000000 --- a/test/src/features/mcp/validate_mcp_application.ts +++ /dev/null @@ -1,94 +0,0 @@ -import { TestValidator } from "@nestia/e2e"; -import { HttpLlm, ILlmSchema, McpLlm, OpenApi } from "@samchon/openapi"; -import fs from "fs"; -import { Singleton, VariadicSingleton } from "tstl"; - -import { LlmSchemaComposer } from "../../../../lib/composers/LlmSchemaComposer"; -import { TestGlobal } from "../../TestGlobal"; - -export const test_mcp_application_of_chatgpt = () => - validate_mcp_application("chatgpt"); - -export const test_mcp_application_of_claude = () => - validate_mcp_application("claude"); - -export const test_mcp_application_of_gemini = () => - validate_mcp_application("gemini"); - -export const test_mcp_application_of_v30 = () => - validate_mcp_application("3.0"); - -export const test_mcp_application_of_v31 = () => - validate_mcp_application("3.1"); - -const validate_mcp_application = async ( - model: Model, -): Promise => { - const llm = await getApplication.get(model); - const mcp = McpLlm.application({ - model, - tools: llm.functions.map((f) => ({ - name: f.name, - description: f.description, - inputSchema: f.parameters, - })), - options: { - reference: true, - } as any, - }); - TestValidator.equals("functions")(llm.functions.length)(mcp.functions.length); - TestValidator.equals("errors")(0)(mcp.errors.length); - - llm.functions.forEach((x) => { - const parameters = { ...x.parameters }; - if (model !== "3.0") { - const visited: Set = new Set(); - LlmSchemaComposer.typeChecker(model).visit({ - closure: (schema: any) => { - if (typeof schema.$ref === "string") - visited.add(schema.$ref.split("/").pop()!); - }, - schema: parameters, - $defs: (parameters as any).$defs, - } as any); - (parameters as any).$defs = Object.fromEntries( - Object.entries((parameters as any).$defs).filter(([key]) => - visited.has(key), - ), - ); - } - const y = mcp.functions.find((y) => y.name === x.name); - TestValidator.equals( - `parameters: ${x.name}`, - (key) => - key === "description" || - key === "discriminator" || - key === "x-discriminator", - )(parameters)((y?.parameters as any) ?? {}); - }); - - if (process.argv.includes("--file")) - await fs.promises.writeFile( - `${TestGlobal.ROOT}/examples/mcp/${model}.application.json`, - JSON.stringify(mcp, null, 2), - "utf8", - ); -}; - -const getApplication = new VariadicSingleton(async (model: ILlmSchema.Model) => - HttpLlm.application({ - model, - document: await getDocument.get(), - options: { - reference: true, - } as any, - }), -); - -const getDocument = new Singleton(async () => - OpenApi.convert( - await fetch( - "https://raw.githubusercontent.com/samchon/shopping-backend/refs/heads/master/packages/api/swagger.json", - ).then((r) => r.json()), - ), -); diff --git a/test/src/utils/LlmApplicationFactory.ts b/test/src/utils/LlmApplicationFactory.ts index 672b22b0..48356c1e 100644 --- a/test/src/utils/LlmApplicationFactory.ts +++ b/test/src/utils/LlmApplicationFactory.ts @@ -4,6 +4,7 @@ import { ILlmSchema, IOpenApiSchemaError, IResult, + OpenApi, } from "@samchon/openapi"; import { LlmSchemaComposer } from "@samchon/openapi/lib/composers/LlmSchemaComposer"; import { IJsonSchemaApplication } from "typia"; @@ -11,55 +12,51 @@ import { IJsonSchemaApplication } from "typia"; import { OpenApiValidator } from "../../../lib/utils/OpenApiValidator"; export namespace LlmApplicationFactory { - export const convert = (props: { - model: Model; + export const convert = (props: { application: IJsonSchemaApplication; - config?: ILlmSchema.IConfig; - }): ILlmApplication => { - const config: ILlmSchema.IConfig = - props.config ?? LlmSchemaComposer.defaultConfig(props.model); + config?: Partial; + }): ILlmApplication => { + const config: ILlmSchema.IConfig = LlmSchemaComposer.getConfig( + props.config, + ); return { - model: props.model, functions: props.application.functions.map((func) => convertFunction({ - model: props.model, - options: config, + config, components: props.application.components, function: func, }), ), - options: config, + config: { + ...config, + separate: null, + validate: null, + }, }; }; - const convertFunction = (props: { - model: Model; - options: ILlmSchema.IConfig; + const convertFunction = (props: { + config: ILlmSchema.IConfig; components: IJsonSchemaApplication.IComponents; function: IJsonSchemaApplication.IFunction; - }): ILlmFunction => { - const parameters: IResult< - ILlmSchema.IParameters, - IOpenApiSchemaError - > = LlmSchemaComposer.parameters(props.model)({ - config: props.options as any, - components: props.components, - schema: props.function.parameters[0].schema as any, - }) satisfies IResult< - ILlmSchema.IParameters, - IOpenApiSchemaError - > as IResult, IOpenApiSchemaError>; + }): ILlmFunction => { + const parameters: IResult = + LlmSchemaComposer.parameters({ + config: props.config, + components: props.components, + schema: props.function.parameters[0].schema as + | OpenApi.IJsonSchema.IObject + | OpenApi.IJsonSchema.IReference, + }); if (parameters.success === false) { console.log(JSON.stringify(parameters.error, null, 2)); throw new Error("Failed to compose parameters schema."); } - const out = ( - schema: ILlmSchema | undefined, - ): ILlmFunction => ({ + const out = (schema: ILlmSchema | undefined): ILlmFunction => ({ name: props.function.name, description: props.function.description, - parameters: parameters.value as any, - output: schema as any, + parameters: parameters.value, + output: schema, validate: OpenApiValidator.create({ components: props.components, schema: props.function.parameters[0].schema, @@ -68,22 +65,17 @@ export namespace LlmApplicationFactory { }); if (props.function.output === undefined) return out(undefined); - const output: IResult< - ILlmSchema, - IOpenApiSchemaError - > = LlmSchemaComposer.schema(props.model)({ - config: props.options as any, - components: props.components, - schema: props.function.output.schema, - $defs: (parameters.value as any).$defs, - }) satisfies IResult as IResult< - ILlmSchema, - IOpenApiSchemaError - >; + const output: IResult = + LlmSchemaComposer.schema({ + config: props.config, + components: props.components, + schema: props.function.output.schema, + $defs: parameters.value.$defs, + }); if (output.success === false) { - console.log(JSON.stringify(output.error), null, 2); + console.log(JSON.stringify(output.error, null, 2)); throw new Error("Failed to compose output schema."); } - return out(output.value as any); + return out(output.value); }; } diff --git a/test/src/utils/LlmFunctionCaller.ts b/test/src/utils/LlmFunctionCaller.ts index 27ad13d0..93ac3eb3 100644 --- a/test/src/utils/LlmFunctionCaller.ts +++ b/test/src/utils/LlmFunctionCaller.ts @@ -6,21 +6,16 @@ import { TestGlobal } from "../TestGlobal"; import { ILlmTextPrompt } from "../dto/ILlmTextPrompt"; export namespace LlmFunctionCaller { - export interface IProps { + export interface IProps { vendor: string; - model: Model; - function: ILlmFunction; + function: ILlmFunction; texts: ILlmTextPrompt[]; handleCompletion: (input: any) => Promise; - handleParameters?: ( - parameters: ILlmSchema.ModelParameters[Model], - ) => Promise; + handleParameters?: (parameters: ILlmSchema.IParameters) => Promise; strict?: boolean; } - export const test = async ( - props: IProps, - ) => { + export const test = async (props: IProps) => { if ( TestGlobal.env.OPENAI_API_KEY === undefined || TestGlobal.env.OPENROUTER_API_KEY === undefined @@ -41,8 +36,8 @@ export namespace LlmFunctionCaller { }); }; - const step = async ( - props: IProps, + const step = async ( + props: IProps, previous?: IValidation.IFailure, ): Promise> => { const client: OpenAI = new OpenAI({