From 43a5df5e3c28f9014382ebfba624312816e327fe Mon Sep 17 00:00:00 2001 From: Jeongho Nam Date: Sun, 25 Aug 2024 02:24:21 +0900 Subject: [PATCH 01/16] New function `OpenApi.llm()` returning LLM function calling schemas. To support `typia.llm.functions()` function which composes LLM function calling schemas from TypeScript function types, `@samchon/openapi` newly added `OpenApi.llm()` function. --- package.json | 2 +- src/OpenApi.ts | 41 +- src/converters/LlmComposer.ts | 258 +++++++++ src/converters/LlmSchemaSeparator.ts | 92 +++ .../MigrateConverter.ts | 7 +- .../MigrateRouteAccessor.ts | 2 +- .../MigrateRouteConverter.ts | 2 +- .../OpenApiTypeChecker.ts | 0 .../OpenApiV3Converter.ts | 0 .../OpenApiV3Downgrader.ts | 0 .../OpenApiV3_1Converter.ts | 0 .../SwaggerV2Converter.ts | 0 .../SwaggerV2Downgrader.ts | 0 src/index.ts | 4 +- src/structures/ILlmDocument.ts | 153 +++++ src/structures/ILlmFunction.ts | 56 ++ src/structures/ILlmProcedure.ts | 239 ++++++++ src/structures/ILlmSchema.ts | 530 ++++++++++++++++++ src/{ => structures}/IMigrateDocument.ts | 7 +- src/{ => structures}/IMigrateRoute.ts | 2 +- src/utils/LlmFetcher.ts | 0 src/utils/LlmTypeChecker.ts | 132 +++++ src/utils/MigrateFetcher.ts | 0 .../test_json_schema_downgrade_v20.ts | 2 +- .../test_json_schema_downgrade_v30.ts | 2 +- 25 files changed, 1512 insertions(+), 19 deletions(-) create mode 100644 src/converters/LlmComposer.ts create mode 100644 src/converters/LlmSchemaSeparator.ts rename src/{internal => converters}/MigrateConverter.ts (92%) rename src/{internal => converters}/MigrateRouteAccessor.ts (98%) rename src/{internal => converters}/MigrateRouteConverter.ts (99%) rename src/{internal => converters}/OpenApiTypeChecker.ts (100%) rename src/{internal => converters}/OpenApiV3Converter.ts (100%) rename src/{internal => converters}/OpenApiV3Downgrader.ts (100%) rename src/{internal => converters}/OpenApiV3_1Converter.ts (100%) rename src/{internal => converters}/SwaggerV2Converter.ts (100%) rename src/{internal => converters}/SwaggerV2Downgrader.ts (100%) create mode 100644 src/structures/ILlmDocument.ts create mode 100644 src/structures/ILlmFunction.ts create mode 100644 src/structures/ILlmProcedure.ts create mode 100644 src/structures/ILlmSchema.ts rename src/{ => structures}/IMigrateDocument.ts (92%) rename src/{ => structures}/IMigrateRoute.ts (99%) create mode 100644 src/utils/LlmFetcher.ts create mode 100644 src/utils/LlmTypeChecker.ts create mode 100644 src/utils/MigrateFetcher.ts diff --git a/package.json b/package.json index 786533f..64fd732 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@samchon/openapi", - "version": "0.4.9", + "version": "0.5.0-dev.20240824", "description": "OpenAPI definitions and converters for 'typia' and 'nestia'.", "main": "./lib/index.js", "module": "./lib/index.mjs", diff --git a/src/OpenApi.ts b/src/OpenApi.ts index eb013dd..e287522 100644 --- a/src/OpenApi.ts +++ b/src/OpenApi.ts @@ -1,13 +1,15 @@ -import { IMigrateDocument } from "./IMigrateDocument"; import { OpenApiV3 } from "./OpenApiV3"; import { OpenApiV3_1 } from "./OpenApiV3_1"; import { SwaggerV2 } from "./SwaggerV2"; -import { MigrateConverter } from "./internal/MigrateConverter"; -import { OpenApiV3Converter } from "./internal/OpenApiV3Converter"; -import { OpenApiV3Downgrader } from "./internal/OpenApiV3Downgrader"; -import { OpenApiV3_1Converter } from "./internal/OpenApiV3_1Converter"; -import { SwaggerV2Converter } from "./internal/SwaggerV2Converter"; -import { SwaggerV2Downgrader } from "./internal/SwaggerV2Downgrader"; +import { LlmComposer } from "./converters/LlmComposer"; +import { MigrateConverter } from "./converters/MigrateConverter"; +import { OpenApiV3Converter } from "./converters/OpenApiV3Converter"; +import { OpenApiV3Downgrader } from "./converters/OpenApiV3Downgrader"; +import { OpenApiV3_1Converter } from "./converters/OpenApiV3_1Converter"; +import { SwaggerV2Converter } from "./converters/SwaggerV2Converter"; +import { SwaggerV2Downgrader } from "./converters/SwaggerV2Downgrader"; +import { ILlmDocument } from "./structures/ILlmDocument"; +import { IMigrateDocument } from "./structures/IMigrateDocument"; /** * Emended OpenAPI v3.1 definition used by `typia` and `nestia`. @@ -153,6 +155,31 @@ export namespace OpenApi { return MigrateConverter.convert(document); } + export function llm( + document: OpenApi.IDocument, + options?: ILlmDocument.IOptions, + ): ILlmDocument; + + export function llm( + mirate: IMigrateDocument, + options?: ILlmDocument.IOptions, + ): ILlmDocument; + + /** + * @internal + */ + export function llm( + document: OpenApi.IDocument | IMigrateDocument, + options?: ILlmDocument.IOptions, + ): ILlmDocument { + if ((document as OpenApi.IDocument)["x-samchon-emended"] !== true) + document = migrate(document as OpenApi.IDocument); + return LlmComposer.compose(document as IMigrateDocument, { + keyword: options?.keyword ?? false, + separate: options?.separate ?? null, + }); + } + /* ----------------------------------------------------------- PATH ITEMS ----------------------------------------------------------- */ diff --git a/src/converters/LlmComposer.ts b/src/converters/LlmComposer.ts new file mode 100644 index 0000000..599adbe --- /dev/null +++ b/src/converters/LlmComposer.ts @@ -0,0 +1,258 @@ +import { OpenApi } from "../OpenApi"; +import { OpenApiTypeChecker } from "../OpenApiTypeChecker"; +import { ILlmDocument } from "../structures/ILlmDocument"; +import { ILlmProcedure } from "../structures/ILlmProcedure"; +import { ILlmSchema } from "../structures/ILlmSchema"; +import { IMigrateDocument } from "../structures/IMigrateDocument"; +import { IMigrateRoute } from "../structures/IMigrateRoute"; +import { LlmTypeChecker } from "../utils/LlmTypeChecker"; +import { LlmSchemaSeparator } from "./LlmSchemaSeparator"; +import { OpenApiV3Downgrader } from "./OpenApiV3Downgrader"; + +export namespace LlmComposer { + export const compose = ( + migrate: IMigrateDocument, + options: ILlmDocument.IOptions, + ): ILlmDocument => { + // COMPOSE FUNCTIONS + const errors: ILlmDocument.IError[] = migrate.errors.map((e) => ({ + method: e.method, + path: e.path, + messages: e.messages, + operation: () => e.operation(), + route: () => undefined, + })); + const functions: ILlmProcedure[] = migrate.routes + .map((route) => { + if (route.method === "head") return null; + const func: ILlmProcedure | null = composeFunction(options)( + migrate.document().components, + )(route); + if (func === null) + errors.push({ + method: route.method, + path: route.path, + messages: ["Failed to escape $ref"], + operation: () => route.operation(), + route: () => route, + }); + return func; + }) + .filter((v): v is ILlmProcedure => v !== null); + return { + openapi: "3.0.3", + functions, + errors, + options, + }; + }; + + export const schema = ( + components: OpenApi.IComponents, + schema: OpenApi.IJsonSchema, + ): ILlmSchema | null => { + const escaped: OpenApi.IJsonSchema | null = escapeReference(components)( + new Set(), + )(schema); + if (escaped === null) return null; + const downgraded: ILlmSchema = OpenApiV3Downgrader.downgradeSchema({ + original: {}, + downgraded: {}, + })(escaped) as ILlmSchema; + LlmTypeChecker.visit(downgraded, (schema) => { + if ( + LlmTypeChecker.isOneOf(schema) && + (schema as any).discriminator !== undefined + ) + delete (schema as any).discriminator; + }); + return downgraded; + }; + + const composeFunction = + (options: ILlmDocument.IOptions) => + (components: OpenApi.IComponents) => + (route: IMigrateRoute): ILlmProcedure | null => { + // CAST SCHEMA TYPES + const cast = (s: OpenApi.IJsonSchema) => schema(components, s); + const output: ILlmSchema | null | undefined = + route.success && route.success ? cast(route.success.schema) : undefined; + if (output === null) return null; + const properties: [string, ILlmSchema | null][] = [ + ...route.parameters.map((p) => ({ + key: p.key, + schema: { + ...p.schema, + title: p.parameter().title ?? p.schema.title, + description: p.parameter().description ?? p.schema.description, + }, + })), + ...(route.query + ? [ + { + key: route.query.key, + schema: { + ...route.query.schema, + title: route.query.title() ?? route.query.schema.title, + description: + route.query.description() ?? route.query.schema.description, + }, + }, + ] + : []), + ...(route.body + ? [ + { + key: route.body.key, + schema: { + ...route.body.schema, + description: + route.body.description() ?? route.body.schema.description, + }, + }, + ] + : []), + ].map((o) => [o.key, cast(o.schema)]); + if (properties.some(([_k, v]) => v === null)) return null; + + // COMPOSE PARAMETERS + const parameters: ILlmSchema[] = options.keyword + ? [ + { + type: "object", + properties: Object.fromEntries( + properties as [string, ILlmSchema][], + ), + }, + ] + : properties.map(([_k, v]) => v!); + const operation: OpenApi.IOperation = route.operation(); + + // FINALIZATION + return { + method: route.method as "get", + path: route.path, + name: route.accessor.join("_"), + strict: true, + parameters, + separated: options.separate + ? LlmSchemaSeparator.parameters({ + parameters, + predicator: options.separate, + }) + : undefined, + output: output + ? (OpenApiV3Downgrader.downgradeSchema({ + original: {}, + downgraded: {}, + })(output) as ILlmSchema) + : undefined, + description: (() => { + if (operation.summary && operation.description) { + return operation.description.startsWith(operation.summary) + ? operation.description + : [ + operation.summary, + operation.summary.endsWith(".") ? "" : ".", + "\n\n", + operation.description, + ].join(""); + } + return operation.description ?? operation.summary; + })(), + route: () => route, + operation: () => operation, + }; + }; + + const escapeReference = + (components: OpenApi.IComponents) => + (visited: Set) => + (input: OpenApi.IJsonSchema): OpenApi.IJsonSchema | null => { + if (OpenApiTypeChecker.isReference(input)) { + // REFERENCE + const name: string = input.$ref.split("#/components/schemas/")[1]; + const target: OpenApi.IJsonSchema | undefined = + components.schemas?.[name]; + if (!target) return null; + else if (visited.has(name)) return null; + return escapeReference(components)(new Set([...visited, name]))(target); + } else if (OpenApiTypeChecker.isOneOf(input)) { + // ONE-OF + const oneOf: Array = input.oneOf.map( + (schema) => escapeReference(components)(visited)(schema)!, + ); + if (oneOf.some((v) => v === null)) return null; + return { + ...input, + oneOf: oneOf as OpenApi.IJsonSchema[], + }; + } else if (OpenApiTypeChecker.isObject(input)) { + // OBJECT + const properties: + | Array<[string, OpenApi.IJsonSchema | null]> + | undefined = input.properties + ? Object.entries(input.properties).map( + ([key, value]) => + [key, escapeReference(components)(visited)(value)] as const, + ) + : undefined; + const additionalProperties: + | OpenApi.IJsonSchema + | null + | boolean + | undefined = input.additionalProperties + ? typeof input.additionalProperties === "object" && + input.additionalProperties !== null + ? escapeReference(components)(visited)(input.additionalProperties) + : input.additionalProperties + : undefined; + if (properties && properties.some(([_k, v]) => v === null)) return null; + else if (additionalProperties === null) return null; + return { + ...input, + properties: properties + ? Object.fromEntries( + properties.filter(([_k, v]) => !!v) as Array< + [string, OpenApi.IJsonSchema] + >, + ) + : undefined, + additionalProperties, + }; + } else if (OpenApiTypeChecker.isTuple(input)) { + // TUPLE + const prefixItems: Array = + input.prefixItems.map((schema) => + escapeReference(components)(visited)(schema), + ); + const additionalItems: + | OpenApi.IJsonSchema + | null + | boolean + | undefined = + typeof input.additionalItems === "object" && + input.additionalItems !== null + ? escapeReference(components)(visited)(input.additionalItems) + : input.additionalItems; + if (prefixItems.some((v) => v === null)) return null; + else if (additionalItems === null) return null; + return { + ...input, + prefixItems: prefixItems as OpenApi.IJsonSchema[], + additionalItems, + }; + } else if (OpenApiTypeChecker.isArray(input)) { + // ARRAY + const items: OpenApi.IJsonSchema | null = escapeReference(components)( + visited, + )(input.items); + if (items === null) return null; + return { + ...input, + items, + }; + } + return input; + }; +} diff --git a/src/converters/LlmSchemaSeparator.ts b/src/converters/LlmSchemaSeparator.ts new file mode 100644 index 0000000..e0283fc --- /dev/null +++ b/src/converters/LlmSchemaSeparator.ts @@ -0,0 +1,92 @@ +import { ILlmProcedure } from "../structures/ILlmProcedure"; +import { ILlmSchema } from "../structures/ILlmSchema"; +import { LlmTypeChecker } from "../utils/LlmTypeChecker"; + +export namespace LlmSchemaSeparator { + export interface IProps { + parameters: ILlmSchema[]; + predicator: (schema: ILlmSchema) => boolean; + } + export const parameters = (props: IProps): ILlmProcedure.ISeparated => { + const indexes: Array<[ILlmSchema | null, ILlmSchema | null]> = + props.parameters.map(schema(props.predicator)); + return { + llm: indexes + .map(([llm], index) => ({ + index, + schema: llm!, + })) + .filter(({ schema }) => schema !== null), + human: indexes + .map(([, human], index) => ({ + index, + schema: human!, + })) + .filter(({ schema }) => schema !== null), + }; + }; + + export const schema = + (predicator: (schema: ILlmSchema) => boolean) => + (input: ILlmSchema): [ILlmSchema | null, ILlmSchema | null] => { + if (predicator(input) === true) return [null, input]; + else if (LlmTypeChecker.isUnknown(input) || LlmTypeChecker.isOneOf(input)) + return [input, null]; + else if (LlmTypeChecker.isObject(input)) + return separateObject(predicator)(input); + else if (LlmTypeChecker.isArray(input)) + return separateArray(predicator)(input); + return [input, null]; + }; + + const separateArray = + (predicator: (schema: ILlmSchema) => boolean) => + ( + input: ILlmSchema.IArray, + ): [ILlmSchema.IArray | null, ILlmSchema.IArray | null] => { + const [x, y] = schema(predicator)(input.items); + return [ + x !== null ? { ...input, items: x } : null, + y !== null ? { ...input, items: y } : null, + ]; + }; + + const separateObject = + (predicator: (schema: ILlmSchema) => boolean) => + ( + input: ILlmSchema.IObject, + ): [ILlmSchema.IObject | null, ILlmSchema.IObject | null] => { + if ( + !!input.additionalProperties || + Object.keys(input.properties ?? {}).length === 0 + ) + return [input, null]; + const llm = { + ...input, + properties: {} as Record, + } satisfies ILlmSchema.IObject; + const human = { + ...input, + properties: {} as Record, + } satisfies ILlmSchema.IObject; + for (const [key, value] of Object.entries(input.properties ?? {})) { + const [x, y] = schema(predicator)(value); + if (x !== null) llm.properties[key] = x; + if (y !== null) human.properties[key] = y; + } + return [ + Object.keys(llm.properties).length === 0 ? null : shrinkRequired(llm), + Object.keys(human.properties).length === 0 + ? null + : shrinkRequired(human), + ]; + }; + + const shrinkRequired = (input: ILlmSchema.IObject): ILlmSchema.IObject => { + if (input.required !== undefined) + input.required = input.required.filter( + (key) => input.properties?.[key] !== undefined, + ); + return input; + }; +} diff --git a/src/internal/MigrateConverter.ts b/src/converters/MigrateConverter.ts similarity index 92% rename from src/internal/MigrateConverter.ts rename to src/converters/MigrateConverter.ts index b4b352a..6796533 100644 --- a/src/internal/MigrateConverter.ts +++ b/src/converters/MigrateConverter.ts @@ -1,9 +1,9 @@ -import { IMigrateRoute } from "../IMigrateRoute"; -import { IMigrateDocument } from "../IMigrateDocument"; import { OpenApi } from "../OpenApi"; +import { IMigrateDocument } from "../structures/IMigrateDocument"; +import { IMigrateRoute } from "../structures/IMigrateRoute"; import { StringUtil } from "../utils/StringUtil"; -import { MigrateRouteConverter } from "./MigrateRouteConverter"; import { MigrateRouteAccessor } from "./MigrateRouteAccessor"; +import { MigrateRouteConverter } from "./MigrateRouteConverter"; export namespace MigrateConverter { export const convert = < @@ -49,6 +49,7 @@ export namespace MigrateConverter { ); MigrateRouteAccessor.overwrite(operations); return { + document: () => document, routes: operations, errors, }; diff --git a/src/internal/MigrateRouteAccessor.ts b/src/converters/MigrateRouteAccessor.ts similarity index 98% rename from src/internal/MigrateRouteAccessor.ts rename to src/converters/MigrateRouteAccessor.ts index 75821fd..7bf1bc8 100644 --- a/src/internal/MigrateRouteAccessor.ts +++ b/src/converters/MigrateRouteAccessor.ts @@ -1,5 +1,5 @@ -import { IMigrateRoute } from "../IMigrateRoute"; import { OpenApi } from "../OpenApi"; +import { IMigrateRoute } from "../structures/IMigrateRoute"; import { Escaper } from "../utils/Escaper"; import { MapUtil } from "../utils/MapUtil"; import { StringUtil } from "../utils/StringUtil"; diff --git a/src/internal/MigrateRouteConverter.ts b/src/converters/MigrateRouteConverter.ts similarity index 99% rename from src/internal/MigrateRouteConverter.ts rename to src/converters/MigrateRouteConverter.ts index 900df8e..bef0ad0 100644 --- a/src/internal/MigrateRouteConverter.ts +++ b/src/converters/MigrateRouteConverter.ts @@ -1,6 +1,6 @@ -import { IMigrateRoute } from "../IMigrateRoute"; import { OpenApi } from "../OpenApi"; import { OpenApiTypeChecker } from "../OpenApiTypeChecker"; +import { IMigrateRoute } from "../structures/IMigrateRoute"; import { Escaper } from "../utils/Escaper"; import { StringUtil } from "../utils/StringUtil"; diff --git a/src/internal/OpenApiTypeChecker.ts b/src/converters/OpenApiTypeChecker.ts similarity index 100% rename from src/internal/OpenApiTypeChecker.ts rename to src/converters/OpenApiTypeChecker.ts diff --git a/src/internal/OpenApiV3Converter.ts b/src/converters/OpenApiV3Converter.ts similarity index 100% rename from src/internal/OpenApiV3Converter.ts rename to src/converters/OpenApiV3Converter.ts diff --git a/src/internal/OpenApiV3Downgrader.ts b/src/converters/OpenApiV3Downgrader.ts similarity index 100% rename from src/internal/OpenApiV3Downgrader.ts rename to src/converters/OpenApiV3Downgrader.ts diff --git a/src/internal/OpenApiV3_1Converter.ts b/src/converters/OpenApiV3_1Converter.ts similarity index 100% rename from src/internal/OpenApiV3_1Converter.ts rename to src/converters/OpenApiV3_1Converter.ts diff --git a/src/internal/SwaggerV2Converter.ts b/src/converters/SwaggerV2Converter.ts similarity index 100% rename from src/internal/SwaggerV2Converter.ts rename to src/converters/SwaggerV2Converter.ts diff --git a/src/internal/SwaggerV2Downgrader.ts b/src/converters/SwaggerV2Downgrader.ts similarity index 100% rename from src/internal/SwaggerV2Downgrader.ts rename to src/converters/SwaggerV2Downgrader.ts diff --git a/src/index.ts b/src/index.ts index 1681d00..a45cbc9 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,5 @@ -export * from "./IMigrateRoute"; -export * from "./IMigrateDocument"; +export * from "./structures/IMigrateRoute"; +export * from "./structures/IMigrateDocument"; export * from "./OpenApi"; export * from "./OpenApiTypeChecker"; diff --git a/src/structures/ILlmDocument.ts b/src/structures/ILlmDocument.ts new file mode 100644 index 0000000..eab6d4f --- /dev/null +++ b/src/structures/ILlmDocument.ts @@ -0,0 +1,153 @@ +import { OpenApi } from "../OpenApi"; +import { ILlmProcedure } from "./ILlmProcedure"; +import { ILlmSchema } from "./ILlmSchema"; +import { IMigrateRoute } from "./IMigrateRoute"; + +export interface ILlmDocument< + Schema extends ILlmSchema = ILlmSchema, + Operation extends OpenApi.IOperation = OpenApi.IOperation, + Route extends IMigrateRoute = IMigrateRoute, +> { + /** + * Version of OpenAPI. + * + * LLM function call schema is based on OpenAPI 3.0.3 specification. + */ + openapi: "3.0.3"; + + /** + * List of function metadata. + * + * List of function metadata that can be used for the LLM function call. + * + * When you want to execute the function with LLM constructed arguments, + * you can do it through {@link LlmFetcher.execute} function. + */ + functions: ILlmProcedure[]; + + /** + * List of errors occurred during the composition. + */ + errors: ILlmDocument.IError[]; + + /** + * Options for the document. + * + * Adjusted options when composing the document through + * {@link OpenApi.llm} function. + */ + options: ILlmDocument.IOptions; +} +export namespace ILlmDocument { + /** + * Error occurred in the composition. + */ + export interface IError< + Operation extends OpenApi.IOperation = OpenApi.IOperation, + Route extends IMigrateRoute = IMigrateRoute, + > { + /** + * HTTP method of the endpoint. + */ + method: "get" | "post" | "put" | "patch" | "delete" | "head"; + + /** + * Path of the endpoint. + */ + path: string; + + /** + * Error messsages. + */ + messages: string[]; + + /** + * Get the Swagger operation metadata. + * + * Get the Swagger operation metadata, of the source. + */ + operation: () => Operation; + + /** + * Get the migration route metadata. + * + * Get the migration route metadata, of the source. + * + * If the property returns `undefined`, it means that the error has + * been occured in the migration level, not of LLM document composition. + * + * @returns Migration route metadata. + */ + route: () => Route | undefined; + } + + /** + * Options for composing the LLM document. + */ + export interface IOptions { + /** + * Whether the parameters are keyworded or not. + * + * If this property value is `true`, length of the + * {@link ILlmDocument.IFunction.parameters} is always 1, and type of + * the pararameter is always {@link ILlmSchema.IObject} type. + * Also, its properties are following below rules: + * + * - `pathParameters`: Path parameters of {@link IMigrateRoute.parameters} + * - `query`: Query parameter of {@link IMigrateRoute.query} + * - `body`: Body parameter of {@link IMigrateRoute.body} + * + * ```typescript + * { + * ...pathParameters, + * query, + * body, + * } + * ``` + * + * Otherwise (this property value is `false`), length of the + * {@link ILlmProcedure.parameters} is variable, and sequence of the + * parameters are following below rules. + * + * ```typescript + * [ + * ...pathParameters, + * ...(query ? [query] : []), + * ...(body ? [body] : []), + * ] + * ``` + * + * @default false + */ + keyword: boolean; + + /** + * Separator function for the parameters. + * + * When composing parameter arguments through LLM function call, + * there can be a case that some parameters must be composed by human, + * or LLM cannot understand the parameter. For example, if the + * parameter type has configured + * {@link ILlmSchema.IString.contentMediaType} which indicates file + * uploading, it must be composed by human, not by LLM + * (Large Language Model). + * + * In that case, if you configure this property with a function that + * predicating whether the schema value must be composed by human or + * not, the parameters would be separated into two parts. + * + * - {@link ILlmProcedure.separated.llm} + * - {@link ILlmProcedure.separated.human} + * + * When writing the function, note that returning value `true` means + * to be a human composing the value, and `false` means to LLM + * composing the value. Also, when predicating the schema, it would + * better to utilize the {@link LlmTypeChecker} features. + * + * @param schema Schema to be separated. + * @returns Whether the schema value must be composed by human or not. + * @default null + */ + separate: null | ((schema: Schema) => boolean); + } +} diff --git a/src/structures/ILlmFunction.ts b/src/structures/ILlmFunction.ts new file mode 100644 index 0000000..fa5cc31 --- /dev/null +++ b/src/structures/ILlmFunction.ts @@ -0,0 +1,56 @@ +import { ILlmSchema } from "./ILlmSchema"; + +/** + * LLM function metadata. + * + * `ILlmFunction` is an interface representing a function metadata, + * which has been used for the LLM (Language Large Model) function + * calling. Also, it's a function structure containing the function + * {@link name}, {@link parameters} and {@link output return type}. + * + * If you provide this `ILlmFunction` data to the LLM like "OpenAI", + * the "OpenAI" will compose a function arguments by analyzing + * conversations with the user. With the LLM composed arguments, you can + * execute the function and get the result. + * + * By the way, do not sure that LLM will always provide the correct + * arguments. The LLM of present age is not perfect, so that you would + * better to validate the arguments before executing the function. + * I recommend you to validate the arguments before execution by using + * [`typia`](https://github.com/samchon/typia) library. + * + * @reference https://platform.openai.com/docs/guides/function-calling + * @author Jeongho Nam - https://github.com/samchon + */ +export interface ILlmFunction { + /** + * Representative name of the function. + */ + name: string; + + /** + * List of parameter types. + */ + parameters: Schema[]; + + /** + * Expected return type. + * + * If the function returns nothing (`void`), the `output` value would + * be `undefined`. + */ + output?: Schema | undefined; + + /** + * Description of the function. + * + * For reference, the `description` is very important property to teach + * the purpose of the function to the LLM (Language Large Model), and + * LLM actually determines which function to call by the description. + * + * Also, when the LLM conversates with the user, the `description` is + * used to explain the function to the user. Therefore, the `description` + * property has the highest priroity, and you have to consider it. + */ + description?: string | undefined; +} diff --git a/src/structures/ILlmProcedure.ts b/src/structures/ILlmProcedure.ts new file mode 100644 index 0000000..57f3cf7 --- /dev/null +++ b/src/structures/ILlmProcedure.ts @@ -0,0 +1,239 @@ +import { OpenApi } from "../OpenApi"; +import { ILlmSchema } from "./ILlmSchema"; +import { IMigrateRoute } from "./IMigrateRoute"; + +/** + * LLM procedure metadata from OpenAPI operation. + * + * `ILlmProcedure` is a data structure representing a procedure converted + * from the OpenAPI operation, used for the LLM (Large Language Model) + * function calling. It's a typical RPC (Remote Procedure Call) structure + * containing the procedure {@link name}, {@link parameters}, and + * {@link output return type}. + * + * If you provide this `ILlmProcedure` data to the LLM like "OpenAI", + * the "OpenAI" will compose a function arguments by analyzing + * conversations with the user. With the LLM composed arguments, you can + * execute the procedure through {@link LlmFetcher.execute} and get the + * result. + * + * For reference, different between `ILlmProcedure` and its origin source + * {@link OpenApi.IOperation} is, `ILlmProcedure` has converted every type + * schema informations from {@link OpenApi.IJsonSchema} to {@link ILlmSchema} + * to escape {@link OpenApi.IJsonSchema.IReference reference types}, and + * downgrade the version of the JSON schema to OpenAPI 3.0. It's because + * LLM function call feature cannot understand both reference types and + * OpenAPI 3.1 specification. + * + * Additionally, if you've composed `ILlmProcedure` with + * {@link ILlmDocument.IOptions.keyword} configuration as `true`, number of + * {@link ILlmProcedure.parameters} are always 1 and the first parameter's + * type is always {@link ILlmSchema.IObject}. The properties' rule is: + * + * - `pathParameters`: Path parameters of {@link OpenApi.IOperation.parameters} + * - `query`: Query parameter of {@link IMigrateRoute.query} + * - `body`: Body parameter of {@link IMigrateRoute.body} + * + * ```typescript + * { + * ...pathParameters, + * query, + * body, + * } + * ``` + * + * Otherwise, the parameters would be multiple, and the sequence of the parameters + * are following below rules: + * + * ```typescript + * [ + * ...pathParameters, + * ...(query ? [query] : []), + * ...(body ? [body] : []), + * ] + * ``` + * + * @reference https://platform.openai.com/docs/guides/function-calling + * @author Jeongho Nam - https://github.com/samchon + */ +export interface ILlmProcedure< + Schema extends ILlmSchema = ILlmSchema, + Operation extends OpenApi.IOperation = OpenApi.IOperation, + Route extends IMigrateRoute = IMigrateRoute, +> { + /** + * HTTP method of the endpoint. + */ + method: "get" | "post" | "patch" | "put" | "delete"; + + /** + * Path of the endpoint. + */ + path: string; + + /** + * Representative name of the function. + * + * The `name` is a repsentative name identifying the function in the + * {@link ILlmDocument}. The `name` value is just composed by joining the + * {@link IMigrateRoute.accessor} by underscore `_` character. + * + * Here is the composition rule of the {@link IMigrateRoute.accessor}: + * + * > The `accessor` is composed with the following rules. At first, + * > namespaces are composed by static directory names in the {@link path}. + * > Parametric symbols represented by `:param` or `{param}` cannot be + * > a part of the namespace. + * > + * > Instead, they would be a part of the function name. The function + * > name is composed with the {@link method HTTP method} and parametric + * > symbols like `getByParam` or `postByParam`. If there are multiple + * > path parameters, they would be concatenated by `And` like + * > `getByParam1AndParam2`. + * > + * > For refefence, if the {@link operation}'s {@link method} is `delete`, + * > the function name would be replaced to `erase` instead of `delete`. + * > It is the reason why the `delete` is a reserved keyword in many + * > programming languages. + * > + * > - Example 1 + * > - path: `POST /shopping/sellers/sales` + * > - accessor: `shopping.sellers.sales.post` + * > - Example 2 + * > - endpoint: `GET /shoppings/sellers/sales/:saleId/reviews/:reviewId/comments/:id + * > - accessor: `shoppings.sellers.sales.reviews.getBySaleIdAndReviewIdAndCommentId` + */ + name: string; + + /** + * Whether the function schema types are strict or not. + * + * Newly added specification to "OpenAI" at 2024-08-07. + * + * @reference https://openai.com/index/introducing-structured-outputs-in-the-api/ + */ + strict: true; + + /** + * List of parameter types. + * + * If you've configured {@link ILlmDocument.IOptions.keyword} as `true`, + * number of {@link ILlmProcedure.parameters} are always 1 and the first + * parameter's type is always {@link ILlmSchema.IObject}. The + * properties' rule is: + * + * - `pathParameters`: Path parameters of {@link IMigrateRoute.parameters} + * - `query`: Query parameter of {@link IMigrateRoute.query} + * - `body`: Body parameter of {@link IMigrateRoute.body} + * + * ```typescript + * { + * ...pathParameters, + * query, + * body, + * } + * ``` + * + * Otherwise, the parameters would be multiple, and the sequence of the + * parameters are following below rules: + * + * ```typescript + * [ + * ...pathParameters, + * ...(query ? [query] : []), + * ...(body ? [body] : []), + * ] + * ``` + */ + parameters: Schema[]; + + /** + * Collection of separated parameters. + * + * Filled only when {@link ILlmDocument.IOptions.separate} is configured. + */ + separated?: ILlmProcedure.ISeparated; + + /** + * Expected return type. + * + * If the target operation returns nothing (`void`), the `output` + * would be `undefined`. + */ + output?: Schema | undefined; + + /** + * Description of the procedure. + * + * `ILlmProcedure.description` is composed by below rule: + * + * 1. Starts from the {@link OpenApi.IOperation.summary} paragraph. + * 2. The next paragraphs are filled with the + * {@link OpenApi.IOperation.description}. By the way, if the first + * paragraph of {@link OpenApi.IOperation.description} is same with the + * {@link OpenApi.IOperation.summary}, it would not be duplicated. + * 3. Parameters' descriptions are added with `@param` tag. + * 4. {@link OpenApi.IOperation.security Security requirements} are added + * with `@security` tag. + * 5. Tag names are added with `@tag` tag. + * 6. If {@link OpenApi.IOperation.deprecated}, `@deprecated` tag is added. + * + * For reference, the `description` is very important property to teach + * the purpose of the function to the LLM (Language Large Model), and + * LLM actually determines which function to call by the description. + * + * Also, when the LLM conversates with the user, the `description` is + * used to explain the function to the user. Therefore, the `description` + * property has the highest priroity, and you have to consider it. + */ + description?: string; + + /** + * Get the Swagger operation metadata. + * + * Get the Swagger operation metadata, of the source. + * + * @returns Swagger operation metadata. + */ + operation: () => Operation; + + /** + * Get the migration route metadata. + * + * Get the migration route metadata, of the source. + * + * @returns Migration route metadata. + */ + route: () => Route; +} +export namespace ILlmProcedure { + /** + * Collection of separated parameters. + */ + export interface ISeparated { + /** + * Parameters that would be composed by the LLM. + */ + llm: ISeparatedParameter[]; + + /** + * Parameters that would be composed by the human. + */ + human: ISeparatedParameter[]; + } + + /** + * Separated parameter. + */ + export interface ISeparatedParameter { + /** + * Index of the parameter. + */ + index: number; + + /** + * Type schema info of the parameter. + */ + schema: Schema; + } +} diff --git a/src/structures/ILlmSchema.ts b/src/structures/ILlmSchema.ts new file mode 100644 index 0000000..e00ef7c --- /dev/null +++ b/src/structures/ILlmSchema.ts @@ -0,0 +1,530 @@ +/** + * Type schema info of LLM function call. + * + * `ILlmSchema` is a type metadata of LLM (Large Language Model) + * function calling. + * + * `ILlmSchema` basically follows the JSON schema definition of OpenAPI + * v3.0 specification; {@link OpenApiV3.IJsonSchema}. However, `ILlmSchema` + * does not have the reference type; {@link OpenApiV3.IJsonSchema.IReference}. + * It's because the LLM cannot compose the reference typed arguments. + * + * For reference, the OpenAPI v3.0 based JSON schema definition can't + * express the tuple array type. It has been supported since OpenAPI v3.1; + * {@link OpenApi.IJsonSchema.ITuple}. Therefore, it would better to avoid + * using the tuple array type in the LLM function calling. + * + * @reference https://platform.openai.com/docs/guides/function-calling + * @author Jeongho Nam - https://github.com/samchon + */ +export type ILlmSchema = + | ILlmSchema.IBoolean + | ILlmSchema.IInteger + | ILlmSchema.INumber + | ILlmSchema.IString + | ILlmSchema.IArray + | ILlmSchema.IObject + | ILlmSchema.IUnknown + | ILlmSchema.INullOnly + | ILlmSchema.IOneOf; +export namespace ILlmSchema { + /** + * Boolean type schema info. + */ + export interface IBoolean extends __ISignificant<"boolean"> { + /** + * Default value. + */ + default?: boolean; + + /** + * Enumeration values. + */ + enum?: boolean[]; + } + + /** + * Integer type schema info. + */ + export interface IInteger extends __ISignificant<"integer"> { + /** + * Default value. + * + * @type int64 + */ + default?: number; + + /** + * Enumeration values. + * + * @type int64 + */ + enum?: number[]; + + /** + * Minimum value restriction. + * + * @type int64 + */ + minimum?: number; + + /** + * Maximum value restriction. + * + * @type int64 + */ + maximum?: number; + + /** + * Exclusive minimum value restriction. + * + * For reference, even though your Swagger document has defined the + * `exclusiveMinimum` value as `number`, it has been forcibly converted + * to `boolean` type, and assigns the numeric value to the + * {@link minimum} property in the {@link OpenApi} conversion. + */ + exclusiveMinimum?: boolean; + + /** + * Exclusive maximum value restriction. + * + * For reference, even though your Swagger document has defined the + * `exclusiveMaximum` value as `number`, it has been forcibly converted + * to `boolean` type, and assigns the numeric value to the + * {@link maximum} property in the {@link OpenApi} conversion. + */ + exclusiveMaximum?: boolean; + + /** + * Multiple of value restriction. + * + * @type uint64 + * @exclusiveMinimum 0 + */ + multipleOf?: number; + } + + /** + * Number type schema info. + */ + export interface INumber extends __ISignificant<"number"> { + /** + * Default value. + */ + default?: number; + + /** + * Enumeration values. + */ + enum?: number[]; + + /** + * Minimum value restriction. + */ + minimum?: number; + + /** + * Maximum value restriction. + */ + maximum?: number; + + /** + * Exclusive minimum value restriction. + * + * For reference, even though your Swagger (or OpenAPI) document has + * defined the `exclusiveMinimum` value as `number`, {@link OpenAiComposer} + * forcibly converts it to `boolean` type, and assign the numeric value to + * the {@link minimum} property. + */ + exclusiveMinimum?: boolean; + + /** + * Exclusive maximum value restriction. + * + * For reference, even though your Swagger (or OpenAPI) document has + * defined the `exclusiveMaximum` value as `number`, {@link OpenAiComposer} + * forcibly converts it to `boolean` type, and assign the numeric value to + * the {@link maximum} property. + */ + exclusiveMaximum?: boolean; + + /** + * Multiple of value restriction. + * + * @exclusiveMinimum 0 + */ + multipleOf?: number; + } + + /** + * String type schema info. + */ + export interface IString extends __ISignificant<"string"> { + /** + * Default value. + */ + default?: string; + + /** + * Enumeration values. + */ + enum?: string[]; + + /** + * Format restriction. + */ + format?: + | "binary" + | "byte" + | "password" + | "regex" + | "uuid" + | "email" + | "hostname" + | "idn-email" + | "idn-hostname" + | "iri" + | "iri-reference" + | "ipv4" + | "ipv6" + | "uri" + | "uri-reference" + | "uri-template" + | "url" + | "date-time" + | "date" + | "time" + | "duration" + | "json-pointer" + | "relative-json-pointer" + | (string & {}); + + /** + * Pattern restriction. + */ + pattern?: string; + + /** + * Minimum length restriction. + * + * @type uint64 + */ + minLength?: number; + + /** + * Maximum length restriction. + * + * @type uint64 + */ + maxLength?: number; + + /** + * Content media type restriction. + */ + contentMediaType?: string; + + /** + * Secret key for the schema. + * + * `x-wrtn-secret-key` is a property means a secret key that is required + * for the target API endpoint calling. If the secret key is not filled, + * the API call would be failed. + */ + "x-wrtn-secret-key"?: string; + + /** + * Secret scopes for the schema. + * + * `x-wrtn-secret-scopes` is a property means a list of secret scopes that + * are required for the target API endpoint calling. If the secret scopes + * are not satisfied, the API call would be failed. + */ + "x-wrtn-secret-scopes"?: string[]; + } + + /** + * Array type schema info. + */ + export interface IArray extends __ISignificant<"array"> { + /** + * Items type schema info. + * + * The `items` means the type of the array elements. In other words, it is + * the type schema info of the `T` in the TypeScript array type `Array`. + */ + items: ILlmSchema; + + /** + * Unique items restriction. + * + * If this property value is `true`, target array must have unique items. + */ + uniqueItems?: boolean; + + /** + * Minimum items restriction. + * + * Restriction of minumum number of items in the array. + * + * @type uint64 + */ + minItems?: number; + + /** + * Maximum items restriction. + * + * Restriction of maximum number of items in the array. + * + * @type uint64 + */ + maxItems?: number; + } + + /** + * Object type schema info. + */ + export interface IObject extends __ISignificant<"object"> { + /** + * Properties of the object. + * + * The `properties` means a list of key-value pairs of the object's + * regular properties. The key is the name of the regular property, + * and the value is the type schema info. + * + * If you need additional properties that is represented by dynamic key, + * you can use the {@link additionalProperties} instead. + */ + properties?: Record; + + /** + * List of key values of the required properties. + * + * The `required` means a list of the key values of the required + * {@link properties}. If some property key is not listed in the `required` + * list, it means that property is optional. Otherwise some property key + * exists in the `required` list, it means that the property must be filled. + * + * Below is an example of the {@link properties} and `required`. + * + * ```typescript + * interface SomeObject { + * id: string; + * email: string; + * name?: string; + * } + * ``` + * + * As you can see, `id` and `email` {@link properties} are {@link required}, + * so that they are listed in the `required` list. + * + * ```json + * { + * "type": "object", + * "properties": { + * "id": { "type": "string" }, + * "email": { "type": "string" }, + * "name": { "type": "string" } + * }, + * "required": ["id", "email"] + * } + * ``` + */ + required?: string[]; + + /** + * Additional properties' info. + * + * The `additionalProperties` means the type schema info of the additional + * properties that are not listed in the {@link properties}. + * + * If the value is `true`, it means that the additional properties are not + * restricted. They can be any type. Otherwise, if the value is + * {@link ILlmSchema} type, it means that the additional properties must + * follow the type schema info. + * + * - `true`: `Record` + * - `ILlmSchema`: `Record` + */ + additionalProperties?: boolean | ILlmSchema; + } + + /** + * Unknown type schema info. + * + * It means the type of the value is `any`. + */ + export interface IUnknown extends __IAttribute { + /** + * Type is never be defined. + */ + type?: undefined; + } + + /** + * Null only type schema info. + */ + export interface INullOnly extends __IAttribute { + /** + * Type is always `null`. + */ + type: "null"; + } + + /** + * One of type schema info. + * + * `IOneOf` represents an union type of the TypeScript (`A | B | C`). + * + * For reference, even though your Swagger (or OpenAPI) document has + * defined `anyOf` instead of the `oneOf`, it has been forcibly converted + * to `oneOf` type by {@link OpenApi.convert OpenAPI conversion}. + */ + export interface IOneOf extends __IAttribute { + /** + * List of the union types. + */ + oneOf: ILlmSchema[]; + } + + /** + * Significant attributes that can be applied to the most types. + */ + export interface __ISignificant extends __IAttribute { + /** + * Discriminator value of the type. + */ + type: Type; + + /** + * Whether to allow `null` value or not. + */ + nullable?: boolean; + } + + /** + * Common attributes that can be applied to all types. + */ + export interface __IAttribute { + /** + * Title of the schema. + */ + title?: string; + + /** + * Detailed description of the schema. + */ + description?: string; + + /** + * Whether the type is deprecated or not. + */ + deprecated?: boolean; + + /** + * Placeholder value for frontend application. + * + * Placeholder means the value to be shown in the input field as a hint. + * For example, when an email input field exists, the placeholder value + * would be "Insert your email address here". + */ + "x-wrtn-placeholder"?: string; + + /** + * Prerequisite API endpoint for the schema. + * + * `x-wrtn-prerequisite` is a property representing the prerequisite API + * interaction. It means that, the endpoint API should be called before + * calling the target API, for composing some argument value. + * + * @reference https://github.com/wrtnio/decorators/blob/main/src/Prerequisite.ts + */ + "x-wrtn-prerequisite"?: { + /** + * HTTP method to call the endpoint. + */ + method: "get" | "post" | "patch" | "put" | "delete"; + + /** + * Path of the endpoint. + */ + path: string; + } & ( + | { + /** + * Function returning transformed values using JMESPath expression. + * + * `Prerequisite.Props.jmesPath` is a string typed property that extracts desired values + * from the prerequisite API response using a JMESPath expression. This property simplifies + * and replaces the `label`, `value`, and `array` properties. + * + * JMESPath expressions are used to extract the desired data based on the API response. + * The expression must always be a valid JMESPath syntax. + * + * - Type: `jmesPath: string` + * - Example: `"members[*].data.title"` + * - Usage: `jmespath.search(response, jmesPath)` + * + * Note: The `label`, `value`, and `array` properties are no longer in use. + */ + jmesPath: string; + } + | { + /** + * Transform function returning label. + * + * `Prerequisite.Props.label` is a string typed property representing + * a function returning the label from the element instance of the + * prerequisite API respond array. + * + * The function script must be a string value that can be parsed by + * `new Function(string)` statement. Also, its parameter names are + * always `elem`, `index` and `array`. Of course, the function's + * return type must be always `string`. + * + * - type: `label: (elem: Element, index: number, array: Element[]) => string` + * - example: `return elem.title` + * - how to use: `new Function("elem", "index", "array", labelScript)(...)` + */ + label: string; + + /** + * Transform function returning target value. + * + * `Prerequisite.Props.value` is a string typed property representing + * a function returning the target value from the element instance of + * the prerequisite API respond array. If you've defined this `Prerequisite` + * type to a `number` type, the returned value must be actual number type. + * + * The function script must be a string value that can be parsed by + * `new Function(string)` statement. Also, its parameter names are always + * `elem`, `index` and `array`. + * + * - type: `value: (elem: Element, index: number, array: Element[]) => Value` + * - example: `return elem.no` + * - how to use: `new Function("elem", "index", "array", valueScript)(...)` + */ + value: string; + + /** + * Transform function returning array instance. + * + * `Prerequisite.Props.array` is a string typed property representing + * a function returning an array instance from the response of the + * prerequisite API. + * + * The function script must be a string value that can be parsed by + * `new Function(string)` statement. Also, its parameter name is + * always `response`. + * + * If the prerequisite API responses an array and it is the desired one, + * you don't need to specify this property. + * + * - type: `array: (response: Response) => Elemenet[]` + * - example: `return response.members.map(m => m.data)` + * - how to use: `new Function("response", arrayScript)(response)` + */ + array?: string; + } + ); + } +} diff --git a/src/IMigrateDocument.ts b/src/structures/IMigrateDocument.ts similarity index 92% rename from src/IMigrateDocument.ts rename to src/structures/IMigrateDocument.ts index 89d7230..57bb48e 100644 --- a/src/IMigrateDocument.ts +++ b/src/structures/IMigrateDocument.ts @@ -1,5 +1,5 @@ +import { OpenApi } from "../OpenApi"; import { IMigrateRoute } from "./IMigrateRoute"; -import { OpenApi } from "./OpenApi"; /** * Document of migration. @@ -27,6 +27,11 @@ export interface IMigrateDocument< * List of errors occurred during the migration. */ errors: IMigrateDocument.IError[]; + + /** + * Source OpenAPI document. + */ + document: () => OpenApi.IDocument; } export namespace IMigrateDocument { /** diff --git a/src/IMigrateRoute.ts b/src/structures/IMigrateRoute.ts similarity index 99% rename from src/IMigrateRoute.ts rename to src/structures/IMigrateRoute.ts index bd297d7..64000cb 100644 --- a/src/IMigrateRoute.ts +++ b/src/structures/IMigrateRoute.ts @@ -1,4 +1,4 @@ -import { OpenApi } from "./OpenApi"; +import { OpenApi } from "../OpenApi"; /** * Route information for migration. diff --git a/src/utils/LlmFetcher.ts b/src/utils/LlmFetcher.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/utils/LlmTypeChecker.ts b/src/utils/LlmTypeChecker.ts new file mode 100644 index 0000000..8381d81 --- /dev/null +++ b/src/utils/LlmTypeChecker.ts @@ -0,0 +1,132 @@ +import { ILlmSchema } from "../structures/ILlmSchema"; + +/** + * Type checker for LLM function calling schema. + * + * `LlmTypeChecker` is a type checker of {@link ILlmSchema}. + * + * @author Samchon + */ +export namespace LlmTypeChecker { + /** + * Visit every nested schemas. + * + * Visit every nested schemas of the target, and apply the callback function + * to them. + * + * If the visitor meets an union type, it will visit every individual schemas + * in the union type. Otherwise meets an object type, it will visit every + * properties and additional properties. If the visitor meets an array type, + * it will visit the item type. + * + * @param schema Target schema to visit + * @param callback Callback function to apply + */ + export const visit = ( + schema: ILlmSchema, + callback: (schema: ILlmSchema) => void, + ): void => { + callback(schema); + if (isOneOf(schema)) schema.oneOf.forEach((s) => visit(s, callback)); + else if (isObject(schema)) { + for (const [_, s] of Object.entries(schema.properties ?? {})) + visit(s, callback); + if ( + typeof schema.additionalProperties === "object" && + schema.additionalProperties !== null + ) + visit(schema.additionalProperties, callback); + } else if (isArray(schema)) visit(schema.items, callback); + }; + + /** + * Test whether the schema is an union type. + * + * @param schema Target schema + * @returns Whether union type or not + */ + export const isOneOf = (schema: ILlmSchema): schema is ILlmSchema.IOneOf => + (schema as ILlmSchema.IOneOf).oneOf !== undefined; + + /** + * Test whether the schema is an object type. + * + * @param schema Target schema + * @returns Whether object type or not + */ + export const isObject = (schema: ILlmSchema): schema is ILlmSchema.IObject => + (schema as ILlmSchema.IObject).type === "object"; + + /** + * Test whether the schema is an array type. + * + * @param schema Target schema + * @returns Whether array type or not + */ + export const isArray = (schema: ILlmSchema): schema is ILlmSchema.IArray => + (schema as ILlmSchema.IArray).type === "array"; + + /** + * Test whether the schema is a boolean type. + * + * @param schema Target schema + * @returns Whether boolean type or not + */ + export const isBoolean = ( + schema: ILlmSchema, + ): schema is ILlmSchema.IBoolean => + (schema as ILlmSchema.IBoolean).type === "boolean"; + + /** + * Test whether the schema is a number type. + * + * @param schema Target schema + * @returns Whether number type or not + */ + export const isNumber = (schema: ILlmSchema): schema is ILlmSchema.INumber => + (schema as ILlmSchema.INumber).type === "number"; + + /** + * Test whether the schema is a string type. + * + * @param schema Target schema + * @returns Whether string type or not + */ + export const isString = (schema: ILlmSchema): schema is ILlmSchema.IString => + (schema as ILlmSchema.IString).type === "string"; + + /** + * Test whether the schema is a null type. + * + * @param schema Target schema + * @returns Whether null type or not + */ + export const isNullOnly = ( + schema: ILlmSchema, + ): schema is ILlmSchema.INullOnly => + (schema as ILlmSchema.INullOnly).type === "null"; + + /** + * Test whether the schema is a nullable type. + * + * @param schema Target schema + * @returns Whether nullable type or not + */ + export const isNullable = (schema: ILlmSchema): boolean => + !isUnknown(schema) && + (isNullOnly(schema) || + (isOneOf(schema) + ? schema.oneOf.some(isNullable) + : schema.nullable === true)); + + /** + * Test whether the schema is an unknown type. + * + * @param schema Target schema + * @returns Whether unknown type or not + */ + export const isUnknown = ( + schema: ILlmSchema, + ): schema is ILlmSchema.IUnknown => + !isOneOf(schema) && (schema as ILlmSchema.IUnknown).type === undefined; +} diff --git a/src/utils/MigrateFetcher.ts b/src/utils/MigrateFetcher.ts new file mode 100644 index 0000000..e69de29 diff --git a/test/features/test_json_schema_downgrade_v20.ts b/test/features/test_json_schema_downgrade_v20.ts index da70757..4002f4a 100644 --- a/test/features/test_json_schema_downgrade_v20.ts +++ b/test/features/test_json_schema_downgrade_v20.ts @@ -1,6 +1,6 @@ import { TestValidator } from "@nestia/e2e"; import { OpenApi, SwaggerV2 } from "@samchon/openapi"; -import { SwaggerV2Downgrader } from "@samchon/openapi/lib/internal/SwaggerV2Downgrader"; +import { SwaggerV2Downgrader } from "@samchon/openapi/lib/converters/SwaggerV2Downgrader"; export const test_json_schema_downgrade_v20 = () => { const schema: OpenApi.IJsonSchema = { diff --git a/test/features/test_json_schema_downgrade_v30.ts b/test/features/test_json_schema_downgrade_v30.ts index 51bf90c..c93af16 100644 --- a/test/features/test_json_schema_downgrade_v30.ts +++ b/test/features/test_json_schema_downgrade_v30.ts @@ -1,6 +1,6 @@ import { TestValidator } from "@nestia/e2e"; import { OpenApi, OpenApiV3 } from "@samchon/openapi"; -import { OpenApiV3Downgrader } from "@samchon/openapi/lib/internal/OpenApiV3Downgrader"; +import { OpenApiV3Downgrader } from "@samchon/openapi/lib/converters/OpenApiV3Downgrader"; export const test_json_schema_downgrade_v30 = () => { const schema: OpenApi.IJsonSchema = { From 3663953cf85eba9d12d94fb52d294da51a66e53b Mon Sep 17 00:00:00 2001 From: Jeongho Nam Date: Sat, 31 Aug 2024 04:51:44 +0900 Subject: [PATCH 02/16] `MigrateRouteFetcher` --- src/fetchers/HttpError.ts | 85 +++++++ src/fetchers/MigrateRouteFetcher.ts | 184 +++++++++++++++ src/structures/IHttpConnection.ts | 216 ++++++++++++++++++ src/structures/IHttpResponse.ts | 5 + .../{LlmFetcher.ts => LlmFunctionFetcher.ts} | 0 src/utils/MigrateFetcher.ts | 0 6 files changed, 490 insertions(+) create mode 100644 src/fetchers/HttpError.ts create mode 100644 src/fetchers/MigrateRouteFetcher.ts create mode 100644 src/structures/IHttpConnection.ts create mode 100644 src/structures/IHttpResponse.ts rename src/utils/{LlmFetcher.ts => LlmFunctionFetcher.ts} (100%) delete mode 100644 src/utils/MigrateFetcher.ts diff --git a/src/fetchers/HttpError.ts b/src/fetchers/HttpError.ts new file mode 100644 index 0000000..fb0f90f --- /dev/null +++ b/src/fetchers/HttpError.ts @@ -0,0 +1,85 @@ +/** + * HTTP Error. + * + * `HttpError` is a type of error class who've been thrown by the remote HTTP server. + * + * @author Jeongho Nam - https://github.com/samchon + */ +export class HttpError extends Error { + /** + * @internal + */ + private body_: any = NOT_YET; + + /** + * Initializer Constructor. + * + * @param method Method of the HTTP request. + * @param path Path of the HTTP request. + * @param status Status code from the remote HTTP server. + * @param message Error message from the remote HTTP server. + */ + public constructor( + public readonly method: + | "GET" + | "DELETE" + | "POST" + | "PUT" + | "PATCH" + | "HEAD", + public readonly path: string, + public readonly status: number, + public readonly headers: Record, + message: string, + ) { + super(message); + + // INHERITANCE POLYFILL + const proto: HttpError = new.target.prototype; + if (Object.setPrototypeOf) Object.setPrototypeOf(this, proto); + else (this as any).__proto__ = proto; + } + + /** + * `HttpError` to JSON. + * + * When you call `JSON.stringify()` function on current `HttpError` instance, + * this `HttpError.toJSON()` method would be automatically called. + * + * Also, if response body from the remote HTTP server forms a JSON object, + * this `HttpError.toJSON()` method would be useful because it returns the + * parsed JSON object about the {@link message} property. + * + * @template T Expected type of the response body. + * @returns JSON object of the `HttpError`. + */ + public toJSON(): HttpError.IProps { + if (this.body_ === NOT_YET) + try { + this.body_ = JSON.parse(this.message); + } catch { + this.body_ = this.message; + } + return { + method: this.method, + path: this.path, + status: this.status, + headers: this.headers, + message: this.body_, + }; + } +} +export namespace HttpError { + /** + * Returned type of {@link HttpError.toJSON} method. + */ + export interface IProps { + method: "GET" | "DELETE" | "POST" | "PUT" | "PATCH" | "HEAD"; + path: string; + status: number; + headers: Record; + message: T; + } +} + +const NOT_YET = {} as any; diff --git a/src/fetchers/MigrateRouteFetcher.ts b/src/fetchers/MigrateRouteFetcher.ts new file mode 100644 index 0000000..273717b --- /dev/null +++ b/src/fetchers/MigrateRouteFetcher.ts @@ -0,0 +1,184 @@ +import { IHttpConnection } from "../structures/IHttpConnection"; +import { IHttpResponse } from "../structures/IHttpResponse"; +import { IMigrateRoute } from "../structures/IMigrateRoute"; +import { HttpError } from "./HttpError"; + +export namespace MigrateRouteFetcher { + export interface IProps { + connection: IHttpConnection; + route: IMigrateRoute; + parameters: Array; + query?: object | undefined; + body?: object | undefined; + } + + export const request = async (props: IProps): Promise => { + const result: IHttpResponse = await _Propagate("request", props); + if (result.status === 200 || result.status === 201) + throw new HttpError( + props.route.method.toUpperCase() as "GET", + props.route.path, + result.status, + result.headers, + result.body as string, + ); + return result.body; + }; + + export const propagate = (props: IProps): Promise => + _Propagate("propagate", props); +} + +const _Propagate = async ( + funcName: string, + props: MigrateRouteFetcher.IProps, +): Promise => { + // VALIDATION + const error = (message: string) => + new Error(`Error on MigrateRouteFetcher.${funcName}(): ${message}`); + if (props.route.parameters.length !== props.parameters.length) + throw error(`number of parameters is not matched.`); + else if (!!props.route.query !== !!props.query) + throw error(`query is not matched.`); + else if (!!props.route.body !== (props.body !== undefined)) + throw error(`body is not matched.`); + + // INIT REQUEST DATA + const headers: Record = { + ...(props.connection.headers ?? {}), + }; + const init: RequestInit = { + ...(props.connection.options ?? {}), + method: props.route.method, + headers: (() => { + const output: [string, string][] = []; + for (const [key, value] of Object.entries(headers)) + if (value === undefined) continue; + else if (Array.isArray(value)) + for (const v of value) output.push([key, String(v)]); + else output.push([key, String(value)]); + return output; + })(), + }; + if (props.body !== undefined) + props.route.body?.type === "application/x-www-form-urlencoded" + ? requestQueryBody(props.body) + : props.route.body?.type === "multipart/form-data" + ? requestFormDataBody(props.body) + : props.route.body?.type !== "text/plain" + ? JSON.stringify(props.body) + : props.body; + + // DO REQUEST + const path: string = + props.connection.host[props.connection.host.length - 1] !== "/" && + props.route.path[0] !== "/" + ? `/${getPath(props)}` + : getPath(props); + const url: URL = new URL(`${props.connection.host}${path}`); + + const response: Response = await (props.connection.fetch ?? fetch)(url, init); + const status: number = response.status; + const out = (body: unknown): IHttpResponse => ({ + status, + headers: responseHeaders(response.headers), + body, + }); + + if (status === 200 || status === 201) { + // SUCCESS CASE + if (props.route.method === "head") return out(undefined); + else if ( + props.route.success === null || + props.route.success.type === "text/plain" + ) + return out(await response.text()); + else if (props.route.success.type === "application/json") { + const text: string = await response.text(); + return out(text.length ? JSON.parse(text) : undefined); + } else if (props.route.success.type === "application/x-www-form-urlencoded") + return out(new URLSearchParams(await response.text())); + else if (props.route.success.type === "multipart/form-data") + return out(await response.formData()); + throw error("Unsupported response body type."); + } else { + // FAILURE CASE + const type: string = ( + response.headers.get("content-type") ?? + response.headers.get("Content-Type") ?? + "" + ) + .split(";")[0] + .trim(); + if (type === "" || type.startsWith("text/")) + return out(await response.text()); + else if (type === "application/json") return out(await response.json()); + else if (type === "application/x-www-form-urlencoded") + return out(new URLSearchParams(await response.text())); + else if (type === "multipart/form-data") + return out(await response.formData()); + else if (type === "application/octet-stream") + return out(await response.blob()); + return out(await response.text()); + } +}; + +const getPath = ( + props: Pick, +): string => { + let path: string = props.route.emendedPath; + props.route.parameters.forEach((p, i) => { + path = path.replace(`:${p.key}`, String(props.parameters[i] ?? "null")); + }); + if (props.route.query) path += getQueryPath(props.query ?? {}); + return path; +}; + +const getQueryPath = (query: Record): string => { + const variables = new URLSearchParams(); + for (const [key, value] of Object.entries(query)) + if (undefined === value) continue; + else if (Array.isArray(value)) + value.forEach((elem: any) => variables.append(key, String(elem))); + else variables.set(key, String(value)); + return 0 === variables.size ? "" : `?${variables.toString()}`; +}; + +const requestQueryBody = (input: any): URLSearchParams => { + const q: URLSearchParams = new URLSearchParams(); + for (const [key, value] of Object.entries(input)) + if (value === undefined) continue; + else if (Array.isArray(value)) + value.forEach((elem) => q.append(key, String(elem))); + else q.set(key, String(value)); + return q; +}; +const requestFormDataBody = (input: Record): FormData => { + const encoded: FormData = new FormData(); + const append = (key: string) => (value: any) => { + if (value === undefined) return; + else if (value instanceof Blob) + if (value instanceof File) encoded.append(key, value, value.name); + else encoded.append(key, value); + else encoded.append(key, String(value)); + }; + for (const [key, value] of Object.entries(input)) + if (Array.isArray(value)) value.map(append(key)); + else append(key)(value); + return encoded; +}; + +const responseHeaders = ( + headers: Headers, +): Record => { + const output: Record = {}; + headers.forEach((value, key) => { + if (key === "set-cookie") { + output[key] ??= []; + (output[key] as string[]).push( + ...value.split(";").map((str) => str.trim()), + ); + } else output[key] = value; + }); + return output; +}; diff --git a/src/structures/IHttpConnection.ts b/src/structures/IHttpConnection.ts new file mode 100644 index 0000000..58649dc --- /dev/null +++ b/src/structures/IHttpConnection.ts @@ -0,0 +1,216 @@ +/// + +/** + * Connection information. + * + * `IConnection` is an interface ttype who represents connection information of the + * remote HTTP server. You can target the remote HTTP server by wring the + * {@link IHttpConnection.host} variable down. Also, you can configure special header values + * by specializing the {@link IHttpConnection.headers} variable. + * + * If the remote HTTP server encrypts or decrypts its body data through the AES-128/256 + * algorithm, specify the {@link IHttpConnection.encryption} with {@link IEncryptionPassword} + * or {@link IEncryptionPassword.Closure} variable. + * + * @author Jenogho Nam - https://github.com/samchon + * @author Seungjun We - https://github.com/SeungjunWe + */ +export interface IHttpConnection< + Headers extends object | undefined = object | undefined, +> { + /** + * Host address of the remote HTTP server. + */ + host: string; + + /** + * Header values delivered to the remote HTTP server. + */ + headers?: Record & + IHttpConnection.Headerify; + + /** + * Additional options for the `fetch` function. + */ + options?: IHttpConnection.IOptions; + + /** + * Custom fetch function. + * + * If you want to use custom `fetch` function instead of built-in, + * assign your custom `fetch` function into this property. + * + * For reference, the `fetch` function has started to be supported + * since version 20 of NodeJS. Therefore, if you are using NodeJS + * version 19 or lower, you have to assign the `node-fetch` module + * into this property. + */ + fetch?: typeof fetch; +} +export namespace IHttpConnection { + /** + * Addiotional options for the `fetch` function. + * + * Almost same with {@link RequestInit} type of the {@link fetch} function, + * but `body`, `headers` and `method` properties are omitted. + * + * The reason why defining duplicated definition of {@link RequestInit} + * is for legacy NodeJS environments, which does not have the {@link fetch} + * function type. + */ + export interface IOptions { + /** + * A string indicating how the request will interact with the browser's + * cache to set request's cache. + */ + cache?: + | "default" + | "force-cache" + | "no-cache" + | "no-store" + | "only-if-cached" + | "reload"; + + /** + * A string indicating whether credentials will be sent with the request + * always, never, or only when sent to a same-origin URL. Sets request's + * credentials. + */ + credentials?: "omit" | "same-origin" | "include"; + + /** + * A cryptographic hash of the resource to be fetched by request. + * + * Sets request's integrity. + */ + integrity?: string; + + /** + * A boolean to set request's keepalive. + */ + keepalive?: boolean; + + /** + * A string to indicate whether the request will use CORS, or will be + * restricted to same-origin URLs. + * + * Sets request's mode. + */ + mode?: "cors" | "navigate" | "no-cors" | "same-origin"; + + /** + * A string indicating whether request follows redirects, results in + * an error upon encountering a redirect, or returns the redirect + * (in an opaque fashion). + * + * Sets request's redirect. + */ + redirect?: "error" | "follow" | "manual"; + + /** + * A string whose value is a same-origin URL, "about:client", or the + * empty string, to set request's referrer. + */ + referrer?: string; + + /** + * A referrer policy to set request's referrerPolicy. + */ + referrerPolicy?: + | "" + | "no-referrer" + | "no-referrer-when-downgrade" + | "origin" + | "origin-when-cross-origin" + | "same-origin" + | "strict-origin" + | "strict-origin-when-cross-origin" + | "unsafe-url"; + + /** + * An AbortSignal to set request's signal. + */ + signal?: AbortSignal | null; + } + + /** + * Type of allowed header values. + * + * Only atomic or array of atomic values are allowed. + */ + export type HeaderValue = + | string + | boolean + | number + | bigint + | string + | Array + | Array + | Array + | Array + | Array; + + /** + * Type of headers + * + * `Headerify` removes every properties that are not allowed in the + * HTTP headers type. + * + * Below are list of prohibited in HTTP headers. + * + * 1. Value type one of {@link HeaderValue} + * 2. Key is "set-cookie", but value is not an Array type + * 3. Key is one of them, but value is Array type + * - "age" + * - "authorization" + * - "content-length" + * - "content-type" + * - "etag" + * - "expires" + * - "from" + * - "host" + * - "if-modified-since" + * - "if-unmodified-since" + * - "last-modified" + * - "location" + * - "max-forwards" + * - "proxy-authorization" + * - "referer" + * - "retry-after" + * - "server" + * - "user-agent" + */ + export type Headerify = { + [P in keyof T]?: T[P] extends HeaderValue | undefined + ? P extends string + ? Lowercase

extends "set-cookie" + ? T[P] extends Array + ? T[P] | undefined + : never + : Lowercase

extends + | "age" + | "authorization" + | "content-length" + | "content-type" + | "etag" + | "expires" + | "from" + | "host" + | "if-modified-since" + | "if-unmodified-since" + | "last-modified" + | "location" + | "max-forwards" + | "proxy-authorization" + | "referer" + | "retry-after" + | "server" + | "user-agent" + ? T[P] extends Array + ? never + : T[P] | undefined + : T[P] | undefined + : never + : never; + }; +} diff --git a/src/structures/IHttpResponse.ts b/src/structures/IHttpResponse.ts new file mode 100644 index 0000000..cdd12e9 --- /dev/null +++ b/src/structures/IHttpResponse.ts @@ -0,0 +1,5 @@ +export interface IHttpResponse { + status: number; + headers: Record; + body: unknown; +} diff --git a/src/utils/LlmFetcher.ts b/src/utils/LlmFunctionFetcher.ts similarity index 100% rename from src/utils/LlmFetcher.ts rename to src/utils/LlmFunctionFetcher.ts diff --git a/src/utils/MigrateFetcher.ts b/src/utils/MigrateFetcher.ts deleted file mode 100644 index e69de29..0000000 From 342d6294de0812280f2ba97abbeb30461c4c7a3f Mon Sep 17 00:00:00 2001 From: Jeongho Nam Date: Sun, 1 Sep 2024 04:42:19 +0900 Subject: [PATCH 03/16] Main program completed, and starting test programs --- package.json | 7 +- src/OpenApi.ts | 19 +--- src/converters/LlmComposer.ts | 20 ++-- src/converters/LlmSchemaSeparator.ts | 4 +- src/{fetchers => http}/HttpError.ts | 0 src/http/LlmFunctionFetcher.ts | 68 +++++++++++ src/{fetchers => http}/MigrateRouteFetcher.ts | 32 ++++-- src/index.ts | 9 +- ...ILlmDocument.ts => ILlmHttpApplication.ts} | 20 ++-- .../{ILlmProcedure.ts => ILlmHttpFunction.ts} | 32 +++--- src/utils/LlmFunctionFetcher.ts | 0 src/utils/LlmTypeChecker.ts | 4 +- test/controllers/AppController.ts | 107 ++++++++++++++++++ test/controllers/AppModule.ts | 8 ++ .../test_document_migrate_route_comment.ts | 0 .../test_document_migrate_v20.ts | 0 .../test_document_migrate_v30.ts | 0 .../test_document_migrate_v31.ts | 0 .../test_document_convert_v20.ts | 0 .../test_document_convert_v30.ts | 0 .../test_document_convert_v31.ts | 0 .../test_document_downgrade_v20.ts | 0 .../test_document_downgrade_v30.ts | 0 .../test_json_schema_downgrade_v20.ts | 0 .../test_json_schema_downgrade_v30.ts | 0 ...test_json_schema_type_checker_cover_any.ts | 0 ...st_json_schema_type_checker_cover_array.ts | 0 ...json_schema_type_checker_cover_nullable.ts | 0 ...t_json_schema_type_checker_cover_number.ts | 0 ...t_json_schema_type_checker_cover_object.ts | 0 ...t_json_schema_type_checker_cover_string.ts | 0 test/index.ts | 19 +++- test/tsconfig.json | 2 + 33 files changed, 281 insertions(+), 70 deletions(-) rename src/{fetchers => http}/HttpError.ts (100%) create mode 100644 src/http/LlmFunctionFetcher.ts rename src/{fetchers => http}/MigrateRouteFetcher.ts (88%) rename src/structures/{ILlmDocument.ts => ILlmHttpApplication.ts} (88%) rename src/structures/{ILlmProcedure.ts => ILlmHttpFunction.ts} (86%) delete mode 100644 src/utils/LlmFunctionFetcher.ts create mode 100644 test/controllers/AppController.ts create mode 100644 test/controllers/AppModule.ts rename test/features/{ => migrate}/test_document_migrate_route_comment.ts (100%) rename test/features/{ => migrate}/test_document_migrate_v20.ts (100%) rename test/features/{ => migrate}/test_document_migrate_v30.ts (100%) rename test/features/{ => migrate}/test_document_migrate_v31.ts (100%) rename test/features/{ => openapi}/test_document_convert_v20.ts (100%) rename test/features/{ => openapi}/test_document_convert_v30.ts (100%) rename test/features/{ => openapi}/test_document_convert_v31.ts (100%) rename test/features/{ => openapi}/test_document_downgrade_v20.ts (100%) rename test/features/{ => openapi}/test_document_downgrade_v30.ts (100%) rename test/features/{ => openapi}/test_json_schema_downgrade_v20.ts (100%) rename test/features/{ => openapi}/test_json_schema_downgrade_v30.ts (100%) rename test/features/{ => openapi}/test_json_schema_type_checker_cover_any.ts (100%) rename test/features/{ => openapi}/test_json_schema_type_checker_cover_array.ts (100%) rename test/features/{ => openapi}/test_json_schema_type_checker_cover_nullable.ts (100%) rename test/features/{ => openapi}/test_json_schema_type_checker_cover_number.ts (100%) rename test/features/{ => openapi}/test_json_schema_type_checker_cover_object.ts (100%) rename test/features/{ => openapi}/test_json_schema_type_checker_cover_string.ts (100%) diff --git a/package.json b/package.json index 64fd732..8a20d76 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,12 @@ }, "homepage": "https://github.com/samchon/openapi", "devDependencies": { + "@nestia/core": "^3.12.0", "@nestia/e2e": "^0.7.0", + "@nestia/fetcher": "^3.12.0", + "@nestjs/common": "^10.4.1", + "@nestjs/core": "^10.4.1", + "@nestjs/platform-express": "^10.4.1", "@rollup/plugin-terser": "^0.4.4", "@rollup/plugin-typescript": "^11.1.6", "@trivago/prettier-plugin-sort-imports": "^4.3.0", @@ -46,7 +51,7 @@ "ts-patch": "^3.2.1", "typescript": "^5.5.3", "typescript-transform-paths": "^3.4.7", - "typia": "^6.0.0" + "typia": "^6.9.0" }, "files": [ "lib", diff --git a/src/OpenApi.ts b/src/OpenApi.ts index e287522..4f8c358 100644 --- a/src/OpenApi.ts +++ b/src/OpenApi.ts @@ -8,7 +8,7 @@ import { OpenApiV3Downgrader } from "./converters/OpenApiV3Downgrader"; import { OpenApiV3_1Converter } from "./converters/OpenApiV3_1Converter"; import { SwaggerV2Converter } from "./converters/SwaggerV2Converter"; import { SwaggerV2Downgrader } from "./converters/SwaggerV2Downgrader"; -import { ILlmDocument } from "./structures/ILlmDocument"; +import { ILlmHttpApplication } from "./structures/ILlmHttpApplication"; import { IMigrateDocument } from "./structures/IMigrateDocument"; /** @@ -155,23 +155,10 @@ export namespace OpenApi { return MigrateConverter.convert(document); } - export function llm( - document: OpenApi.IDocument, - options?: ILlmDocument.IOptions, - ): ILlmDocument; - - export function llm( - mirate: IMigrateDocument, - options?: ILlmDocument.IOptions, - ): ILlmDocument; - - /** - * @internal - */ export function llm( document: OpenApi.IDocument | IMigrateDocument, - options?: ILlmDocument.IOptions, - ): ILlmDocument { + options?: ILlmHttpApplication.IOptions, + ): ILlmHttpApplication { if ((document as OpenApi.IDocument)["x-samchon-emended"] !== true) document = migrate(document as OpenApi.IDocument); return LlmComposer.compose(document as IMigrateDocument, { diff --git a/src/converters/LlmComposer.ts b/src/converters/LlmComposer.ts index 599adbe..7b2e702 100644 --- a/src/converters/LlmComposer.ts +++ b/src/converters/LlmComposer.ts @@ -1,7 +1,7 @@ import { OpenApi } from "../OpenApi"; import { OpenApiTypeChecker } from "../OpenApiTypeChecker"; -import { ILlmDocument } from "../structures/ILlmDocument"; -import { ILlmProcedure } from "../structures/ILlmProcedure"; +import { ILlmHttpApplication } from "../structures/ILlmHttpApplication"; +import { ILlmHttpFunction } from "../structures/ILlmHttpFunction"; import { ILlmSchema } from "../structures/ILlmSchema"; import { IMigrateDocument } from "../structures/IMigrateDocument"; import { IMigrateRoute } from "../structures/IMigrateRoute"; @@ -12,20 +12,20 @@ import { OpenApiV3Downgrader } from "./OpenApiV3Downgrader"; export namespace LlmComposer { export const compose = ( migrate: IMigrateDocument, - options: ILlmDocument.IOptions, - ): ILlmDocument => { + options: ILlmHttpApplication.IOptions, + ): ILlmHttpApplication => { // COMPOSE FUNCTIONS - const errors: ILlmDocument.IError[] = migrate.errors.map((e) => ({ + const errors: ILlmHttpApplication.IError[] = migrate.errors.map((e) => ({ method: e.method, path: e.path, messages: e.messages, operation: () => e.operation(), route: () => undefined, })); - const functions: ILlmProcedure[] = migrate.routes + const functions: ILlmHttpFunction[] = migrate.routes .map((route) => { if (route.method === "head") return null; - const func: ILlmProcedure | null = composeFunction(options)( + const func: ILlmHttpFunction | null = composeFunction(options)( migrate.document().components, )(route); if (func === null) @@ -38,7 +38,7 @@ export namespace LlmComposer { }); return func; }) - .filter((v): v is ILlmProcedure => v !== null); + .filter((v): v is ILlmHttpFunction => v !== null); return { openapi: "3.0.3", functions, @@ -70,9 +70,9 @@ export namespace LlmComposer { }; const composeFunction = - (options: ILlmDocument.IOptions) => + (options: ILlmHttpApplication.IOptions) => (components: OpenApi.IComponents) => - (route: IMigrateRoute): ILlmProcedure | null => { + (route: IMigrateRoute): ILlmHttpFunction | null => { // CAST SCHEMA TYPES const cast = (s: OpenApi.IJsonSchema) => schema(components, s); const output: ILlmSchema | null | undefined = diff --git a/src/converters/LlmSchemaSeparator.ts b/src/converters/LlmSchemaSeparator.ts index e0283fc..5a5c449 100644 --- a/src/converters/LlmSchemaSeparator.ts +++ b/src/converters/LlmSchemaSeparator.ts @@ -1,4 +1,4 @@ -import { ILlmProcedure } from "../structures/ILlmProcedure"; +import { ILlmHttpFunction } from "../structures/ILlmHttpFunction"; import { ILlmSchema } from "../structures/ILlmSchema"; import { LlmTypeChecker } from "../utils/LlmTypeChecker"; @@ -7,7 +7,7 @@ export namespace LlmSchemaSeparator { parameters: ILlmSchema[]; predicator: (schema: ILlmSchema) => boolean; } - export const parameters = (props: IProps): ILlmProcedure.ISeparated => { + export const parameters = (props: IProps): ILlmHttpFunction.ISeparated => { const indexes: Array<[ILlmSchema | null, ILlmSchema | null]> = props.parameters.map(schema(props.predicator)); return { diff --git a/src/fetchers/HttpError.ts b/src/http/HttpError.ts similarity index 100% rename from src/fetchers/HttpError.ts rename to src/http/HttpError.ts diff --git a/src/http/LlmFunctionFetcher.ts b/src/http/LlmFunctionFetcher.ts new file mode 100644 index 0000000..a9d9ac5 --- /dev/null +++ b/src/http/LlmFunctionFetcher.ts @@ -0,0 +1,68 @@ +import { IHttpConnection } from "../structures/IHttpConnection"; +import { IHttpResponse } from "../structures/IHttpResponse"; +import { ILlmHttpApplication } from "../structures/ILlmHttpApplication"; +import { ILlmHttpFunction } from "../structures/ILlmHttpFunction"; +import { IMigrateRoute } from "../structures/IMigrateRoute"; +import { MigrateRouteFetcher } from "./MigrateRouteFetcher"; + +export namespace LlmFunctionFetcher { + export interface IProps { + /** + * Document of the OpenAI function call schemas. + */ + document: ILlmHttpApplication; + + /** + * Procedure schema to call. + */ + procedure: ILlmHttpFunction; + + /** + * Connection info to the server. + */ + connection: IHttpConnection; + + /** + * Arguments for the function call. + */ + arguments: any[]; + } + + export const execute = async (props: IProps): Promise => + MigrateRouteFetcher.request(getFetchArguments(props)); + + export const propagate = async (props: IProps): Promise => + MigrateRouteFetcher.propagate(getFetchArguments(props)); + + const getFetchArguments = (props: IProps): MigrateRouteFetcher.IProps => { + const route: IMigrateRoute = props.procedure.route(); + if (props.document.options.keyword === true) { + const input: Pick< + MigrateRouteFetcher.IProps, + "parameters" | "query" | "body" + > = props.arguments[0]; + return { + connection: props.connection, + route, + parameters: input.parameters, + query: input.query, + body: input.body, + }; + } + const parameters: Array = + props.arguments.slice(0, route.parameters.length); + const query: object | undefined = route.query + ? props.arguments[route.parameters.length] + : undefined; + const body: object | undefined = route.body + ? props.arguments[route.parameters.length + (route.query ? 1 : 0)] + : undefined; + return { + connection: props.connection, + route, + parameters, + query, + body, + }; + }; +} diff --git a/src/fetchers/MigrateRouteFetcher.ts b/src/http/MigrateRouteFetcher.ts similarity index 88% rename from src/fetchers/MigrateRouteFetcher.ts rename to src/http/MigrateRouteFetcher.ts index 273717b..719adba 100644 --- a/src/fetchers/MigrateRouteFetcher.ts +++ b/src/http/MigrateRouteFetcher.ts @@ -7,7 +7,9 @@ export namespace MigrateRouteFetcher { export interface IProps { connection: IHttpConnection; route: IMigrateRoute; - parameters: Array; + parameters: + | Array + | Record; query?: object | undefined; body?: object | undefined; } @@ -30,15 +32,24 @@ export namespace MigrateRouteFetcher { } const _Propagate = async ( - funcName: string, + from: string, props: MigrateRouteFetcher.IProps, ): Promise => { - // VALIDATION + // VALIDATE PARAMETERS const error = (message: string) => - new Error(`Error on MigrateRouteFetcher.${funcName}(): ${message}`); - if (props.route.parameters.length !== props.parameters.length) + new Error(`Error on MigrateRouteFetcher.${from}(): ${message}`); + if (Array.isArray(props.parameters)) { + if (props.route.parameters.length !== props.parameters.length) + throw error(`number of parameters is not matched.`); + } else if ( + props.route.parameters.every( + (p) => (props.parameters as Record)[p.key] !== undefined, + ) === false + ) throw error(`number of parameters is not matched.`); - else if (!!props.route.query !== !!props.query) + + // VALIDATE QUERY + if (!!props.route.query !== !!props.query) throw error(`query is not matched.`); else if (!!props.route.body !== (props.body !== undefined)) throw error(`body is not matched.`); @@ -128,7 +139,14 @@ const getPath = ( ): string => { let path: string = props.route.emendedPath; props.route.parameters.forEach((p, i) => { - path = path.replace(`:${p.key}`, String(props.parameters[i] ?? "null")); + path = path.replace( + `:${p.key}`, + String( + (Array.isArray(props.parameters) + ? props.parameters[i] + : props.parameters[p.key]) ?? "null", + ), + ); }); if (props.route.query) path += getQueryPath(props.query ?? {}); return path; diff --git a/src/index.ts b/src/index.ts index a45cbc9..277ff89 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,8 +1,15 @@ +export * from "./structures/ILlmHttpApplication"; +export * from "./structures/ILlmHttpFunction"; +export * from "./structures/ILlmSchema"; export * from "./structures/IMigrateRoute"; export * from "./structures/IMigrateDocument"; -export * from "./OpenApi"; + +export * from "./http/HttpError"; +export * from "./http/LlmFunctionFetcher"; +export * from "./http/MigrateRouteFetcher"; export * from "./OpenApiTypeChecker"; +export * from "./OpenApi"; export * from "./SwaggerV2"; export * from "./OpenApiV3"; export * from "./OpenApiV3_1"; diff --git a/src/structures/ILlmDocument.ts b/src/structures/ILlmHttpApplication.ts similarity index 88% rename from src/structures/ILlmDocument.ts rename to src/structures/ILlmHttpApplication.ts index eab6d4f..c1a2b56 100644 --- a/src/structures/ILlmDocument.ts +++ b/src/structures/ILlmHttpApplication.ts @@ -1,9 +1,9 @@ import { OpenApi } from "../OpenApi"; -import { ILlmProcedure } from "./ILlmProcedure"; +import { ILlmHttpFunction } from "./ILlmHttpFunction"; import { ILlmSchema } from "./ILlmSchema"; import { IMigrateRoute } from "./IMigrateRoute"; -export interface ILlmDocument< +export interface ILlmHttpApplication< Schema extends ILlmSchema = ILlmSchema, Operation extends OpenApi.IOperation = OpenApi.IOperation, Route extends IMigrateRoute = IMigrateRoute, @@ -23,12 +23,12 @@ export interface ILlmDocument< * When you want to execute the function with LLM constructed arguments, * you can do it through {@link LlmFetcher.execute} function. */ - functions: ILlmProcedure[]; + functions: ILlmHttpFunction[]; /** * List of errors occurred during the composition. */ - errors: ILlmDocument.IError[]; + errors: ILlmHttpApplication.IError[]; /** * Options for the document. @@ -36,9 +36,9 @@ export interface ILlmDocument< * Adjusted options when composing the document through * {@link OpenApi.llm} function. */ - options: ILlmDocument.IOptions; + options: ILlmHttpApplication.IOptions; } -export namespace ILlmDocument { +export namespace ILlmHttpApplication { /** * Error occurred in the composition. */ @@ -89,7 +89,7 @@ export namespace ILlmDocument { * Whether the parameters are keyworded or not. * * If this property value is `true`, length of the - * {@link ILlmDocument.IFunction.parameters} is always 1, and type of + * {@link ILlmHttpApplication.IFunction.parameters} is always 1, and type of * the pararameter is always {@link ILlmSchema.IObject} type. * Also, its properties are following below rules: * @@ -106,7 +106,7 @@ export namespace ILlmDocument { * ``` * * Otherwise (this property value is `false`), length of the - * {@link ILlmProcedure.parameters} is variable, and sequence of the + * {@link ILlmHttpFunction.parameters} is variable, and sequence of the * parameters are following below rules. * * ```typescript @@ -136,8 +136,8 @@ export namespace ILlmDocument { * predicating whether the schema value must be composed by human or * not, the parameters would be separated into two parts. * - * - {@link ILlmProcedure.separated.llm} - * - {@link ILlmProcedure.separated.human} + * - {@link ILlmHttpFunction.separated.llm} + * - {@link ILlmHttpFunction.separated.human} * * When writing the function, note that returning value `true` means * to be a human composing the value, and `false` means to LLM diff --git a/src/structures/ILlmProcedure.ts b/src/structures/ILlmHttpFunction.ts similarity index 86% rename from src/structures/ILlmProcedure.ts rename to src/structures/ILlmHttpFunction.ts index 57f3cf7..056eb60 100644 --- a/src/structures/ILlmProcedure.ts +++ b/src/structures/ILlmHttpFunction.ts @@ -3,31 +3,31 @@ import { ILlmSchema } from "./ILlmSchema"; import { IMigrateRoute } from "./IMigrateRoute"; /** - * LLM procedure metadata from OpenAPI operation. + * LLM function metadata from HTTP (OpenAPI) operation. * - * `ILlmProcedure` is a data structure representing a procedure converted + * `ILlmHttpFunction` is a data structure representing a procedure converted * from the OpenAPI operation, used for the LLM (Large Language Model) * function calling. It's a typical RPC (Remote Procedure Call) structure * containing the procedure {@link name}, {@link parameters}, and * {@link output return type}. * - * If you provide this `ILlmProcedure` data to the LLM like "OpenAI", + * If you provide this `ILlmHttpFunction` data to the LLM like "OpenAI", * the "OpenAI" will compose a function arguments by analyzing * conversations with the user. With the LLM composed arguments, you can * execute the procedure through {@link LlmFetcher.execute} and get the * result. * - * For reference, different between `ILlmProcedure` and its origin source - * {@link OpenApi.IOperation} is, `ILlmProcedure` has converted every type + * For reference, different between `ILlmHttpFunction` and its origin source + * {@link OpenApi.IOperation} is, `ILlmHttpFunction` has converted every type * schema informations from {@link OpenApi.IJsonSchema} to {@link ILlmSchema} * to escape {@link OpenApi.IJsonSchema.IReference reference types}, and * downgrade the version of the JSON schema to OpenAPI 3.0. It's because * LLM function call feature cannot understand both reference types and * OpenAPI 3.1 specification. * - * Additionally, if you've composed `ILlmProcedure` with - * {@link ILlmDocument.IOptions.keyword} configuration as `true`, number of - * {@link ILlmProcedure.parameters} are always 1 and the first parameter's + * Additionally, if you've composed `ILlmHttpFunction` with + * {@link ILlmHttpApplication.IOptions.keyword} configuration as `true`, number of + * {@link ILlmHttpFunction.parameters} are always 1 and the first parameter's * type is always {@link ILlmSchema.IObject}. The properties' rule is: * * - `pathParameters`: Path parameters of {@link OpenApi.IOperation.parameters} @@ -56,7 +56,7 @@ import { IMigrateRoute } from "./IMigrateRoute"; * @reference https://platform.openai.com/docs/guides/function-calling * @author Jeongho Nam - https://github.com/samchon */ -export interface ILlmProcedure< +export interface ILlmHttpFunction< Schema extends ILlmSchema = ILlmSchema, Operation extends OpenApi.IOperation = OpenApi.IOperation, Route extends IMigrateRoute = IMigrateRoute, @@ -75,7 +75,7 @@ export interface ILlmProcedure< * Representative name of the function. * * The `name` is a repsentative name identifying the function in the - * {@link ILlmDocument}. The `name` value is just composed by joining the + * {@link ILlmHttpApplication}. The `name` value is just composed by joining the * {@link IMigrateRoute.accessor} by underscore `_` character. * * Here is the composition rule of the {@link IMigrateRoute.accessor}: @@ -117,8 +117,8 @@ export interface ILlmProcedure< /** * List of parameter types. * - * If you've configured {@link ILlmDocument.IOptions.keyword} as `true`, - * number of {@link ILlmProcedure.parameters} are always 1 and the first + * If you've configured {@link ILlmHttpApplication.IOptions.keyword} as `true`, + * number of {@link ILlmHttpFunction.parameters} are always 1 and the first * parameter's type is always {@link ILlmSchema.IObject}. The * properties' rule is: * @@ -150,9 +150,9 @@ export interface ILlmProcedure< /** * Collection of separated parameters. * - * Filled only when {@link ILlmDocument.IOptions.separate} is configured. + * Filled only when {@link ILlmHttpApplication.IOptions.separate} is configured. */ - separated?: ILlmProcedure.ISeparated; + separated?: ILlmHttpFunction.ISeparated; /** * Expected return type. @@ -165,7 +165,7 @@ export interface ILlmProcedure< /** * Description of the procedure. * - * `ILlmProcedure.description` is composed by below rule: + * `ILlmHttpFunction.description` is composed by below rule: * * 1. Starts from the {@link OpenApi.IOperation.summary} paragraph. * 2. The next paragraphs are filled with the @@ -206,7 +206,7 @@ export interface ILlmProcedure< */ route: () => Route; } -export namespace ILlmProcedure { +export namespace ILlmHttpFunction { /** * Collection of separated parameters. */ diff --git a/src/utils/LlmFunctionFetcher.ts b/src/utils/LlmFunctionFetcher.ts deleted file mode 100644 index e69de29..0000000 diff --git a/src/utils/LlmTypeChecker.ts b/src/utils/LlmTypeChecker.ts index 8381d81..633403e 100644 --- a/src/utils/LlmTypeChecker.ts +++ b/src/utils/LlmTypeChecker.ts @@ -1,9 +1,9 @@ import { ILlmSchema } from "../structures/ILlmSchema"; /** - * Type checker for LLM function calling schema. + * Type checker for LLM type schema. * - * `LlmTypeChecker` is a type checker of {@link ILlmSchema}. + * `LlmSchemaTypeChecker` is a type checker of {@link ILlmSchema}. * * @author Samchon */ diff --git a/test/controllers/AppController.ts b/test/controllers/AppController.ts new file mode 100644 index 0000000..5d24c5c --- /dev/null +++ b/test/controllers/AppController.ts @@ -0,0 +1,107 @@ +import { + TypedBody, + TypedFormData, + TypedParam, + TypedQuery, + TypedRoute, +} from "@nestia/core"; +import { Controller, Query } from "@nestjs/common"; + +@Controller() +export class AppController { + @TypedRoute.Get(":a/:b/:c/parameters") + public parameters( + @TypedParam("a") a: string, + @TypedParam("b") b: number, + @TypedParam("c") c: boolean, + ) { + return { a, b, c }; + } + + @TypedRoute.Get(":a/:b/:c/query") + public query( + @TypedParam("a") a: string, + @TypedParam("b") b: number, + @TypedParam("c") c: boolean, + @TypedQuery() + query: { + flag: boolean; + value: number; + text: string; + }, + ) { + return { a, b, c, query }; + } + + @TypedRoute.Post(":a/:b/:c/body") + public body( + @TypedParam("a") a: string, + @TypedParam("b") b: number, + @TypedParam("c") c: boolean, + @TypedBody() + body: { + flag: boolean; + value: number; + text: string; + }, + ) { + return { a, b, c, body }; + } + + @TypedRoute.Post(":a/:b/:c/query/body") + public query_body( + @TypedParam("a") a: string, + @TypedParam("b") b: number, + @TypedParam("c") c: boolean, + @Query("name") name: string, + @Query("reference") reference: string, + @TypedQuery() + query: { + memo: string; + }, + @TypedBody() + body: { + flag: boolean; + value: number; + text: string; + }, + ) { + return { + a, + b, + c, + query: { + ...query, + name, + reference, + }, + body, + }; + } + + @TypedRoute.Post(":a/:b/:c/multipart") + public query_multipart( + @TypedParam("a") a: string, + @TypedParam("b") b: number, + @TypedParam("c") c: boolean, + @TypedQuery() query: { flag: boolean; value: number; text: string }, + @TypedFormData.Body() + body: { + name: string; + reference: string; + file: File; + }, + ) { + return { + a, + b, + c, + query, + body: { + name: body.name, + reference: body.reference, + file: `http://localhost:3000/files/${body.file.name}`, + }, + }; + } +} diff --git a/test/controllers/AppModule.ts b/test/controllers/AppModule.ts new file mode 100644 index 0000000..322f15a --- /dev/null +++ b/test/controllers/AppModule.ts @@ -0,0 +1,8 @@ +import { Module } from "@nestjs/common"; + +import { AppController } from "./AppController"; + +@Module({ + controllers: [AppController], +}) +export class AppModule {} diff --git a/test/features/test_document_migrate_route_comment.ts b/test/features/migrate/test_document_migrate_route_comment.ts similarity index 100% rename from test/features/test_document_migrate_route_comment.ts rename to test/features/migrate/test_document_migrate_route_comment.ts diff --git a/test/features/test_document_migrate_v20.ts b/test/features/migrate/test_document_migrate_v20.ts similarity index 100% rename from test/features/test_document_migrate_v20.ts rename to test/features/migrate/test_document_migrate_v20.ts diff --git a/test/features/test_document_migrate_v30.ts b/test/features/migrate/test_document_migrate_v30.ts similarity index 100% rename from test/features/test_document_migrate_v30.ts rename to test/features/migrate/test_document_migrate_v30.ts diff --git a/test/features/test_document_migrate_v31.ts b/test/features/migrate/test_document_migrate_v31.ts similarity index 100% rename from test/features/test_document_migrate_v31.ts rename to test/features/migrate/test_document_migrate_v31.ts diff --git a/test/features/test_document_convert_v20.ts b/test/features/openapi/test_document_convert_v20.ts similarity index 100% rename from test/features/test_document_convert_v20.ts rename to test/features/openapi/test_document_convert_v20.ts diff --git a/test/features/test_document_convert_v30.ts b/test/features/openapi/test_document_convert_v30.ts similarity index 100% rename from test/features/test_document_convert_v30.ts rename to test/features/openapi/test_document_convert_v30.ts diff --git a/test/features/test_document_convert_v31.ts b/test/features/openapi/test_document_convert_v31.ts similarity index 100% rename from test/features/test_document_convert_v31.ts rename to test/features/openapi/test_document_convert_v31.ts diff --git a/test/features/test_document_downgrade_v20.ts b/test/features/openapi/test_document_downgrade_v20.ts similarity index 100% rename from test/features/test_document_downgrade_v20.ts rename to test/features/openapi/test_document_downgrade_v20.ts diff --git a/test/features/test_document_downgrade_v30.ts b/test/features/openapi/test_document_downgrade_v30.ts similarity index 100% rename from test/features/test_document_downgrade_v30.ts rename to test/features/openapi/test_document_downgrade_v30.ts diff --git a/test/features/test_json_schema_downgrade_v20.ts b/test/features/openapi/test_json_schema_downgrade_v20.ts similarity index 100% rename from test/features/test_json_schema_downgrade_v20.ts rename to test/features/openapi/test_json_schema_downgrade_v20.ts diff --git a/test/features/test_json_schema_downgrade_v30.ts b/test/features/openapi/test_json_schema_downgrade_v30.ts similarity index 100% rename from test/features/test_json_schema_downgrade_v30.ts rename to test/features/openapi/test_json_schema_downgrade_v30.ts diff --git a/test/features/test_json_schema_type_checker_cover_any.ts b/test/features/openapi/test_json_schema_type_checker_cover_any.ts similarity index 100% rename from test/features/test_json_schema_type_checker_cover_any.ts rename to test/features/openapi/test_json_schema_type_checker_cover_any.ts diff --git a/test/features/test_json_schema_type_checker_cover_array.ts b/test/features/openapi/test_json_schema_type_checker_cover_array.ts similarity index 100% rename from test/features/test_json_schema_type_checker_cover_array.ts rename to test/features/openapi/test_json_schema_type_checker_cover_array.ts diff --git a/test/features/test_json_schema_type_checker_cover_nullable.ts b/test/features/openapi/test_json_schema_type_checker_cover_nullable.ts similarity index 100% rename from test/features/test_json_schema_type_checker_cover_nullable.ts rename to test/features/openapi/test_json_schema_type_checker_cover_nullable.ts diff --git a/test/features/test_json_schema_type_checker_cover_number.ts b/test/features/openapi/test_json_schema_type_checker_cover_number.ts similarity index 100% rename from test/features/test_json_schema_type_checker_cover_number.ts rename to test/features/openapi/test_json_schema_type_checker_cover_number.ts diff --git a/test/features/test_json_schema_type_checker_cover_object.ts b/test/features/openapi/test_json_schema_type_checker_cover_object.ts similarity index 100% rename from test/features/test_json_schema_type_checker_cover_object.ts rename to test/features/openapi/test_json_schema_type_checker_cover_object.ts diff --git a/test/features/test_json_schema_type_checker_cover_string.ts b/test/features/openapi/test_json_schema_type_checker_cover_string.ts similarity index 100% rename from test/features/test_json_schema_type_checker_cover_string.ts rename to test/features/openapi/test_json_schema_type_checker_cover_string.ts diff --git a/test/index.ts b/test/index.ts index 0416acb..b0a0860 100644 --- a/test/index.ts +++ b/test/index.ts @@ -1,12 +1,23 @@ import { DynamicExecutor } from "@nestia/e2e"; +import { NestFactory } from "@nestjs/core"; import chalk from "chalk"; +import { AppModule } from "./controllers/AppModule"; + const main = async (): Promise => { + // PREPARE SERVER + const app = await NestFactory.create(AppModule, { logger: false }); + await app.listen(3_000); + // DO TEST const report: DynamicExecutor.IReport = await DynamicExecutor.validate({ prefix: "test_", location: __dirname + "/features", - parameters: () => [], + parameters: () => [ + { + connection: { host: `http://localhost:3000` }, + }, + ], onComplete: (exec) => { const trace = (str: string) => console.log(` - ${chalk.green(exec.name)}: ${str}`); @@ -31,9 +42,7 @@ const main = async (): Promise => { console.log("Failed"); console.log("Elapsed time", report.time.toLocaleString(), `ms`); } + await app.close(); if (exceptions.length) process.exit(-1); }; -main().catch((exp) => { - console.error(exp); - process.exit(-1); -}); +main().catch(console.error); diff --git a/test/tsconfig.json b/test/tsconfig.json index 753ac46..e4385c0 100644 --- a/test/tsconfig.json +++ b/test/tsconfig.json @@ -2,6 +2,8 @@ "extends": "../tsconfig.json", "compilerOptions": { "outDir": "../bin", + "emitDecoratorMetadata": true, + "experimentalDecorators": true, "paths": { "@samchon/openapi": ["../src/index.ts"], "@samchon/openapi/lib/*": ["../src/*"], From 7bd8e442532d91fa638dfa2b4e8908d68651cd83 Mon Sep 17 00:00:00 2001 From: Jeongho Nam Date: Mon, 2 Sep 2024 11:29:55 +0900 Subject: [PATCH 04/16] `HttpMigrateRouteFetcher` test completed --- README.md | 8 +- package.json | 3 + src/OpenApi.ts | 16 +- src/converters/LlmComposer.ts | 28 +- src/converters/LlmSchemaSeparator.ts | 4 +- src/converters/MigrateConverter.ts | 18 +- src/converters/MigrateRouteAccessor.ts | 12 +- src/converters/MigrateRouteConverter.ts | 26 +- src/converters/OpenApiTypeChecker.ts | 2 - src/converters/OpenApiV3Downgrader.ts | 2 +- src/converters/SwaggerV2Downgrader.ts | 2 +- ...onFetcher.ts => HttpLlmFunctionFetcher.ts} | 24 +- ...eFetcher.ts => HttpMigrateRouteFetcher.ts} | 35 +- src/index.ts | 20 +- ...pApplication.ts => IHttpLlmApplication.ts} | 32 +- ...LlmHttpFunction.ts => IHttpLlmFunction.ts} | 48 +- ...Document.ts => IHttpMigrateApplication.ts} | 16 +- ...{IMigrateRoute.ts => IHttpMigrateRoute.ts} | 32 +- src/{ => utils}/OpenApiTypeChecker.ts | 4 +- test/controllers/AppController.ts | 54 +-- test/controllers/AppFilter.ts | 15 + .../migrate/test_http_migrate_fetch_body.ts | 38 ++ ...t_http_migrate_fetch_keyword_parameters.ts | 30 ++ .../test_http_migrate_fetch_multipart.ts | 40 ++ ...ttp_migrate_fetch_positional_parameters.ts | 26 ++ .../test_http_migrate_fetch_propagate.ts | 29 ++ .../migrate/test_http_migrate_fetch_query.ts | 34 ++ .../test_http_migrate_fetch_query_and_body.ts | 39 ++ ....ts => test_http_migrate_route_comment.ts} | 14 +- ...igrate_v20.ts => test_http_migrate_v20.ts} | 8 +- ...igrate_v30.ts => test_http_migrate_v30.ts} | 8 +- ...igrate_v31.ts => test_http_migrate_v31.ts} | 12 +- .../openapi/test_document_convert_v20.ts | 2 +- .../openapi/test_document_convert_v30.ts | 2 +- .../openapi/test_document_convert_v31.ts | 2 +- .../openapi/test_document_downgrade_v20.ts | 2 +- .../openapi/test_document_downgrade_v30.ts | 2 +- test/index.ts | 7 +- test/nestia.config.ts | 13 + test/swagger.json | 428 ++++++++++++++++++ test/tsconfig.json | 5 +- tsconfig.json | 2 +- 42 files changed, 931 insertions(+), 213 deletions(-) delete mode 100644 src/converters/OpenApiTypeChecker.ts rename src/http/{LlmFunctionFetcher.ts => HttpLlmFunctionFetcher.ts} (67%) rename src/http/{MigrateRouteFetcher.ts => HttpMigrateRouteFetcher.ts} (87%) rename src/structures/{ILlmHttpApplication.ts => IHttpLlmApplication.ts} (80%) rename src/structures/{ILlmHttpFunction.ts => IHttpLlmFunction.ts} (81%) rename src/structures/{IMigrateDocument.ts => IHttpMigrateApplication.ts} (76%) rename src/structures/{IMigrateRoute.ts => IHttpMigrateRoute.ts} (89%) rename src/{ => utils}/OpenApiTypeChecker.ts (99%) create mode 100644 test/controllers/AppFilter.ts create mode 100644 test/features/migrate/test_http_migrate_fetch_body.ts create mode 100644 test/features/migrate/test_http_migrate_fetch_keyword_parameters.ts create mode 100644 test/features/migrate/test_http_migrate_fetch_multipart.ts create mode 100644 test/features/migrate/test_http_migrate_fetch_positional_parameters.ts create mode 100644 test/features/migrate/test_http_migrate_fetch_propagate.ts create mode 100644 test/features/migrate/test_http_migrate_fetch_query.ts create mode 100644 test/features/migrate/test_http_migrate_fetch_query_and_body.ts rename test/features/migrate/{test_document_migrate_route_comment.ts => test_http_migrate_route_comment.ts} (78%) rename test/features/migrate/{test_document_migrate_v20.ts => test_http_migrate_v20.ts} (59%) rename test/features/migrate/{test_document_migrate_v30.ts => test_http_migrate_v30.ts} (59%) rename test/features/migrate/{test_document_migrate_v31.ts => test_http_migrate_v31.ts} (58%) create mode 100644 test/nestia.config.ts create mode 100644 test/swagger.json diff --git a/README.md b/README.md index ef75877..4f3b3eb 100644 --- a/README.md +++ b/README.md @@ -46,9 +46,9 @@ Here is the entire list of differences between OpenAPI v3.1 and emended OpenApi. - Merge `OpenApiV3_1.IJsonSchema.IRecursiveReference` to `OpenApi.IJsonSchema.IReference` - Merge `OpenApiV3_1.IJsonSchema.IAllOf` to `OpenApi.IJsonSchema.IObject` -Additionally, `@samchon/openapi` provides [`IMigrateDocument`](https://github.com/samchon/openapi/blob/master/src/IMigrateDocument.ts) for OpenAPI generators. +Additionally, `@samchon/openapi` provides [`IHttpMigrateApplication`](https://github.com/samchon/openapi/blob/master/src/IHttpMigrateApplication.ts) for OpenAPI generators. -If you're developing TypeScript, [`@nestia/editor`](https://nestia.io/docs/editor) would be the best project utilizing the [`IMigrateDocument`](https://github.com/samchon/openapi/blob/master/src/IMigrateDocument.ts) for the OpenAPI SDK generation. Otherwise, you wanna utilize OpenAPI document for OpenAI function calling, [`@wrtnio/openai-function-schema`](https://github.com/wrtnio/openai-function-schema/) has been prepared for you. +If you're developing TypeScript, [`@nestia/editor`](https://nestia.io/docs/editor) would be the best project utilizing the [`IHttpMigrateApplication`](https://github.com/samchon/openapi/blob/master/src/IHttpMigrateApplication.ts) for the OpenAPI SDK generation. Otherwise, you wanna utilize OpenAPI document for OpenAI function calling, [`@wrtnio/openai-function-schema`](https://github.com/wrtnio/openai-function-schema/) has been prepared for you. ```mermaid flowchart @@ -78,7 +78,7 @@ import { SwaggerV2, OpenApiV3, OpenApiV3_1, - IMigrateDocument, + IHttpMigrateApplication, } from "@samchon/openapi"; // original Swagger/OpenAPI document @@ -100,7 +100,7 @@ OpenApi.downgrade(OpenApi.convert(v2), "3.0"); OpenApi.downgrade(OpenApi.convert(v3), "2.0"); // also helps openapi generator libraries -const migrate: IMigrateDocument = OpenApi.migrate(output); +const migrate: IHttpMigrateApplication = OpenApi.migrate(output); ``` Just install `@samchon/openapi` library and import `OpenApi` module from there. diff --git a/package.json b/package.json index 8a20d76..f7b04bf 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "@nestia/core": "^3.12.0", "@nestia/e2e": "^0.7.0", "@nestia/fetcher": "^3.12.0", + "@nestia/sdk": "^3.12.0", "@nestjs/common": "^10.4.1", "@nestjs/core": "^10.4.1", "@nestjs/platform-express": "^10.4.1", @@ -45,9 +46,11 @@ "@types/node": "^20.12.7", "chalk": "^4.1.2", "js-yaml": "^4.1.0", + "nestia": "^6.0.1", "prettier": "^3.2.5", "rimraf": "^5.0.5", "rollup": "^4.18.1", + "source-map-support": "^0.5.21", "ts-patch": "^3.2.1", "typescript": "^5.5.3", "typescript-transform-paths": "^3.4.7", diff --git a/src/OpenApi.ts b/src/OpenApi.ts index 4f8c358..59966eb 100644 --- a/src/OpenApi.ts +++ b/src/OpenApi.ts @@ -8,8 +8,8 @@ import { OpenApiV3Downgrader } from "./converters/OpenApiV3Downgrader"; import { OpenApiV3_1Converter } from "./converters/OpenApiV3_1Converter"; import { SwaggerV2Converter } from "./converters/SwaggerV2Converter"; import { SwaggerV2Downgrader } from "./converters/SwaggerV2Downgrader"; -import { ILlmHttpApplication } from "./structures/ILlmHttpApplication"; -import { IMigrateDocument } from "./structures/IMigrateDocument"; +import { IHttpLlmApplication } from "./structures/IHttpLlmApplication"; +import { IHttpMigrateApplication } from "./structures/IHttpMigrateApplication"; /** * Emended OpenAPI v3.1 definition used by `typia` and `nestia`. @@ -139,7 +139,7 @@ export namespace OpenApi { /** * Convert to migrate document. * - * Convert the given OpenAPI document to {@link IMigrateDocument}, that is + * Convert the given OpenAPI document to {@link IHttpMigrateApplication}, that is * useful for OpenAPI generator library which makes RPC (Remote Procedure Call) * functions for the Restful API operation. * @@ -151,17 +151,17 @@ export namespace OpenApi { Operation extends IOperation = IOperation, >( document: IDocument, - ): IMigrateDocument { + ): IHttpMigrateApplication { return MigrateConverter.convert(document); } export function llm( - document: OpenApi.IDocument | IMigrateDocument, - options?: ILlmHttpApplication.IOptions, - ): ILlmHttpApplication { + document: OpenApi.IDocument | IHttpMigrateApplication, + options?: IHttpLlmApplication.IOptions, + ): IHttpLlmApplication { if ((document as OpenApi.IDocument)["x-samchon-emended"] !== true) document = migrate(document as OpenApi.IDocument); - return LlmComposer.compose(document as IMigrateDocument, { + return LlmComposer.compose(document as IHttpMigrateApplication, { keyword: options?.keyword ?? false, separate: options?.separate ?? null, }); diff --git a/src/converters/LlmComposer.ts b/src/converters/LlmComposer.ts index 7b2e702..4499b6c 100644 --- a/src/converters/LlmComposer.ts +++ b/src/converters/LlmComposer.ts @@ -1,31 +1,31 @@ import { OpenApi } from "../OpenApi"; -import { OpenApiTypeChecker } from "../OpenApiTypeChecker"; -import { ILlmHttpApplication } from "../structures/ILlmHttpApplication"; -import { ILlmHttpFunction } from "../structures/ILlmHttpFunction"; +import { IHttpLlmApplication } from "../structures/IHttpLlmApplication"; +import { IHttpLlmFunction } from "../structures/IHttpLlmFunction"; +import { IHttpMigrateApplication } from "../structures/IHttpMigrateApplication"; +import { IHttpMigrateRoute } from "../structures/IHttpMigrateRoute"; import { ILlmSchema } from "../structures/ILlmSchema"; -import { IMigrateDocument } from "../structures/IMigrateDocument"; -import { IMigrateRoute } from "../structures/IMigrateRoute"; import { LlmTypeChecker } from "../utils/LlmTypeChecker"; +import { OpenApiTypeChecker } from "../utils/OpenApiTypeChecker"; import { LlmSchemaSeparator } from "./LlmSchemaSeparator"; import { OpenApiV3Downgrader } from "./OpenApiV3Downgrader"; export namespace LlmComposer { export const compose = ( - migrate: IMigrateDocument, - options: ILlmHttpApplication.IOptions, - ): ILlmHttpApplication => { + migrate: IHttpMigrateApplication, + options: IHttpLlmApplication.IOptions, + ): IHttpLlmApplication => { // COMPOSE FUNCTIONS - const errors: ILlmHttpApplication.IError[] = migrate.errors.map((e) => ({ + const errors: IHttpLlmApplication.IError[] = migrate.errors.map((e) => ({ method: e.method, path: e.path, messages: e.messages, operation: () => e.operation(), route: () => undefined, })); - const functions: ILlmHttpFunction[] = migrate.routes + const functions: IHttpLlmFunction[] = migrate.routes .map((route) => { if (route.method === "head") return null; - const func: ILlmHttpFunction | null = composeFunction(options)( + const func: IHttpLlmFunction | null = composeFunction(options)( migrate.document().components, )(route); if (func === null) @@ -38,7 +38,7 @@ export namespace LlmComposer { }); return func; }) - .filter((v): v is ILlmHttpFunction => v !== null); + .filter((v): v is IHttpLlmFunction => v !== null); return { openapi: "3.0.3", functions, @@ -70,9 +70,9 @@ export namespace LlmComposer { }; const composeFunction = - (options: ILlmHttpApplication.IOptions) => + (options: IHttpLlmApplication.IOptions) => (components: OpenApi.IComponents) => - (route: IMigrateRoute): ILlmHttpFunction | null => { + (route: IHttpMigrateRoute): IHttpLlmFunction | null => { // CAST SCHEMA TYPES const cast = (s: OpenApi.IJsonSchema) => schema(components, s); const output: ILlmSchema | null | undefined = diff --git a/src/converters/LlmSchemaSeparator.ts b/src/converters/LlmSchemaSeparator.ts index 5a5c449..fe092f7 100644 --- a/src/converters/LlmSchemaSeparator.ts +++ b/src/converters/LlmSchemaSeparator.ts @@ -1,4 +1,4 @@ -import { ILlmHttpFunction } from "../structures/ILlmHttpFunction"; +import { IHttpLlmFunction } from "../structures/IHttpLlmFunction"; import { ILlmSchema } from "../structures/ILlmSchema"; import { LlmTypeChecker } from "../utils/LlmTypeChecker"; @@ -7,7 +7,7 @@ export namespace LlmSchemaSeparator { parameters: ILlmSchema[]; predicator: (schema: ILlmSchema) => boolean; } - export const parameters = (props: IProps): ILlmHttpFunction.ISeparated => { + export const parameters = (props: IProps): IHttpLlmFunction.ISeparated => { const indexes: Array<[ILlmSchema | null, ILlmSchema | null]> = props.parameters.map(schema(props.predicator)); return { diff --git a/src/converters/MigrateConverter.ts b/src/converters/MigrateConverter.ts index 6796533..120ebf5 100644 --- a/src/converters/MigrateConverter.ts +++ b/src/converters/MigrateConverter.ts @@ -1,6 +1,6 @@ import { OpenApi } from "../OpenApi"; -import { IMigrateDocument } from "../structures/IMigrateDocument"; -import { IMigrateRoute } from "../structures/IMigrateRoute"; +import { IHttpMigrateApplication } from "../structures/IHttpMigrateApplication"; +import { IHttpMigrateRoute } from "../structures/IHttpMigrateRoute"; import { StringUtil } from "../utils/StringUtil"; import { MigrateRouteAccessor } from "./MigrateRouteAccessor"; import { MigrateRouteConverter } from "./MigrateRouteConverter"; @@ -11,9 +11,9 @@ export namespace MigrateConverter { Operation extends OpenApi.IOperation, >( document: OpenApi.IDocument, - ): IMigrateDocument => { - const errors: IMigrateDocument.IError[] = []; - const entire: Array | null> = + ): IHttpMigrateApplication => { + const errors: IHttpMigrateApplication.IError[] = []; + const entire: Array | null> = Object.entries({ ...(document.paths ?? {}), ...(document.webhooks ?? {}), @@ -23,14 +23,14 @@ export namespace MigrateConverter { .filter((method) => collection[method] !== undefined) .map((method) => { const operation: Operation = collection[method]!; - const migrated: IMigrateRoute | string[] = + const migrated: IHttpMigrateRoute | string[] = MigrateRouteConverter.convert({ document, method, path, emendedPath: StringUtil.reJoinWithDecimalParameters(path), operation, - }) as IMigrateRoute | string[]; + }) as IHttpMigrateRoute | string[]; if (Array.isArray(migrated)) { errors.push({ method, @@ -44,8 +44,8 @@ export namespace MigrateConverter { }), ) .flat(); - const operations: IMigrateRoute[] = entire.filter( - (o): o is IMigrateRoute => !!o, + const operations: IHttpMigrateRoute[] = entire.filter( + (o): o is IHttpMigrateRoute => !!o, ); MigrateRouteAccessor.overwrite(operations); return { diff --git a/src/converters/MigrateRouteAccessor.ts b/src/converters/MigrateRouteAccessor.ts index 7bf1bc8..76053d5 100644 --- a/src/converters/MigrateRouteAccessor.ts +++ b/src/converters/MigrateRouteAccessor.ts @@ -1,5 +1,5 @@ import { OpenApi } from "../OpenApi"; -import { IMigrateRoute } from "../structures/IMigrateRoute"; +import { IHttpMigrateRoute } from "../structures/IHttpMigrateRoute"; import { Escaper } from "../utils/Escaper"; import { MapUtil } from "../utils/MapUtil"; import { StringUtil } from "../utils/StringUtil"; @@ -9,7 +9,7 @@ export namespace MigrateRouteAccessor { Schema extends OpenApi.IJsonSchema, Operation extends OpenApi.IOperation, >( - routes: IMigrateRoute[], + routes: IHttpMigrateRoute[], ): void => { const dict: Map> = collect((op) => op.emendedPath @@ -50,10 +50,10 @@ export namespace MigrateRouteAccessor { Schema extends OpenApi.IJsonSchema, Operation extends OpenApi.IOperation, >( - getter: (r: IMigrateRoute) => string[], + getter: (r: IHttpMigrateRoute) => string[], ) => ( - routes: IMigrateRoute[], + routes: IHttpMigrateRoute[], ): Map> => { const dict: Map> = new Map(); for (const r of routes) { @@ -94,7 +94,7 @@ export namespace MigrateRouteAccessor { Schema extends OpenApi.IJsonSchema, Operation extends OpenApi.IOperation, >( - op: IMigrateRoute, + op: IHttpMigrateRoute, ): string => { const method = op.method === "delete" ? "erase" : op.method; if (op.parameters.length === 0) return method; @@ -117,7 +117,7 @@ export namespace MigrateRouteAccessor { Schema extends OpenApi.IJsonSchema, Operation extends OpenApi.IOperation, > { - route: IMigrateRoute; + route: IHttpMigrateRoute; alias: string; } } diff --git a/src/converters/MigrateRouteConverter.ts b/src/converters/MigrateRouteConverter.ts index bef0ad0..362a2c4 100644 --- a/src/converters/MigrateRouteConverter.ts +++ b/src/converters/MigrateRouteConverter.ts @@ -1,7 +1,7 @@ import { OpenApi } from "../OpenApi"; -import { OpenApiTypeChecker } from "../OpenApiTypeChecker"; -import { IMigrateRoute } from "../structures/IMigrateRoute"; +import { IHttpMigrateRoute } from "../structures/IHttpMigrateRoute"; import { Escaper } from "../utils/Escaper"; +import { OpenApiTypeChecker } from "../utils/OpenApiTypeChecker"; import { StringUtil } from "../utils/StringUtil"; export namespace MigrateRouteConverter { @@ -12,11 +12,11 @@ export namespace MigrateRouteConverter { emendedPath: string; operation: OpenApi.IOperation; } - export const convert = (props: IProps): IMigrateRoute | string[] => { + export const convert = (props: IProps): IHttpMigrateRoute | string[] => { //---- // REQUEST AND RESPONSE BODY //---- - const body: false | null | IMigrateRoute.IBody = emplaceBodySchema( + const body: false | null | IHttpMigrateRoute.IBody = emplaceBodySchema( "request", )((schema) => emplaceReference({ @@ -25,7 +25,7 @@ export namespace MigrateRouteConverter { schema, }), )(props.operation.requestBody); - const success: false | null | IMigrateRoute.IBody = emplaceBodySchema( + const success: false | null | IHttpMigrateRoute.IBody = emplaceBodySchema( "response", )((schema) => emplaceReference({ @@ -97,7 +97,7 @@ export namespace MigrateRouteConverter { description: () => elem.description, example: () => elem.example, examples: () => elem.examples, - }) satisfies IMigrateRoute.IHeaders; + }) satisfies IHttpMigrateRoute.IHeaders; if (objects.length === 1 && primitives.length === 0) return out(parameters[0]); @@ -220,7 +220,7 @@ export namespace MigrateRouteConverter { ); if (failures.length) return failures; - const parameters: IMigrateRoute.IParameter[] = ( + const parameters: IHttpMigrateRoute.IParameter[] = ( props.operation.parameters ?? [] ) .filter((p) => p.in === "path") @@ -261,8 +261,8 @@ export namespace MigrateRouteConverter { })), headers: headers || null, query: query || null, - body: body as IMigrateRoute.IBody | null, - success: success as IMigrateRoute.IBody | null, + body: body as IHttpMigrateRoute.IBody | null, + success: success as IHttpMigrateRoute.IBody | null, exceptions: Object.fromEntries( Object.entries(props.operation.responses ?? {}) .filter( @@ -290,9 +290,9 @@ export namespace MigrateRouteConverter { const writeRouteComment = (props: { operation: OpenApi.IOperation; - parameters: IMigrateRoute.IParameter[]; - query: IMigrateRoute.IQuery | null; - body: IMigrateRoute.IBody | null; + parameters: IHttpMigrateRoute.IParameter[]; + query: IHttpMigrateRoute.IQuery | null; + body: IHttpMigrateRoute.IBody | null; }): string => { const commentTags: string[] = []; const add = (text: string) => { @@ -355,7 +355,7 @@ export namespace MigrateRouteConverter { description?: string; content?: Partial>; // ISwaggerRouteBodyContent; "x-nestia-encrypted"?: boolean; - }): false | null | IMigrateRoute.IBody => { + }): false | null | IHttpMigrateRoute.IBody => { if (!meta?.content) return null; const entries: [string, OpenApi.IOperation.IMediaType][] = Object.entries( diff --git a/src/converters/OpenApiTypeChecker.ts b/src/converters/OpenApiTypeChecker.ts deleted file mode 100644 index 64f9a56..0000000 --- a/src/converters/OpenApiTypeChecker.ts +++ /dev/null @@ -1,2 +0,0 @@ -// FOR LEGACY VERSIONS -export * from "../OpenApiTypeChecker"; diff --git a/src/converters/OpenApiV3Downgrader.ts b/src/converters/OpenApiV3Downgrader.ts index 108a1ac..6c9a71d 100644 --- a/src/converters/OpenApiV3Downgrader.ts +++ b/src/converters/OpenApiV3Downgrader.ts @@ -1,6 +1,6 @@ import { OpenApi } from "../OpenApi"; -import { OpenApiTypeChecker } from "../OpenApiTypeChecker"; import { OpenApiV3 } from "../OpenApiV3"; +import { OpenApiTypeChecker } from "../utils/OpenApiTypeChecker"; export namespace OpenApiV3Downgrader { export interface IComponentsCollection { diff --git a/src/converters/SwaggerV2Downgrader.ts b/src/converters/SwaggerV2Downgrader.ts index cdd08a4..7d70ad2 100644 --- a/src/converters/SwaggerV2Downgrader.ts +++ b/src/converters/SwaggerV2Downgrader.ts @@ -1,6 +1,6 @@ import { OpenApi } from "../OpenApi"; -import { OpenApiTypeChecker } from "../OpenApiTypeChecker"; import { SwaggerV2 } from "../SwaggerV2"; +import { OpenApiTypeChecker } from "../utils/OpenApiTypeChecker"; export namespace SwaggerV2Downgrader { export interface IComponentsCollection { diff --git a/src/http/LlmFunctionFetcher.ts b/src/http/HttpLlmFunctionFetcher.ts similarity index 67% rename from src/http/LlmFunctionFetcher.ts rename to src/http/HttpLlmFunctionFetcher.ts index a9d9ac5..ac2c715 100644 --- a/src/http/LlmFunctionFetcher.ts +++ b/src/http/HttpLlmFunctionFetcher.ts @@ -1,21 +1,21 @@ import { IHttpConnection } from "../structures/IHttpConnection"; +import { IHttpLlmApplication } from "../structures/IHttpLlmApplication"; +import { IHttpLlmFunction } from "../structures/IHttpLlmFunction"; +import { IHttpMigrateRoute } from "../structures/IHttpMigrateRoute"; import { IHttpResponse } from "../structures/IHttpResponse"; -import { ILlmHttpApplication } from "../structures/ILlmHttpApplication"; -import { ILlmHttpFunction } from "../structures/ILlmHttpFunction"; -import { IMigrateRoute } from "../structures/IMigrateRoute"; -import { MigrateRouteFetcher } from "./MigrateRouteFetcher"; +import { HttpMigrateRouteFetcher } from "./HttpMigrateRouteFetcher"; -export namespace LlmFunctionFetcher { +export namespace HttpLlmFunctionFetcher { export interface IProps { /** * Document of the OpenAI function call schemas. */ - document: ILlmHttpApplication; + document: IHttpLlmApplication; /** * Procedure schema to call. */ - procedure: ILlmHttpFunction; + procedure: IHttpLlmFunction; /** * Connection info to the server. @@ -29,16 +29,16 @@ export namespace LlmFunctionFetcher { } export const execute = async (props: IProps): Promise => - MigrateRouteFetcher.request(getFetchArguments(props)); + HttpMigrateRouteFetcher.request(getFetchArguments(props)); export const propagate = async (props: IProps): Promise => - MigrateRouteFetcher.propagate(getFetchArguments(props)); + HttpMigrateRouteFetcher.propagate(getFetchArguments(props)); - const getFetchArguments = (props: IProps): MigrateRouteFetcher.IProps => { - const route: IMigrateRoute = props.procedure.route(); + const getFetchArguments = (props: IProps): HttpMigrateRouteFetcher.IProps => { + const route: IHttpMigrateRoute = props.procedure.route(); if (props.document.options.keyword === true) { const input: Pick< - MigrateRouteFetcher.IProps, + HttpMigrateRouteFetcher.IProps, "parameters" | "query" | "body" > = props.arguments[0]; return { diff --git a/src/http/MigrateRouteFetcher.ts b/src/http/HttpMigrateRouteFetcher.ts similarity index 87% rename from src/http/MigrateRouteFetcher.ts rename to src/http/HttpMigrateRouteFetcher.ts index 719adba..d75a194 100644 --- a/src/http/MigrateRouteFetcher.ts +++ b/src/http/HttpMigrateRouteFetcher.ts @@ -1,12 +1,12 @@ import { IHttpConnection } from "../structures/IHttpConnection"; +import { IHttpMigrateRoute } from "../structures/IHttpMigrateRoute"; import { IHttpResponse } from "../structures/IHttpResponse"; -import { IMigrateRoute } from "../structures/IMigrateRoute"; import { HttpError } from "./HttpError"; -export namespace MigrateRouteFetcher { +export namespace HttpMigrateRouteFetcher { export interface IProps { connection: IHttpConnection; - route: IMigrateRoute; + route: IHttpMigrateRoute; parameters: | Array | Record; @@ -16,7 +16,8 @@ export namespace MigrateRouteFetcher { export const request = async (props: IProps): Promise => { const result: IHttpResponse = await _Propagate("request", props); - if (result.status === 200 || result.status === 201) + props.route.success?.media; + if (result.status !== 200 && result.status !== 201) throw new HttpError( props.route.method.toUpperCase() as "GET", props.route.path, @@ -33,7 +34,7 @@ export namespace MigrateRouteFetcher { const _Propagate = async ( from: string, - props: MigrateRouteFetcher.IProps, + props: HttpMigrateRouteFetcher.IProps, ): Promise => { // VALIDATE PARAMETERS const error = (message: string) => @@ -57,6 +58,10 @@ const _Propagate = async ( // INIT REQUEST DATA const headers: Record = { ...(props.connection.headers ?? {}), + ...(props.route.body?.type && + props.route.body.type !== "multipart/form-data" + ? { "Content-Type": props.route.body.type } + : {}), }; const init: RequestInit = { ...(props.connection.options ?? {}), @@ -72,13 +77,15 @@ const _Propagate = async ( })(), }; if (props.body !== undefined) - props.route.body?.type === "application/x-www-form-urlencoded" - ? requestQueryBody(props.body) - : props.route.body?.type === "multipart/form-data" - ? requestFormDataBody(props.body) - : props.route.body?.type !== "text/plain" - ? JSON.stringify(props.body) - : props.body; + init.body = ( + props.route.body?.type === "application/x-www-form-urlencoded" + ? requestQueryBody(props.body) + : props.route.body?.type === "multipart/form-data" + ? requestFormDataBody(props.body) + : props.route.body?.type === "application/json" + ? JSON.stringify(props.body) + : props.body + ) as any; // DO REQUEST const path: string = @@ -86,7 +93,7 @@ const _Propagate = async ( props.route.path[0] !== "/" ? `/${getPath(props)}` : getPath(props); - const url: URL = new URL(`${props.connection.host}${path}`); + const url: URL = new URL(`${props.connection.host}/${path}`); const response: Response = await (props.connection.fetch ?? fetch)(url, init); const status: number = response.status; @@ -135,7 +142,7 @@ const _Propagate = async ( }; const getPath = ( - props: Pick, + props: Pick, ): string => { let path: string = props.route.emendedPath; props.route.parameters.forEach((p, i) => { diff --git a/src/index.ts b/src/index.ts index 277ff89..909dfed 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,14 +1,20 @@ -export * from "./structures/ILlmHttpApplication"; -export * from "./structures/ILlmHttpFunction"; +// STRUCTURES +export * from "./structures/IHttpLlmApplication"; +export * from "./structures/IHttpLlmFunction"; +export * from "./structures/IHttpMigrateRoute"; +export * from "./structures/IHttpMigrateApplication"; export * from "./structures/ILlmSchema"; -export * from "./structures/IMigrateRoute"; -export * from "./structures/IMigrateDocument"; +// HTTP INTERACTION export * from "./http/HttpError"; -export * from "./http/LlmFunctionFetcher"; -export * from "./http/MigrateRouteFetcher"; -export * from "./OpenApiTypeChecker"; +export * from "./http/HttpLlmFunctionFetcher"; +export * from "./http/HttpMigrateRouteFetcher"; +// UTILS +export * from "./utils/OpenApiTypeChecker"; +export * from "./utils/LlmTypeChecker"; + +// OPENAPI MODULES export * from "./OpenApi"; export * from "./SwaggerV2"; export * from "./OpenApiV3"; diff --git a/src/structures/ILlmHttpApplication.ts b/src/structures/IHttpLlmApplication.ts similarity index 80% rename from src/structures/ILlmHttpApplication.ts rename to src/structures/IHttpLlmApplication.ts index c1a2b56..5a2d8a3 100644 --- a/src/structures/ILlmHttpApplication.ts +++ b/src/structures/IHttpLlmApplication.ts @@ -1,12 +1,12 @@ import { OpenApi } from "../OpenApi"; -import { ILlmHttpFunction } from "./ILlmHttpFunction"; +import { IHttpLlmFunction } from "./IHttpLlmFunction"; +import { IHttpMigrateRoute } from "./IHttpMigrateRoute"; import { ILlmSchema } from "./ILlmSchema"; -import { IMigrateRoute } from "./IMigrateRoute"; -export interface ILlmHttpApplication< +export interface IHttpLlmApplication< Schema extends ILlmSchema = ILlmSchema, Operation extends OpenApi.IOperation = OpenApi.IOperation, - Route extends IMigrateRoute = IMigrateRoute, + Route extends IHttpMigrateRoute = IHttpMigrateRoute, > { /** * Version of OpenAPI. @@ -23,12 +23,12 @@ export interface ILlmHttpApplication< * When you want to execute the function with LLM constructed arguments, * you can do it through {@link LlmFetcher.execute} function. */ - functions: ILlmHttpFunction[]; + functions: IHttpLlmFunction[]; /** * List of errors occurred during the composition. */ - errors: ILlmHttpApplication.IError[]; + errors: IHttpLlmApplication.IError[]; /** * Options for the document. @@ -36,15 +36,15 @@ export interface ILlmHttpApplication< * Adjusted options when composing the document through * {@link OpenApi.llm} function. */ - options: ILlmHttpApplication.IOptions; + options: IHttpLlmApplication.IOptions; } -export namespace ILlmHttpApplication { +export namespace IHttpLlmApplication { /** * Error occurred in the composition. */ export interface IError< Operation extends OpenApi.IOperation = OpenApi.IOperation, - Route extends IMigrateRoute = IMigrateRoute, + Route extends IHttpMigrateRoute = IHttpMigrateRoute, > { /** * HTTP method of the endpoint. @@ -89,13 +89,13 @@ export namespace ILlmHttpApplication { * Whether the parameters are keyworded or not. * * If this property value is `true`, length of the - * {@link ILlmHttpApplication.IFunction.parameters} is always 1, and type of + * {@link IHttpLlmApplication.IFunction.parameters} is always 1, and type of * the pararameter is always {@link ILlmSchema.IObject} type. * Also, its properties are following below rules: * - * - `pathParameters`: Path parameters of {@link IMigrateRoute.parameters} - * - `query`: Query parameter of {@link IMigrateRoute.query} - * - `body`: Body parameter of {@link IMigrateRoute.body} + * - `pathParameters`: Path parameters of {@link IHttpMigrateRoute.parameters} + * - `query`: Query parameter of {@link IHttpMigrateRoute.query} + * - `body`: Body parameter of {@link IHttpMigrateRoute.body} * * ```typescript * { @@ -106,7 +106,7 @@ export namespace ILlmHttpApplication { * ``` * * Otherwise (this property value is `false`), length of the - * {@link ILlmHttpFunction.parameters} is variable, and sequence of the + * {@link IHttpLlmFunction.parameters} is variable, and sequence of the * parameters are following below rules. * * ```typescript @@ -136,8 +136,8 @@ export namespace ILlmHttpApplication { * predicating whether the schema value must be composed by human or * not, the parameters would be separated into two parts. * - * - {@link ILlmHttpFunction.separated.llm} - * - {@link ILlmHttpFunction.separated.human} + * - {@link IHttpLlmFunction.separated.llm} + * - {@link IHttpLlmFunction.separated.human} * * When writing the function, note that returning value `true` means * to be a human composing the value, and `false` means to LLM diff --git a/src/structures/ILlmHttpFunction.ts b/src/structures/IHttpLlmFunction.ts similarity index 81% rename from src/structures/ILlmHttpFunction.ts rename to src/structures/IHttpLlmFunction.ts index 056eb60..dc9c527 100644 --- a/src/structures/ILlmHttpFunction.ts +++ b/src/structures/IHttpLlmFunction.ts @@ -1,38 +1,38 @@ import { OpenApi } from "../OpenApi"; +import { IHttpMigrateRoute } from "./IHttpMigrateRoute"; import { ILlmSchema } from "./ILlmSchema"; -import { IMigrateRoute } from "./IMigrateRoute"; /** * LLM function metadata from HTTP (OpenAPI) operation. * - * `ILlmHttpFunction` is a data structure representing a procedure converted + * `IHttpLlmFunction` is a data structure representing a procedure converted * from the OpenAPI operation, used for the LLM (Large Language Model) * function calling. It's a typical RPC (Remote Procedure Call) structure * containing the procedure {@link name}, {@link parameters}, and * {@link output return type}. * - * If you provide this `ILlmHttpFunction` data to the LLM like "OpenAI", + * If you provide this `IHttpLlmFunction` data to the LLM like "OpenAI", * the "OpenAI" will compose a function arguments by analyzing * conversations with the user. With the LLM composed arguments, you can * execute the procedure through {@link LlmFetcher.execute} and get the * result. * - * For reference, different between `ILlmHttpFunction` and its origin source - * {@link OpenApi.IOperation} is, `ILlmHttpFunction` has converted every type + * For reference, different between `IHttpLlmFunction` and its origin source + * {@link OpenApi.IOperation} is, `IHttpLlmFunction` has converted every type * schema informations from {@link OpenApi.IJsonSchema} to {@link ILlmSchema} * to escape {@link OpenApi.IJsonSchema.IReference reference types}, and * downgrade the version of the JSON schema to OpenAPI 3.0. It's because * LLM function call feature cannot understand both reference types and * OpenAPI 3.1 specification. * - * Additionally, if you've composed `ILlmHttpFunction` with - * {@link ILlmHttpApplication.IOptions.keyword} configuration as `true`, number of - * {@link ILlmHttpFunction.parameters} are always 1 and the first parameter's + * Additionally, if you've composed `IHttpLlmFunction` with + * {@link IHttpLlmApplication.IOptions.keyword} configuration as `true`, number of + * {@link IHttpLlmFunction.parameters} are always 1 and the first parameter's * type is always {@link ILlmSchema.IObject}. The properties' rule is: * * - `pathParameters`: Path parameters of {@link OpenApi.IOperation.parameters} - * - `query`: Query parameter of {@link IMigrateRoute.query} - * - `body`: Body parameter of {@link IMigrateRoute.body} + * - `query`: Query parameter of {@link IHttpMigrateRoute.query} + * - `body`: Body parameter of {@link IHttpMigrateRoute.body} * * ```typescript * { @@ -56,10 +56,10 @@ import { IMigrateRoute } from "./IMigrateRoute"; * @reference https://platform.openai.com/docs/guides/function-calling * @author Jeongho Nam - https://github.com/samchon */ -export interface ILlmHttpFunction< +export interface IHttpLlmFunction< Schema extends ILlmSchema = ILlmSchema, Operation extends OpenApi.IOperation = OpenApi.IOperation, - Route extends IMigrateRoute = IMigrateRoute, + Route extends IHttpMigrateRoute = IHttpMigrateRoute, > { /** * HTTP method of the endpoint. @@ -75,10 +75,10 @@ export interface ILlmHttpFunction< * Representative name of the function. * * The `name` is a repsentative name identifying the function in the - * {@link ILlmHttpApplication}. The `name` value is just composed by joining the - * {@link IMigrateRoute.accessor} by underscore `_` character. + * {@link IHttpLlmApplication}. The `name` value is just composed by joining the + * {@link IHttpMigrateRoute.accessor} by underscore `_` character. * - * Here is the composition rule of the {@link IMigrateRoute.accessor}: + * Here is the composition rule of the {@link IHttpMigrateRoute.accessor}: * * > The `accessor` is composed with the following rules. At first, * > namespaces are composed by static directory names in the {@link path}. @@ -117,14 +117,14 @@ export interface ILlmHttpFunction< /** * List of parameter types. * - * If you've configured {@link ILlmHttpApplication.IOptions.keyword} as `true`, - * number of {@link ILlmHttpFunction.parameters} are always 1 and the first + * If you've configured {@link IHttpLlmApplication.IOptions.keyword} as `true`, + * number of {@link IHttpLlmFunction.parameters} are always 1 and the first * parameter's type is always {@link ILlmSchema.IObject}. The * properties' rule is: * - * - `pathParameters`: Path parameters of {@link IMigrateRoute.parameters} - * - `query`: Query parameter of {@link IMigrateRoute.query} - * - `body`: Body parameter of {@link IMigrateRoute.body} + * - `pathParameters`: Path parameters of {@link IHttpMigrateRoute.parameters} + * - `query`: Query parameter of {@link IHttpMigrateRoute.query} + * - `body`: Body parameter of {@link IHttpMigrateRoute.body} * * ```typescript * { @@ -150,9 +150,9 @@ export interface ILlmHttpFunction< /** * Collection of separated parameters. * - * Filled only when {@link ILlmHttpApplication.IOptions.separate} is configured. + * Filled only when {@link IHttpLlmApplication.IOptions.separate} is configured. */ - separated?: ILlmHttpFunction.ISeparated; + separated?: IHttpLlmFunction.ISeparated; /** * Expected return type. @@ -165,7 +165,7 @@ export interface ILlmHttpFunction< /** * Description of the procedure. * - * `ILlmHttpFunction.description` is composed by below rule: + * `IHttpLlmFunction.description` is composed by below rule: * * 1. Starts from the {@link OpenApi.IOperation.summary} paragraph. * 2. The next paragraphs are filled with the @@ -206,7 +206,7 @@ export interface ILlmHttpFunction< */ route: () => Route; } -export namespace ILlmHttpFunction { +export namespace IHttpLlmFunction { /** * Collection of separated parameters. */ diff --git a/src/structures/IMigrateDocument.ts b/src/structures/IHttpMigrateApplication.ts similarity index 76% rename from src/structures/IMigrateDocument.ts rename to src/structures/IHttpMigrateApplication.ts index 57bb48e..e88fa11 100644 --- a/src/structures/IMigrateDocument.ts +++ b/src/structures/IHttpMigrateApplication.ts @@ -1,39 +1,39 @@ import { OpenApi } from "../OpenApi"; -import { IMigrateRoute } from "./IMigrateRoute"; +import { IHttpMigrateRoute } from "./IHttpMigrateRoute"; /** * Document of migration. * - * The `IMigrateDocument` interface is a document of migration from + * The `IHttpMigrateApplication` interface is an application migrated from * {@link OpenAPI.IDocument OpenAPI document} to RPC (Remote Procedure Call) - * functions; {@link IMigrateRoute}. + * functions; {@link IHttpMigrateRoute}. * - * As the `IMigrateDocument` and {@link IMigrateRoute} have a lot of special + * As the `IHttpMigrateApplication` and {@link IHttpMigrateRoute} have a lot of special * stories, when you're developing OpenAPI generator library, please read * their descriptions carefully including the description of properties. * * @author Jeongho Nam - https://github.com/samchon */ -export interface IMigrateDocument< +export interface IHttpMigrateApplication< Schema extends OpenApi.IJsonSchema = OpenApi.IJsonSchema, Operation extends OpenApi.IOperation = OpenApi.IOperation, > { /** * List of routes for migration. */ - routes: IMigrateRoute[]; + routes: IHttpMigrateRoute[]; /** * List of errors occurred during the migration. */ - errors: IMigrateDocument.IError[]; + errors: IHttpMigrateApplication.IError[]; /** * Source OpenAPI document. */ document: () => OpenApi.IDocument; } -export namespace IMigrateDocument { +export namespace IHttpMigrateApplication { /** * Error of migration in the operation level. */ diff --git a/src/structures/IMigrateRoute.ts b/src/structures/IHttpMigrateRoute.ts similarity index 89% rename from src/structures/IMigrateRoute.ts rename to src/structures/IHttpMigrateRoute.ts index 64000cb..c262fb0 100644 --- a/src/structures/IMigrateRoute.ts +++ b/src/structures/IHttpMigrateRoute.ts @@ -3,17 +3,17 @@ import { OpenApi } from "../OpenApi"; /** * Route information for migration. * - * The `IMigrateRoute` is a structure representing a route information for + * The `IHttpMigrateRoute` is a structure representing a route information for * OpenAPI generated RPC (Remote Procedure Call) function composed from the * {@link OpenApi.IOperation OpenAPI operation}. * - * As the `IMigrateRoute` has a lot of speical stories, when you're developing + * As the `IHttpMigrateRoute` has a lot of speical stories, when you're developing * OpenAPI generator library, please read its description carefully including * the description of its properties. * * @author Jeongho Nam - https://github.com/samchon */ -export interface IMigrateRoute< +export interface IHttpMigrateRoute< Schema extends OpenApi.IJsonSchema = OpenApi.IJsonSchema, Operation extends OpenApi.IOperation = OpenApi.IOperation, > { @@ -78,7 +78,7 @@ export interface IMigrateRoute< * * Note that, not a list of every parameters, but only path parameters. */ - parameters: IMigrateRoute.IParameter[]; + parameters: IHttpMigrateRoute.IParameter[]; /** * Metadata of headers. @@ -86,16 +86,16 @@ export interface IMigrateRoute< * The `headers` property is a metadata of HTTP request headers for RPC function, * including the parameter variable name and schema. * - * Also, its {@link IMigrateRoute.IHeaders.schema} is always object or reference + * Also, its {@link IHttpMigrateRoute.IHeaders.schema} is always object or reference * to object. Even though the original {@link OpenApi.IOperation OpenAPI operation}'s * headers are separated to atomic typed properties, the `headers` property forcibly * combines them into a single object type. * * For reference, if the `headers` property has been converted to an object type - * forcibly, its property {@link IMigrateRoute.IHeaders.name name} and - * {@link IMigrateRoute.IHeaders.key key} are always "headers". + * forcibly, its property {@link IHttpMigrateRoute.IHeaders.name name} and + * {@link IHttpMigrateRoute.IHeaders.key key} are always "headers". */ - headers: IMigrateRoute.IHeaders | null; + headers: IHttpMigrateRoute.IHeaders | null; /** * Metadata of query values. @@ -103,16 +103,16 @@ export interface IMigrateRoute< * The `query` property is a metadata of HTTP request query values for RPC function, * including the parameter variable name and schema. * - * Also, its {@link IMigrateRoute.IQuery.schema} is always object or reference + * Also, its {@link IHttpMigrateRoute.IQuery.schema} is always object or reference * to object. Even though the original {@link OpenApi.IOperation OpenAPI operation}'s * query parameters are separated to atomic typed properties, the `query` property * forcibly combines them into a single object type. * * For reference, if the `query` property has been converted to an object type - * forcibly, its property {@link IMigrateRoute.IQuery.name name} and - * {@link IMigrateRoute.IQuery.key key} are always "headers". + * forcibly, its property {@link IHttpMigrateRoute.IQuery.name name} and + * {@link IHttpMigrateRoute.IQuery.key key} are always "headers". */ - query: IMigrateRoute.IQuery | null; + query: IHttpMigrateRoute.IQuery | null; /** * Metadata of request body. @@ -123,7 +123,7 @@ export interface IMigrateRoute< * If the `body` property is `null`, it means the operation does not require * the request body data. */ - body: IMigrateRoute.IBody | null; + body: IHttpMigrateRoute.IBody | null; /** * Metadata of response body for success case. @@ -134,7 +134,7 @@ export interface IMigrateRoute< * If the `success` property is `null`, it means the operation does not have * the response body data. In other words, the RPC function would return `void`. */ - success: IMigrateRoute.IBody | null; + success: IHttpMigrateRoute.IBody | null; /** * Metadata of response body for exceptional status cases. @@ -147,7 +147,7 @@ export interface IMigrateRoute< * stringified number, but sometimes it could be a string like "default", * because the OpenAPI document allows the status code to be a string. */ - exceptions: Record>; + exceptions: Record>; /** * Description comment for the route function. @@ -173,7 +173,7 @@ export interface IMigrateRoute< */ operation: () => Operation; } -export namespace IMigrateRoute { +export namespace IHttpMigrateRoute { /** * Metadata of path parameter. */ diff --git a/src/OpenApiTypeChecker.ts b/src/utils/OpenApiTypeChecker.ts similarity index 99% rename from src/OpenApiTypeChecker.ts rename to src/utils/OpenApiTypeChecker.ts index 1d1d4ff..79e79af 100644 --- a/src/OpenApiTypeChecker.ts +++ b/src/utils/OpenApiTypeChecker.ts @@ -1,5 +1,5 @@ -import { OpenApi } from "./OpenApi"; -import { MapUtil } from "./utils/MapUtil"; +import { OpenApi } from "../OpenApi"; +import { MapUtil } from "./MapUtil"; export namespace OpenApiTypeChecker { export const visit = diff --git a/test/controllers/AppController.ts b/test/controllers/AppController.ts index 5d24c5c..9b2554f 100644 --- a/test/controllers/AppController.ts +++ b/test/controllers/AppController.ts @@ -6,6 +6,7 @@ import { TypedRoute, } from "@nestia/core"; import { Controller, Query } from "@nestjs/common"; +import { tags } from "typia"; @Controller() export class AppController { @@ -24,11 +25,7 @@ export class AppController { @TypedParam("b") b: number, @TypedParam("c") c: boolean, @TypedQuery() - query: { - flag: boolean; - value: number; - text: string; - }, + query: IQuery, ) { return { a, b, c, query }; } @@ -39,11 +36,7 @@ export class AppController { @TypedParam("b") b: number, @TypedParam("c") c: boolean, @TypedBody() - body: { - flag: boolean; - value: number; - text: string; - }, + body: IBody, ) { return { a, b, c, body }; } @@ -53,18 +46,14 @@ export class AppController { @TypedParam("a") a: string, @TypedParam("b") b: number, @TypedParam("c") c: boolean, - @Query("name") name: string, - @Query("reference") reference: string, + @Query("thumbnail") + thumbnail: string & tags.Format<"uri"> & tags.ContentMediaType<"image/*">, @TypedQuery() query: { - memo: string; + summary: string; }, @TypedBody() - body: { - flag: boolean; - value: number; - text: string; - }, + body: IBody, ) { return { a, @@ -72,8 +61,7 @@ export class AppController { c, query: { ...query, - name, - reference, + thumbnail, }, body, }; @@ -84,13 +72,9 @@ export class AppController { @TypedParam("a") a: string, @TypedParam("b") b: number, @TypedParam("c") c: boolean, - @TypedQuery() query: { flag: boolean; value: number; text: string }, + @TypedQuery() query: IQuery, @TypedFormData.Body() - body: { - name: string; - reference: string; - file: File; - }, + body: IMultipart, ) { return { a, @@ -98,10 +82,22 @@ export class AppController { c, query, body: { - name: body.name, - reference: body.reference, - file: `http://localhost:3000/files/${body.file.name}`, + ...body, + file: `http://localhost:3000/files/${Date.now()}.raw`, }, }; } } + +interface IQuery { + summary: string; + thumbnail: string & tags.Format<"uri"> & tags.ContentMediaType<"image/*">; +} +interface IBody { + title: string; + body: string; + draft: boolean; +} +interface IMultipart extends IBody { + file: File; +} diff --git a/test/controllers/AppFilter.ts b/test/controllers/AppFilter.ts new file mode 100644 index 0000000..354b8b5 --- /dev/null +++ b/test/controllers/AppFilter.ts @@ -0,0 +1,15 @@ +import { ArgumentsHost, Catch, HttpException, Logger } from "@nestjs/common"; +import { HttpArgumentsHost } from "@nestjs/common/interfaces"; +import { BaseExceptionFilter } from "@nestjs/core"; + +@Catch() +export class AppFilter extends BaseExceptionFilter { + private readonly logger = new Logger(AppFilter.name); + + public async catch(exception: HttpException | Error, host: ArgumentsHost) { + const status: number = + exception instanceof HttpException ? exception.getStatus() : 500; + if (status === 500) console.info(exception); + return super.catch(exception, host); + } +} diff --git a/test/features/migrate/test_http_migrate_fetch_body.ts b/test/features/migrate/test_http_migrate_fetch_body.ts new file mode 100644 index 0000000..5316f2b --- /dev/null +++ b/test/features/migrate/test_http_migrate_fetch_body.ts @@ -0,0 +1,38 @@ +import { TestValidator } from "@nestia/e2e"; +import { + HttpMigrateRouteFetcher, + IHttpMigrateApplication, + IHttpMigrateRoute, +} from "@samchon/openapi"; +import { OpenApi } from "@samchon/openapi/lib/OpenApi"; +import { IHttpConnection } from "@samchon/openapi/lib/structures/IHttpConnection"; +import { IHttpResponse } from "@samchon/openapi/lib/structures/IHttpResponse"; + +import swagger from "../../swagger.json"; + +export const test_http_migrate_fetch_body = async ( + connection: IHttpConnection, +): Promise => { + const document: OpenApi.IDocument = OpenApi.convert(swagger as any); + const app: IHttpMigrateApplication = OpenApi.migrate(document); + const route: IHttpMigrateRoute | undefined = app.routes.find( + (r) => r.path === "/{a}/{b}/{c}/body" && r.method === "post", + ); + if (route === undefined) throw new Error("Route not found"); + + const response: IHttpResponse = await HttpMigrateRouteFetcher.propagate({ + connection, + route, + parameters: { + a: "string", + b: 123, + c: true, + }, + body: { + title: "some title", + body: "some body", + draft: false, + }, + }); + TestValidator.equals("response.status")(response.status)(201); +}; diff --git a/test/features/migrate/test_http_migrate_fetch_keyword_parameters.ts b/test/features/migrate/test_http_migrate_fetch_keyword_parameters.ts new file mode 100644 index 0000000..0384e02 --- /dev/null +++ b/test/features/migrate/test_http_migrate_fetch_keyword_parameters.ts @@ -0,0 +1,30 @@ +import { + HttpMigrateRouteFetcher, + IHttpMigrateApplication, + IHttpMigrateRoute, + OpenApi, +} from "@samchon/openapi"; +import { IHttpConnection } from "@samchon/openapi/lib/structures/IHttpConnection"; + +import swagger from "../../swagger.json"; + +export const test_http_migrate_fetch_keyword_parameters = async ( + connection: IHttpConnection, +): Promise => { + const document: OpenApi.IDocument = OpenApi.convert(swagger as any); + const app: IHttpMigrateApplication = OpenApi.migrate(document); + const route: IHttpMigrateRoute | undefined = app.routes.find( + (r) => r.path === "/{a}/{b}/{c}/parameters" && r.method === "get", + ); + if (route === undefined) throw new Error("Route not found"); + + await HttpMigrateRouteFetcher.request({ + connection, + route, + parameters: { + a: "three", + b: 2, + c: true, + }, + }); +}; diff --git a/test/features/migrate/test_http_migrate_fetch_multipart.ts b/test/features/migrate/test_http_migrate_fetch_multipart.ts new file mode 100644 index 0000000..60a873c --- /dev/null +++ b/test/features/migrate/test_http_migrate_fetch_multipart.ts @@ -0,0 +1,40 @@ +import { + HttpMigrateRouteFetcher, + IHttpMigrateApplication, + IHttpMigrateRoute, + OpenApi, +} from "@samchon/openapi"; +import { IHttpConnection } from "@samchon/openapi/lib/structures/IHttpConnection"; + +import swagger from "../../swagger.json"; + +export const test_http_migrate_fetch_multipart = async ( + connection: IHttpConnection, +): Promise => { + const document: OpenApi.IDocument = OpenApi.convert(swagger as any); + const app: IHttpMigrateApplication = OpenApi.migrate(document); + const route: IHttpMigrateRoute | undefined = app.routes.find( + (r) => r.path === "/{a}/{b}/{c}/multipart" && r.method === "post", + ); + if (route === undefined) throw new Error("Route not found"); + + await HttpMigrateRouteFetcher.request({ + connection, + route, + parameters: { + a: "three", + b: 2, + c: true, + }, + query: { + summary: "some summary", + thumbnail: "https://some.url", + }, + body: { + title: "some title", + body: "some body", + draft: false, + file: new File([new Uint8Array(999).fill(1)], "file.txt"), + }, + }); +}; diff --git a/test/features/migrate/test_http_migrate_fetch_positional_parameters.ts b/test/features/migrate/test_http_migrate_fetch_positional_parameters.ts new file mode 100644 index 0000000..b3259b8 --- /dev/null +++ b/test/features/migrate/test_http_migrate_fetch_positional_parameters.ts @@ -0,0 +1,26 @@ +import { + HttpMigrateRouteFetcher, + IHttpMigrateApplication, + IHttpMigrateRoute, + OpenApi, +} from "@samchon/openapi"; +import { IHttpConnection } from "@samchon/openapi/lib/structures/IHttpConnection"; + +import swagger from "../../swagger.json"; + +export const test_http_migrate_fetch_positional_parameters = async ( + connection: IHttpConnection, +): Promise => { + const document: OpenApi.IDocument = OpenApi.convert(swagger as any); + const app: IHttpMigrateApplication = OpenApi.migrate(document); + const route: IHttpMigrateRoute | undefined = app.routes.find( + (r) => r.path === "/{a}/{b}/{c}/parameters" && r.method === "get", + ); + if (route === undefined) throw new Error("Route not found"); + + await HttpMigrateRouteFetcher.request({ + connection, + route, + parameters: ["three", 2, true], + }); +}; diff --git a/test/features/migrate/test_http_migrate_fetch_propagate.ts b/test/features/migrate/test_http_migrate_fetch_propagate.ts new file mode 100644 index 0000000..1648e35 --- /dev/null +++ b/test/features/migrate/test_http_migrate_fetch_propagate.ts @@ -0,0 +1,29 @@ +import { TestValidator } from "@nestia/e2e"; +import { + HttpMigrateRouteFetcher, + IHttpMigrateApplication, + IHttpMigrateRoute, + OpenApi, +} from "@samchon/openapi"; +import { IHttpConnection } from "@samchon/openapi/lib/structures/IHttpConnection"; +import { IHttpResponse } from "@samchon/openapi/lib/structures/IHttpResponse"; + +import swagger from "../../swagger.json"; + +export const test_http_migrate_fetch_propagate = async ( + connection: IHttpConnection, +): Promise => { + const document: OpenApi.IDocument = OpenApi.convert(swagger as any); + const app: IHttpMigrateApplication = OpenApi.migrate(document); + const route: IHttpMigrateRoute | undefined = app.routes.find( + (r) => r.path === "/{a}/{b}/{c}/parameters" && r.method === "get", + ); + if (route === undefined) throw new Error("Route not found"); + + const response: IHttpResponse = await HttpMigrateRouteFetcher.propagate({ + connection, + route, + parameters: ["three", "two", "one"], + }); + TestValidator.equals("status")(response.status)(400); +}; diff --git a/test/features/migrate/test_http_migrate_fetch_query.ts b/test/features/migrate/test_http_migrate_fetch_query.ts new file mode 100644 index 0000000..ccc58d7 --- /dev/null +++ b/test/features/migrate/test_http_migrate_fetch_query.ts @@ -0,0 +1,34 @@ +import { + HttpMigrateRouteFetcher, + IHttpMigrateApplication, + IHttpMigrateRoute, +} from "@samchon/openapi"; +import { OpenApi } from "@samchon/openapi/lib/OpenApi"; +import { IHttpConnection } from "@samchon/openapi/lib/structures/IHttpConnection"; + +import swagger from "../../swagger.json"; + +export const test_http_migrate_fetch_query = async ( + connection: IHttpConnection, +): Promise => { + const document: OpenApi.IDocument = OpenApi.convert(swagger as any); + const app: IHttpMigrateApplication = OpenApi.migrate(document); + const route: IHttpMigrateRoute | undefined = app.routes.find( + (r) => r.path === "/{a}/{b}/{c}/query" && r.method === "get", + ); + if (route === undefined) throw new Error("Route not found"); + + await HttpMigrateRouteFetcher.request({ + connection, + route, + parameters: { + a: "string", + b: 123, + c: true, + }, + query: { + summary: "some summary", + thumbnail: "https://some.url", + }, + }); +}; diff --git a/test/features/migrate/test_http_migrate_fetch_query_and_body.ts b/test/features/migrate/test_http_migrate_fetch_query_and_body.ts new file mode 100644 index 0000000..3ced63d --- /dev/null +++ b/test/features/migrate/test_http_migrate_fetch_query_and_body.ts @@ -0,0 +1,39 @@ +import { + HttpMigrateRouteFetcher, + IHttpMigrateApplication, + IHttpMigrateRoute, +} from "@samchon/openapi"; +import { OpenApi } from "@samchon/openapi/lib/OpenApi"; +import { IHttpConnection } from "@samchon/openapi/lib/structures/IHttpConnection"; + +import swagger from "../../swagger.json"; + +export const test_http_migrate_fetch_query_and_body = async ( + connection: IHttpConnection, +): Promise => { + const document: OpenApi.IDocument = OpenApi.convert(swagger as any); + const app: IHttpMigrateApplication = OpenApi.migrate(document); + const route: IHttpMigrateRoute | undefined = app.routes.find( + (r) => r.path === "/{a}/{b}/{c}/query/body" && r.method === "post", + ); + if (route === undefined) throw new Error("Route not found"); + + await HttpMigrateRouteFetcher.request({ + connection, + route, + parameters: { + a: "string", + b: 123, + c: true, + }, + query: { + summary: "some summary", + thumbnail: "https://some.url", + }, + body: { + title: "some title", + body: "some body", + draft: false, + }, + }); +}; diff --git a/test/features/migrate/test_document_migrate_route_comment.ts b/test/features/migrate/test_http_migrate_route_comment.ts similarity index 78% rename from test/features/migrate/test_document_migrate_route_comment.ts rename to test/features/migrate/test_http_migrate_route_comment.ts index bb15eaf..0c3951f 100644 --- a/test/features/migrate/test_document_migrate_route_comment.ts +++ b/test/features/migrate/test_http_migrate_route_comment.ts @@ -1,18 +1,22 @@ import { TestValidator } from "@nestia/e2e"; -import { IMigrateDocument, IMigrateRoute, OpenApi } from "@samchon/openapi"; +import { + IHttpMigrateApplication, + IHttpMigrateRoute, + OpenApi, +} from "@samchon/openapi"; import fs from "fs"; -export const test_document_migrate_route_comment = async (): Promise => { +export const test_http_migrate_route_comment = async (): Promise => { const swagger: OpenApi.IDocument = OpenApi.convert( JSON.parse( await fs.promises.readFile( - `${__dirname}/../../../examples/v3.1/shopping.json`, + `${__dirname}/../../../../examples/v3.1/shopping.json`, "utf8", ), ), ); - const migrate: IMigrateDocument = OpenApi.migrate(swagger); - const route: IMigrateRoute | undefined = migrate.routes.find( + const migrate: IHttpMigrateApplication = OpenApi.migrate(swagger); + const route: IHttpMigrateRoute | undefined = migrate.routes.find( (r) => r.path === "/shoppings/sellers/sales/{id}" && r.method === "put", ); TestValidator.equals("comment")(route?.comment())(EXPECTED); diff --git a/test/features/migrate/test_document_migrate_v20.ts b/test/features/migrate/test_http_migrate_v20.ts similarity index 59% rename from test/features/migrate/test_document_migrate_v20.ts rename to test/features/migrate/test_http_migrate_v20.ts index 8a6b083..a39d91e 100644 --- a/test/features/migrate/test_document_migrate_v20.ts +++ b/test/features/migrate/test_http_migrate_v20.ts @@ -1,16 +1,16 @@ -import { IMigrateDocument, OpenApi, SwaggerV2 } from "@samchon/openapi"; +import { IHttpMigrateApplication, OpenApi, SwaggerV2 } from "@samchon/openapi"; import fs from "fs"; import typia from "typia"; -export const test_document_migrate_v20 = async (): Promise => { - const path: string = `${__dirname}/../../../examples/v2.0`; +export const test_http_migrate_v20 = async (): Promise => { + const path: string = `${__dirname}/../../../../examples/v2.0`; for (const file of await fs.promises.readdir(path)) { if (file.endsWith(".json") === false) continue; const swagger: SwaggerV2.IDocument = typia.assert( JSON.parse(await fs.promises.readFile(`${path}/${file}`, "utf8")), ); const openapi: OpenApi.IDocument = OpenApi.convert(swagger); - const migrate: IMigrateDocument = OpenApi.migrate(openapi); + const migrate: IHttpMigrateApplication = OpenApi.migrate(openapi); typia.assert(migrate); } }; diff --git a/test/features/migrate/test_document_migrate_v30.ts b/test/features/migrate/test_http_migrate_v30.ts similarity index 59% rename from test/features/migrate/test_document_migrate_v30.ts rename to test/features/migrate/test_http_migrate_v30.ts index de3a2e6..5b90a56 100644 --- a/test/features/migrate/test_document_migrate_v30.ts +++ b/test/features/migrate/test_http_migrate_v30.ts @@ -1,16 +1,16 @@ -import { IMigrateDocument, OpenApi, OpenApiV3 } from "@samchon/openapi"; +import { IHttpMigrateApplication, OpenApi, OpenApiV3 } from "@samchon/openapi"; import fs from "fs"; import typia from "typia"; -export const test_document_migrate_v30 = async (): Promise => { - const path: string = `${__dirname}/../../../examples/v3.0`; +export const test_http_migrate_v30 = async (): Promise => { + const path: string = `${__dirname}/../../../../examples/v3.0`; for (const file of await fs.promises.readdir(path)) { if (file.endsWith(".json") === false) continue; const swagger: OpenApiV3.IDocument = typia.assert( JSON.parse(await fs.promises.readFile(`${path}/${file}`, "utf8")), ); const openapi: OpenApi.IDocument = OpenApi.convert(swagger); - const migrate: IMigrateDocument = OpenApi.migrate(openapi); + const migrate: IHttpMigrateApplication = OpenApi.migrate(openapi); typia.assert(migrate); } }; diff --git a/test/features/migrate/test_document_migrate_v31.ts b/test/features/migrate/test_http_migrate_v31.ts similarity index 58% rename from test/features/migrate/test_document_migrate_v31.ts rename to test/features/migrate/test_http_migrate_v31.ts index fdd9d5c..6446ce0 100644 --- a/test/features/migrate/test_document_migrate_v31.ts +++ b/test/features/migrate/test_http_migrate_v31.ts @@ -1,16 +1,20 @@ -import { IMigrateDocument, OpenApi, OpenApiV3_1 } from "@samchon/openapi"; +import { + IHttpMigrateApplication, + OpenApi, + OpenApiV3_1, +} from "@samchon/openapi"; import fs from "fs"; import typia from "typia"; -export const test_document_migrate_v31 = async (): Promise => { - const path: string = `${__dirname}/../../../examples/v3.1`; +export const test_http_migrate_v31 = async (): Promise => { + const path: string = `${__dirname}/../../../../examples/v3.1`; for (const file of await fs.promises.readdir(path)) { if (file.endsWith(".json") === false) continue; const swagger: OpenApiV3_1.IDocument = typia.assert( JSON.parse(await fs.promises.readFile(`${path}/${file}`, "utf8")), ); const openapi: OpenApi.IDocument = OpenApi.convert(swagger); - const migrate: IMigrateDocument = OpenApi.migrate(openapi); + const migrate: IHttpMigrateApplication = OpenApi.migrate(openapi); typia.assert(migrate); } }; diff --git a/test/features/openapi/test_document_convert_v20.ts b/test/features/openapi/test_document_convert_v20.ts index 1002ad7..526bb3a 100644 --- a/test/features/openapi/test_document_convert_v20.ts +++ b/test/features/openapi/test_document_convert_v20.ts @@ -3,7 +3,7 @@ import fs from "fs"; import typia from "typia"; export const test_document_convert_v20 = async (): Promise => { - const path: string = `${__dirname}/../../../examples/v2.0`; + const path: string = `${__dirname}/../../../../examples/v2.0`; for (const file of await fs.promises.readdir(path)) { if (file.endsWith(".json") === false) continue; const swagger: SwaggerV2.IDocument = typia.assert( diff --git a/test/features/openapi/test_document_convert_v30.ts b/test/features/openapi/test_document_convert_v30.ts index a6fd498..ec91471 100644 --- a/test/features/openapi/test_document_convert_v30.ts +++ b/test/features/openapi/test_document_convert_v30.ts @@ -3,7 +3,7 @@ import fs from "fs"; import typia from "typia"; export const test_document_convert_v30 = async (): Promise => { - const path: string = `${__dirname}/../../../examples/v3.0`; + const path: string = `${__dirname}/../../../../examples/v3.0`; for (const file of await fs.promises.readdir(path)) { if (file.endsWith(".json") === false) continue; const swagger: OpenApiV3.IDocument = typia.assert( diff --git a/test/features/openapi/test_document_convert_v31.ts b/test/features/openapi/test_document_convert_v31.ts index a22419a..4a42a2d 100644 --- a/test/features/openapi/test_document_convert_v31.ts +++ b/test/features/openapi/test_document_convert_v31.ts @@ -3,7 +3,7 @@ import fs from "fs"; import typia from "typia"; export const test_document_convert_v31 = async (): Promise => { - const path: string = `${__dirname}/../../../examples/v3.1`; + const path: string = `${__dirname}/../../../../examples/v3.1`; for (const file of await fs.promises.readdir(path)) { if (file.endsWith(".json") === false) continue; const swagger: OpenApiV3_1.IDocument = typia.assert( diff --git a/test/features/openapi/test_document_downgrade_v20.ts b/test/features/openapi/test_document_downgrade_v20.ts index beef3a4..701bb0c 100644 --- a/test/features/openapi/test_document_downgrade_v20.ts +++ b/test/features/openapi/test_document_downgrade_v20.ts @@ -3,7 +3,7 @@ import fs from "fs"; import typia from "typia"; export const test_document_downgrade_v20 = async (): Promise => { - const path: string = `${__dirname}/../../../examples/v3.1`; + const path: string = `${__dirname}/../../../../examples/v3.1`; for (const directory of await fs.promises.readdir(path)) { const stats: fs.Stats = await fs.promises.lstat(`${path}/${directory}`); if (stats.isDirectory() === false) continue; diff --git a/test/features/openapi/test_document_downgrade_v30.ts b/test/features/openapi/test_document_downgrade_v30.ts index b5d078e..90dd404 100644 --- a/test/features/openapi/test_document_downgrade_v30.ts +++ b/test/features/openapi/test_document_downgrade_v30.ts @@ -3,7 +3,7 @@ import fs from "fs"; import typia from "typia"; export const test_document_downgrade_v30 = async (): Promise => { - const path: string = `${__dirname}/../../../examples/v3.1`; + const path: string = `${__dirname}/../../../../examples/v3.1`; for (const directory of await fs.promises.readdir(path)) { const stats: fs.Stats = await fs.promises.lstat(`${path}/${directory}`); if (stats.isDirectory() === false) continue; diff --git a/test/index.ts b/test/index.ts index b0a0860..3f8ae72 100644 --- a/test/index.ts +++ b/test/index.ts @@ -2,11 +2,16 @@ import { DynamicExecutor } from "@nestia/e2e"; import { NestFactory } from "@nestjs/core"; import chalk from "chalk"; +import { AppFilter } from "./controllers/AppFilter"; import { AppModule } from "./controllers/AppModule"; +const EXTENSION = __filename.substr(-2); +if (EXTENSION === "js") require("source-map-support").install(); + const main = async (): Promise => { // PREPARE SERVER const app = await NestFactory.create(AppModule, { logger: false }); + app.useGlobalFilters(new AppFilter(app.getHttpAdapter())); await app.listen(3_000); // DO TEST @@ -15,7 +20,7 @@ const main = async (): Promise => { location: __dirname + "/features", parameters: () => [ { - connection: { host: `http://localhost:3000` }, + host: `http://localhost:3000`, }, ], onComplete: (exec) => { diff --git a/test/nestia.config.ts b/test/nestia.config.ts new file mode 100644 index 0000000..7d06443 --- /dev/null +++ b/test/nestia.config.ts @@ -0,0 +1,13 @@ +import { INestiaConfig } from "@nestia/sdk"; +import { NestFactory } from "@nestjs/core"; + +import { AppModule } from "./controllers/AppModule"; + +export const NESTIA_CONFIG: INestiaConfig = { + input: () => NestFactory.create(AppModule), + swagger: { + output: "./swagger.json", + beautify: true, + }, +}; +export default NESTIA_CONFIG; diff --git a/test/swagger.json b/test/swagger.json new file mode 100644 index 0000000..ea3ada3 --- /dev/null +++ b/test/swagger.json @@ -0,0 +1,428 @@ +{ + "openapi": "3.1.0", + "servers": [ + { + "url": "https://github.com/samchon/nestia", + "description": "insert your server url" + } + ], + "info": { + "version": "0.5.0-dev.20240824", + "title": "@samchon/openapi", + "description": "OpenAPI definitions and converters for 'typia' and 'nestia'.", + "license": { + "name": "MIT" + } + }, + "paths": { + "/{a}/{b}/{c}/parameters": { + "get": { + "tags": [], + "parameters": [ + { + "name": "a", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + }, + { + "name": "b", + "in": "path", + "schema": { + "type": "number" + }, + "required": true + }, + { + "name": "c", + "in": "path", + "schema": { + "type": "boolean" + }, + "required": true + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": {} + } + } + } + } + } + } + }, + "/{a}/{b}/{c}/query": { + "get": { + "tags": [], + "parameters": [ + { + "name": "a", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + }, + { + "name": "b", + "in": "path", + "schema": { + "type": "number" + }, + "required": true + }, + { + "name": "c", + "in": "path", + "schema": { + "type": "boolean" + }, + "required": true + }, + { + "name": "flag", + "in": "query", + "schema": { + "type": "boolean" + }, + "required": true + }, + { + "name": "value", + "in": "query", + "schema": { + "type": "number" + }, + "required": true + }, + { + "name": "text", + "in": "query", + "schema": { + "type": "string" + }, + "required": true + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": {} + } + } + } + } + } + } + }, + "/{a}/{b}/{c}/body": { + "post": { + "tags": [], + "parameters": [ + { + "name": "a", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + }, + { + "name": "b", + "in": "path", + "schema": { + "type": "number" + }, + "required": true + }, + { + "name": "c", + "in": "path", + "schema": { + "type": "boolean" + }, + "required": true + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "flag": { + "type": "boolean" + }, + "value": { + "type": "number" + }, + "text": { + "type": "string" + } + }, + "required": [ + "flag", + "value", + "text" + ] + } + } + }, + "required": true + }, + "responses": { + "201": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": {} + } + } + } + } + } + } + }, + "/{a}/{b}/{c}/query/body": { + "post": { + "tags": [], + "parameters": [ + { + "name": "a", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + }, + { + "name": "b", + "in": "path", + "schema": { + "type": "number" + }, + "required": true + }, + { + "name": "c", + "in": "path", + "schema": { + "type": "boolean" + }, + "required": true + }, + { + "name": "name", + "in": "query", + "schema": { + "type": "string" + }, + "required": true + }, + { + "name": "reference", + "in": "query", + "schema": { + "type": "string" + }, + "required": true + }, + { + "name": "memo", + "in": "query", + "schema": { + "type": "string" + }, + "required": true + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "flag": { + "type": "boolean" + }, + "value": { + "type": "number" + }, + "text": { + "type": "string" + } + }, + "required": [ + "flag", + "value", + "text" + ] + } + } + }, + "required": true + }, + "responses": { + "201": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "query": { + "type": "object", + "properties": { + "memo": { + "type": "string" + } + }, + "required": [ + "memo" + ] + } + }, + "required": [ + "query" + ] + } + } + } + } + } + } + }, + "/{a}/{b}/{c}/multipart": { + "post": { + "tags": [], + "parameters": [ + { + "name": "a", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + }, + { + "name": "b", + "in": "path", + "schema": { + "type": "number" + }, + "required": true + }, + { + "name": "c", + "in": "path", + "schema": { + "type": "boolean" + }, + "required": true + }, + { + "name": "flag", + "in": "query", + "schema": { + "type": "boolean" + }, + "required": true + }, + { + "name": "value", + "in": "query", + "schema": { + "type": "number" + }, + "required": true + }, + { + "name": "text", + "in": "query", + "schema": { + "type": "string" + }, + "required": true + } + ], + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "reference": { + "type": "string" + }, + "file": { + "type": "string", + "format": "binary" + } + }, + "required": [ + "name", + "reference", + "file" + ] + } + } + }, + "required": true + }, + "responses": { + "201": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "body": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "reference": { + "type": "string" + }, + "file": { + "type": "string" + } + }, + "required": [ + "name", + "reference", + "file" + ] + } + }, + "required": [ + "body" + ] + } + } + } + } + } + } + } + }, + "components": { + "schemas": {} + }, + "tags": [], + "x-samchon-emended": true +} \ No newline at end of file diff --git a/test/tsconfig.json b/test/tsconfig.json index e4385c0..1bfade9 100644 --- a/test/tsconfig.json +++ b/test/tsconfig.json @@ -1,16 +1,19 @@ { "extends": "../tsconfig.json", "compilerOptions": { + "target": "ESNext", "outDir": "../bin", "emitDecoratorMetadata": true, "experimentalDecorators": true, + "resolveJsonModule": true, "paths": { "@samchon/openapi": ["../src/index.ts"], "@samchon/openapi/lib/*": ["../src/*"], }, "plugins": [ - { "transform": "typia/lib/transform" }, { "transform": "typescript-transform-paths" }, + { "transform": "typia/lib/transform" }, + { "transform": "@nestia/core/lib/transform" }, ] }, "include": ["../src", "../test"] diff --git a/tsconfig.json b/tsconfig.json index 5711531..c7e2cd1 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -11,7 +11,7 @@ // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ /* Language and Environment */ - "target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ + "target": "ES5", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ "lib": [ "DOM", "ES2020", From 4805d4b320f27f632b9a330b36d54d2a04456942 Mon Sep 17 00:00:00 2001 From: Jeongho Nam Date: Mon, 2 Sep 2024 11:34:33 +0900 Subject: [PATCH 05/16] `downlevelIteration` in `tsconfig.json` --- tsconfig.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tsconfig.json b/tsconfig.json index c7e2cd1..26ffa38 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -63,7 +63,7 @@ // "noEmit": true, /* Disable emitting files from a compilation. */ // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ - // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ + "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ From 1ce5c7bd86f5ff4367949286649174d5b92c6985 Mon Sep 17 00:00:00 2001 From: Jeongho Nam Date: Tue, 3 Sep 2024 04:16:09 +0900 Subject: [PATCH 06/16] Testing LLM functions --- .gitignore | 3 +- src/HttpLanguageModel.ts | 76 ++++ src/HttpMigration.ts | 31 ++ src/OpenApi.ts | 37 +- src/converters/LlmComposer.ts | 400 ++++++++++-------- src/converters/LlmMerger.ts | 90 ++++ src/http/HttpMigrateRouteFetcher.ts | 10 +- src/index.ts | 10 +- src/structures/ILlmFunction.ts | 36 ++ src/structures/ILlmSchema.ts | 142 +------ test/controllers/AppController.ts | 58 +-- test/controllers/AppFilter.ts | 5 +- .../llm/test_http_llm_application_keyword.ts | 36 ++ .../test_http_llm_application_positional.ts | 25 ++ .../features/llm/test_llm_merge_parameters.ts | 42 ++ test/features/llm/test_llm_merge_value.ts | 62 +++ test/features/llm/test_llm_schema_object.ts | 55 +++ test/features/llm/test_llm_schema_oneof.ts | 77 ++++ .../llm/test_llm_schema_separate_array.ts | 43 ++ .../llm/test_llm_schema_separate_nested.ts | 59 +++ .../llm/test_llm_schema_separate_object.ts | 43 ++ .../llm/test_llm_schema_separate_string.ts | 17 + .../migrate/test_http_migrate_fetch_body.ts | 14 +- ...t_http_migrate_fetch_keyword_parameters.ts | 17 +- .../test_http_migrate_fetch_multipart.ts | 17 +- ...ttp_migrate_fetch_positional_parameters.ts | 11 +- .../test_http_migrate_fetch_propagate.ts | 13 +- .../migrate/test_http_migrate_fetch_query.ts | 18 +- .../test_http_migrate_fetch_query_and_body.ts | 19 +- .../test_http_migrate_route_comment.ts | 3 +- .../features/migrate/test_http_migrate_v20.ts | 9 +- .../features/migrate/test_http_migrate_v30.ts | 9 +- .../features/migrate/test_http_migrate_v31.ts | 3 +- test/swagger.json | 230 +++++----- tsconfig.json | 4 +- 35 files changed, 1150 insertions(+), 574 deletions(-) create mode 100644 src/HttpLanguageModel.ts create mode 100644 src/HttpMigration.ts create mode 100644 src/converters/LlmMerger.ts create mode 100644 test/features/llm/test_http_llm_application_keyword.ts create mode 100644 test/features/llm/test_http_llm_application_positional.ts create mode 100644 test/features/llm/test_llm_merge_parameters.ts create mode 100644 test/features/llm/test_llm_merge_value.ts create mode 100644 test/features/llm/test_llm_schema_object.ts create mode 100644 test/features/llm/test_llm_schema_oneof.ts create mode 100644 test/features/llm/test_llm_schema_separate_array.ts create mode 100644 test/features/llm/test_llm_schema_separate_nested.ts create mode 100644 test/features/llm/test_llm_schema_separate_object.ts create mode 100644 test/features/llm/test_llm_schema_separate_string.ts diff --git a/.gitignore b/.gitignore index 2c3b7cb..f84e69a 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,5 @@ lib/ node_modules/ package-lock.json -pnpm-lock.yaml \ No newline at end of file +pnpm-lock.yaml +*.log \ No newline at end of file diff --git a/src/HttpLanguageModel.ts b/src/HttpLanguageModel.ts new file mode 100644 index 0000000..f8a240b --- /dev/null +++ b/src/HttpLanguageModel.ts @@ -0,0 +1,76 @@ +import { HttpMigration } from "./HttpMigration"; +import { OpenApi } from "./OpenApi"; +import { LlmComposer } from "./converters/LlmComposer"; +import { LlmMerger } from "./converters/LlmMerger"; +import { HttpLlmFunctionFetcher } from "./http/HttpLlmFunctionFetcher"; +import { IHttpConnection } from "./structures/IHttpConnection"; +import { IHttpLlmApplication } from "./structures/IHttpLlmApplication"; +import { IHttpLlmFunction } from "./structures/IHttpLlmFunction"; +import { IHttpMigrateApplication } from "./structures/IHttpMigrateApplication"; +import { IHttpResponse } from "./structures/IHttpResponse"; +import { ILlmSchema } from "./structures/ILlmSchema"; + +export namespace HttpLanguageModel { + export const application = < + Schema extends ILlmSchema, + Operation extends OpenApi.IOperation, + >( + document: + | OpenApi.IDocument + | IHttpMigrateApplication, + options?: Partial, + ): IHttpLlmApplication => { + // MIGRATE + if ((document as OpenApi.IDocument)["x-samchon-emended"] === true) + document = HttpMigration.application( + document as OpenApi.IDocument, + ); + return LlmComposer.compose( + document as IHttpMigrateApplication, + { + keyword: options?.keyword ?? false, + separate: options?.separate ?? null, + }, + ); + }; + + export const schema = (props: { + components: OpenApi.IComponents; + schema: OpenApi.IJsonSchema; + }): ILlmSchema | null => LlmComposer.schema(props); + + export interface IExecutionProps { + /** + * Document of the OpenAI function call schemas. + */ + document: IHttpLlmApplication; + + /** + * Procedure schema to call. + */ + procedure: IHttpLlmFunction; + + /** + * Connection info to the server. + */ + connection: IHttpConnection; + + /** + * Arguments for the function call. + */ + arguments: any[]; + } + export const execute = (props: IExecutionProps): Promise => + HttpLlmFunctionFetcher.execute(props); + + export const propagate = (props: IExecutionProps): Promise => + HttpLlmFunctionFetcher.propagate(props); + + export interface IMergeProps { + function: IHttpLlmFunction; + llm: unknown[]; + human: unknown[]; + } + export const merge = (props: IMergeProps): unknown[] => + LlmMerger.parameters(props); +} diff --git a/src/HttpMigration.ts b/src/HttpMigration.ts new file mode 100644 index 0000000..cd91fe1 --- /dev/null +++ b/src/HttpMigration.ts @@ -0,0 +1,31 @@ +import { OpenApi } from "./OpenApi"; +import { MigrateConverter } from "./converters/MigrateConverter"; +import { HttpMigrateRouteFetcher } from "./http/HttpMigrateRouteFetcher"; +import { IHttpConnection } from "./structures/IHttpConnection"; +import { IHttpMigrateApplication } from "./structures/IHttpMigrateApplication"; +import { IHttpMigrateRoute } from "./structures/IHttpMigrateRoute"; +import { IHttpResponse } from "./structures/IHttpResponse"; + +export namespace HttpMigration { + export const application = < + Schema extends OpenApi.IJsonSchema = OpenApi.IJsonSchema, + Operation extends OpenApi.IOperation = OpenApi.IOperation, + >( + document: OpenApi.IDocument, + ): IHttpMigrateApplication => + MigrateConverter.convert(document); + + export interface IProps { + connection: IHttpConnection; + route: IHttpMigrateRoute; + parameters: + | Array + | Record; + query?: object | undefined; + body?: object | undefined; + } + export const request = (props: IProps): Promise => + HttpMigrateRouteFetcher.request(props); + export const propagate = (props: IProps): Promise => + HttpMigrateRouteFetcher.propagate(props); +} diff --git a/src/OpenApi.ts b/src/OpenApi.ts index 59966eb..f9ffae3 100644 --- a/src/OpenApi.ts +++ b/src/OpenApi.ts @@ -1,15 +1,11 @@ import { OpenApiV3 } from "./OpenApiV3"; import { OpenApiV3_1 } from "./OpenApiV3_1"; import { SwaggerV2 } from "./SwaggerV2"; -import { LlmComposer } from "./converters/LlmComposer"; -import { MigrateConverter } from "./converters/MigrateConverter"; import { OpenApiV3Converter } from "./converters/OpenApiV3Converter"; import { OpenApiV3Downgrader } from "./converters/OpenApiV3Downgrader"; import { OpenApiV3_1Converter } from "./converters/OpenApiV3_1Converter"; import { SwaggerV2Converter } from "./converters/SwaggerV2Converter"; import { SwaggerV2Downgrader } from "./converters/SwaggerV2Downgrader"; -import { IHttpLlmApplication } from "./structures/IHttpLlmApplication"; -import { IHttpMigrateApplication } from "./structures/IHttpMigrateApplication"; /** * Emended OpenAPI v3.1 definition used by `typia` and `nestia`. @@ -136,37 +132,6 @@ export namespace OpenApi { throw new TypeError("Unrecognized Swagger/OpenAPI version."); } - /** - * Convert to migrate document. - * - * Convert the given OpenAPI document to {@link IHttpMigrateApplication}, that is - * useful for OpenAPI generator library which makes RPC (Remote Procedure Call) - * functions for the Restful API operation. - * - * @param document OpenAPI document to migrate - * @returns Migrated document - */ - export function migrate< - Schema extends IJsonSchema = IJsonSchema, - Operation extends IOperation = IOperation, - >( - document: IDocument, - ): IHttpMigrateApplication { - return MigrateConverter.convert(document); - } - - export function llm( - document: OpenApi.IDocument | IHttpMigrateApplication, - options?: IHttpLlmApplication.IOptions, - ): IHttpLlmApplication { - if ((document as OpenApi.IDocument)["x-samchon-emended"] !== true) - document = migrate(document as OpenApi.IDocument); - return LlmComposer.compose(document as IHttpMigrateApplication, { - keyword: options?.keyword ?? false, - separate: options?.separate ?? null, - }); - } - /* ----------------------------------------------------------- PATH ITEMS ----------------------------------------------------------- */ @@ -1066,7 +1031,7 @@ export namespace OpenApi { /** * List of the union types. */ - oneOf: Exclude[]; + oneOf: Exclude>[]; /** * Discriminator info of the union type. diff --git a/src/converters/LlmComposer.ts b/src/converters/LlmComposer.ts index 4499b6c..92872e3 100644 --- a/src/converters/LlmComposer.ts +++ b/src/converters/LlmComposer.ts @@ -47,18 +47,20 @@ export namespace LlmComposer { }; }; - export const schema = ( - components: OpenApi.IComponents, - schema: OpenApi.IJsonSchema, - ): ILlmSchema | null => { - const escaped: OpenApi.IJsonSchema | null = escapeReference(components)( - new Set(), - )(schema); - if (escaped === null) return null; + export const schema = (props: { + components: OpenApi.IComponents; + schema: OpenApi.IJsonSchema; + }): ILlmSchema | null => { + const resolved: OpenApi.IJsonSchema | null = escape({ + components: props.components, + visited: new Set(), + input: props.schema, + }); + if (resolved === null) return null; const downgraded: ILlmSchema = OpenApiV3Downgrader.downgradeSchema({ original: {}, downgraded: {}, - })(escaped) as ILlmSchema; + })(resolved) as ILlmSchema; LlmTypeChecker.visit(downgraded, (schema) => { if ( LlmTypeChecker.isOneOf(schema) && @@ -68,191 +70,215 @@ export namespace LlmComposer { }); return downgraded; }; +} - const composeFunction = - (options: IHttpLlmApplication.IOptions) => - (components: OpenApi.IComponents) => - (route: IHttpMigrateRoute): IHttpLlmFunction | null => { - // CAST SCHEMA TYPES - const cast = (s: OpenApi.IJsonSchema) => schema(components, s); - const output: ILlmSchema | null | undefined = - route.success && route.success ? cast(route.success.schema) : undefined; - if (output === null) return null; - const properties: [string, ILlmSchema | null][] = [ - ...route.parameters.map((p) => ({ - key: p.key, - schema: { - ...p.schema, - title: p.parameter().title ?? p.schema.title, - description: p.parameter().description ?? p.schema.description, - }, - })), - ...(route.query - ? [ - { - key: route.query.key, - schema: { - ...route.query.schema, - title: route.query.title() ?? route.query.schema.title, - description: - route.query.description() ?? route.query.schema.description, - }, - }, - ] - : []), - ...(route.body - ? [ - { - key: route.body.key, - schema: { - ...route.body.schema, - description: - route.body.description() ?? route.body.schema.description, - }, +const composeFunction = + (options: IHttpLlmApplication.IOptions) => + (components: OpenApi.IComponents) => + (route: IHttpMigrateRoute): IHttpLlmFunction | null => { + // CAST SCHEMA TYPES + const cast = (s: OpenApi.IJsonSchema) => + LlmComposer.schema({ + components, + schema: s, + }); + const output: ILlmSchema | null | undefined = + route.success && route.success ? cast(route.success.schema) : undefined; + if (output === null) return null; + const properties: [string, ILlmSchema | null][] = [ + ...route.parameters.map((p) => ({ + key: p.key, + schema: { + ...p.schema, + title: p.parameter().title ?? p.schema.title, + description: p.parameter().description ?? p.schema.description, + }, + })), + ...(route.query + ? [ + { + key: route.query.key, + schema: { + ...route.query.schema, + title: route.query.title() ?? route.query.schema.title, + description: + route.query.description() ?? route.query.schema.description, }, - ] - : []), - ].map((o) => [o.key, cast(o.schema)]); - if (properties.some(([_k, v]) => v === null)) return null; - - // COMPOSE PARAMETERS - const parameters: ILlmSchema[] = options.keyword + }, + ] + : []), + ...(route.body ? [ { - type: "object", - properties: Object.fromEntries( - properties as [string, ILlmSchema][], - ), + key: route.body.key, + schema: { + ...route.body.schema, + description: + route.body.description() ?? route.body.schema.description, + }, }, ] - : properties.map(([_k, v]) => v!); - const operation: OpenApi.IOperation = route.operation(); + : []), + ].map((o) => [o.key, cast(o.schema)]); + if (properties.some(([_k, v]) => v === null)) return null; + + // COMPOSE PARAMETERS + const parameters: ILlmSchema[] = options.keyword + ? [ + { + type: "object", + properties: Object.fromEntries( + properties as [string, ILlmSchema][], + ), + }, + ] + : properties.map(([_k, v]) => v!); + const operation: OpenApi.IOperation = route.operation(); - // FINALIZATION - return { - method: route.method as "get", - path: route.path, - name: route.accessor.join("_"), - strict: true, - parameters, - separated: options.separate - ? LlmSchemaSeparator.parameters({ - parameters, - predicator: options.separate, - }) - : undefined, - output: output - ? (OpenApiV3Downgrader.downgradeSchema({ - original: {}, - downgraded: {}, - })(output) as ILlmSchema) - : undefined, - description: (() => { - if (operation.summary && operation.description) { - return operation.description.startsWith(operation.summary) - ? operation.description - : [ - operation.summary, - operation.summary.endsWith(".") ? "" : ".", - "\n\n", - operation.description, - ].join(""); - } - return operation.description ?? operation.summary; - })(), - route: () => route, - operation: () => operation, - }; + // FINALIZATION + return { + method: route.method as "get", + path: route.path, + name: route.accessor.join("_"), + strict: true, + parameters, + separated: options.separate + ? LlmSchemaSeparator.parameters({ + parameters, + predicator: options.separate, + }) + : undefined, + output: output + ? (OpenApiV3Downgrader.downgradeSchema({ + original: {}, + downgraded: {}, + })(output) as ILlmSchema) + : undefined, + description: (() => { + if (operation.summary && operation.description) { + return operation.description.startsWith(operation.summary) + ? operation.description + : [ + operation.summary, + operation.summary.endsWith(".") ? "" : ".", + "\n\n", + operation.description, + ].join(""); + } + return operation.description ?? operation.summary; + })(), + route: () => route, + operation: () => operation, }; + }; - const escapeReference = - (components: OpenApi.IComponents) => - (visited: Set) => - (input: OpenApi.IJsonSchema): OpenApi.IJsonSchema | null => { - if (OpenApiTypeChecker.isReference(input)) { - // REFERENCE - const name: string = input.$ref.split("#/components/schemas/")[1]; - const target: OpenApi.IJsonSchema | undefined = - components.schemas?.[name]; - if (!target) return null; - else if (visited.has(name)) return null; - return escapeReference(components)(new Set([...visited, name]))(target); - } else if (OpenApiTypeChecker.isOneOf(input)) { - // ONE-OF - const oneOf: Array = input.oneOf.map( - (schema) => escapeReference(components)(visited)(schema)!, - ); - if (oneOf.some((v) => v === null)) return null; - return { - ...input, - oneOf: oneOf as OpenApi.IJsonSchema[], - }; - } else if (OpenApiTypeChecker.isObject(input)) { - // OBJECT - const properties: - | Array<[string, OpenApi.IJsonSchema | null]> - | undefined = input.properties - ? Object.entries(input.properties).map( - ([key, value]) => - [key, escapeReference(components)(visited)(value)] as const, - ) - : undefined; - const additionalProperties: - | OpenApi.IJsonSchema - | null - | boolean - | undefined = input.additionalProperties - ? typeof input.additionalProperties === "object" && - input.additionalProperties !== null - ? escapeReference(components)(visited)(input.additionalProperties) - : input.additionalProperties - : undefined; - if (properties && properties.some(([_k, v]) => v === null)) return null; - else if (additionalProperties === null) return null; - return { - ...input, - properties: properties - ? Object.fromEntries( - properties.filter(([_k, v]) => !!v) as Array< - [string, OpenApi.IJsonSchema] - >, - ) - : undefined, - additionalProperties, - }; - } else if (OpenApiTypeChecker.isTuple(input)) { - // TUPLE - const prefixItems: Array = - input.prefixItems.map((schema) => - escapeReference(components)(visited)(schema), - ); - const additionalItems: - | OpenApi.IJsonSchema - | null - | boolean - | undefined = - typeof input.additionalItems === "object" && - input.additionalItems !== null - ? escapeReference(components)(visited)(input.additionalItems) - : input.additionalItems; - if (prefixItems.some((v) => v === null)) return null; - else if (additionalItems === null) return null; - return { - ...input, - prefixItems: prefixItems as OpenApi.IJsonSchema[], - additionalItems, - }; - } else if (OpenApiTypeChecker.isArray(input)) { - // ARRAY - const items: OpenApi.IJsonSchema | null = escapeReference(components)( - visited, - )(input.items); - if (items === null) return null; - return { - ...input, - items, - }; - } - return input; +const escape = (props: { + components: OpenApi.IComponents; + visited: Set; + input: OpenApi.IJsonSchema; +}): OpenApi.IJsonSchema | null => { + if (OpenApiTypeChecker.isReference(props.input)) { + // REFERENCE + const name: string = props.input.$ref.split("#/components/schemas/")[1]; + const target: OpenApi.IJsonSchema | undefined = + props.components.schemas?.[name]; + if (!target) return null; + else if (props.visited.has(name)) return null; + return escape({ + components: props.components, + visited: new Set([...props.visited, name]), + input: target, + }); + } else if (OpenApiTypeChecker.isOneOf(props.input)) { + // ONE-OF + const oneOf: Array = props.input.oneOf.map( + (schema) => + escape({ + ...props, + input: schema, + })!, + ); + if (oneOf.some((v) => v === null)) return null; + return { + ...props.input, + oneOf: oneOf as OpenApi.IJsonSchema[], }; -} + } else if (OpenApiTypeChecker.isObject(props.input)) { + // OBJECT + const properties: Array<[string, OpenApi.IJsonSchema | null]> | undefined = + props.input.properties + ? Object.entries(props.input.properties).map( + ([key, value]) => + [ + key, + escape({ + ...props, + input: value, + }), + ] as const, + ) + : undefined; + const additionalProperties: + | OpenApi.IJsonSchema + | null + | boolean + | undefined = props.input.additionalProperties + ? typeof props.input.additionalProperties === "object" && + props.input.additionalProperties !== null + ? escape({ + ...props, + input: props.input.additionalProperties, + }) + : props.input.additionalProperties + : undefined; + if (properties && properties.some(([_k, v]) => v === null)) return null; + else if (additionalProperties === null) return null; + return { + ...props.input, + properties: properties + ? Object.fromEntries( + properties.filter(([_k, v]) => !!v) as Array< + [string, OpenApi.IJsonSchema] + >, + ) + : undefined, + additionalProperties, + }; + } else if (OpenApiTypeChecker.isTuple(props.input)) { + // TUPLE + const prefixItems: Array = + props.input.prefixItems.map((schema) => + escape({ + ...props, + input: schema, + }), + ); + const additionalItems: OpenApi.IJsonSchema | null | boolean | undefined = + typeof props.input.additionalItems === "object" && + props.input.additionalItems !== null + ? escape({ + ...props, + input: props.input.additionalItems, + }) + : props.input.additionalItems; + if (prefixItems.some((v) => v === null)) return null; + else if (additionalItems === null) return null; + return { + ...props.input, + prefixItems: prefixItems as OpenApi.IJsonSchema[], + additionalItems, + }; + } else if (OpenApiTypeChecker.isArray(props.input)) { + // ARRAY + const items: OpenApi.IJsonSchema | null = escape({ + ...props, + input: props.input.items, + }); + if (items === null) return null; + return { + ...props.input, + items, + }; + } + return props.input; +}; diff --git a/src/converters/LlmMerger.ts b/src/converters/LlmMerger.ts new file mode 100644 index 0000000..ea966d6 --- /dev/null +++ b/src/converters/LlmMerger.ts @@ -0,0 +1,90 @@ +import { IHttpLlmFunction } from "../structures/IHttpLlmFunction"; +import { ILlmFunction } from "../structures/ILlmFunction"; + +/** + * Data combiner for LLM function call. + * + * @author Samchon + */ +export namespace LlmMerger { + /** + * Properties of {@link parameters} function. + */ + export interface IProps { + /** + * Target function to call. + */ + function: ILlmFunction; + + /** + * Arguments composed by LLM (Large Language Model). + */ + llm: any[]; + + /** + * Arguments composed by human. + */ + human: any[]; + } + + /** + * Combine LLM and human arguments into one. + * + * When you composes {@link IOpenAiDocument} with + * {@link IOpenAiDocument.IOptions.separate} option, then the arguments of the + * target function would be separated into two parts; LLM (Large Language Model) + * and human. + * + * In that case, you can combine both LLM and human composed arguments into one + * by utilizing this {@link LlmMerger.parameters} function, referencing + * the target function metadata {@link IOpenAiFunction.separated}. + * + * @param props Properties to combine LLM and human arguments with metadata. + * @returns Combined arguments + */ + export const parameters = (props: IProps): unknown[] => { + const separated: IHttpLlmFunction.ISeparated | undefined = + props.function.separated; + if (separated === undefined) + throw new Error( + "Error on OpenAiDataComposer.parameters(): the function parameters are not separated.", + ); + return new Array(props.function.parameters.length).fill(0).map((_, i) => { + const llm: number = separated.llm.findIndex((p) => p.index === i); + const human: number = separated.human.findIndex((p) => p.index === i); + if (llm === -1 && human === -1) + throw new Error( + "Error on OpenAiDataComposer.parameters(): failed to gather separated arguments, because both LLM and human sides are all empty.", + ); + return value(props.llm[llm], props.human[human]); + }); + }; + + /** + * Combine two values into one. + * + * If both values are objects, then combines them in the properties level. + * + * Otherwise, returns the latter value if it's not null, otherwise the former value + * + * - `return (y ?? x)` + * + * @param x Value X + * @param y Value Y + * @returns Combined value + */ + export const value = (x: unknown, y: unknown): unknown => + typeof x === "object" && typeof y === "object" && x !== null && y !== null + ? combineObject(x, y) + : Array.isArray(x) && Array.isArray(y) + ? new Array(Math.max(x.length, y.length)) + .fill(0) + .map((_, i) => value(x[i], y[i])) + : y ?? x; + + const combineObject = (x: any, y: any): any => { + const output: any = { ...x }; + for (const [k, v] of Object.entries(y)) output[k] = value(x[k], v); + return output; + }; +} diff --git a/src/http/HttpMigrateRouteFetcher.ts b/src/http/HttpMigrateRouteFetcher.ts index d75a194..c2551cc 100644 --- a/src/http/HttpMigrateRouteFetcher.ts +++ b/src/http/HttpMigrateRouteFetcher.ts @@ -148,10 +148,12 @@ const getPath = ( props.route.parameters.forEach((p, i) => { path = path.replace( `:${p.key}`, - String( - (Array.isArray(props.parameters) - ? props.parameters[i] - : props.parameters[p.key]) ?? "null", + encodeURIComponent( + String( + (Array.isArray(props.parameters) + ? props.parameters[i] + : props.parameters[p.key]) ?? "null", + ), ), ); }); diff --git a/src/index.ts b/src/index.ts index 909dfed..ba5e544 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,16 +1,14 @@ // STRUCTURES +export * from "./structures/IHttpConnection"; export * from "./structures/IHttpLlmApplication"; export * from "./structures/IHttpLlmFunction"; export * from "./structures/IHttpMigrateRoute"; export * from "./structures/IHttpMigrateApplication"; +export * from "./structures/IHttpResponse"; export * from "./structures/ILlmSchema"; -// HTTP INTERACTION -export * from "./http/HttpError"; -export * from "./http/HttpLlmFunctionFetcher"; -export * from "./http/HttpMigrateRouteFetcher"; - // UTILS +export * from "./http/HttpError"; export * from "./utils/OpenApiTypeChecker"; export * from "./utils/LlmTypeChecker"; @@ -19,3 +17,5 @@ export * from "./OpenApi"; export * from "./SwaggerV2"; export * from "./OpenApiV3"; export * from "./OpenApiV3_1"; +export * from "./HttpLanguageModel"; +export * from "./HttpMigration"; diff --git a/src/structures/ILlmFunction.ts b/src/structures/ILlmFunction.ts index fa5cc31..7b13072 100644 --- a/src/structures/ILlmFunction.ts +++ b/src/structures/ILlmFunction.ts @@ -33,6 +33,11 @@ export interface ILlmFunction { */ parameters: Schema[]; + /** + * Collection of separated parameters. + */ + separated?: ILlmFunction.ISeparated; + /** * Expected return type. * @@ -54,3 +59,34 @@ export interface ILlmFunction { */ description?: string | undefined; } +export namespace ILlmFunction { + /** + * Collection of separated parameters. + */ + export interface ISeparated { + /** + * Parameters that would be composed by the LLM. + */ + llm: ISeparatedParameter[]; + + /** + * Parameters that would be composed by the human. + */ + human: ISeparatedParameter[]; + } + + /** + * Separated parameter. + */ + export interface ISeparatedParameter { + /** + * Index of the parameter. + */ + index: number; + + /** + * Type schema info of the parameter. + */ + schema: Schema; + } +} diff --git a/src/structures/ILlmSchema.ts b/src/structures/ILlmSchema.ts index e00ef7c..f35e78d 100644 --- a/src/structures/ILlmSchema.ts +++ b/src/structures/ILlmSchema.ts @@ -222,37 +222,20 @@ export namespace ILlmSchema { * Content media type restriction. */ contentMediaType?: string; - - /** - * Secret key for the schema. - * - * `x-wrtn-secret-key` is a property means a secret key that is required - * for the target API endpoint calling. If the secret key is not filled, - * the API call would be failed. - */ - "x-wrtn-secret-key"?: string; - - /** - * Secret scopes for the schema. - * - * `x-wrtn-secret-scopes` is a property means a list of secret scopes that - * are required for the target API endpoint calling. If the secret scopes - * are not satisfied, the API call would be failed. - */ - "x-wrtn-secret-scopes"?: string[]; } /** * Array type schema info. */ - export interface IArray extends __ISignificant<"array"> { + export interface IArray + extends __ISignificant<"array"> { /** * Items type schema info. * * The `items` means the type of the array elements. In other words, it is * the type schema info of the `T` in the TypeScript array type `Array`. */ - items: ILlmSchema; + items: Schema; /** * Unique items restriction. @@ -283,7 +266,8 @@ export namespace ILlmSchema { /** * Object type schema info. */ - export interface IObject extends __ISignificant<"object"> { + export interface IObject + extends __ISignificant<"object"> { /** * Properties of the object. * @@ -294,7 +278,7 @@ export namespace ILlmSchema { * If you need additional properties that is represented by dynamic key, * you can use the {@link additionalProperties} instead. */ - properties?: Record; + properties?: Record; /** * List of key values of the required properties. @@ -345,7 +329,7 @@ export namespace ILlmSchema { * - `true`: `Record` * - `ILlmSchema`: `Record` */ - additionalProperties?: boolean | ILlmSchema; + additionalProperties?: boolean | Schema; } /** @@ -379,11 +363,12 @@ export namespace ILlmSchema { * defined `anyOf` instead of the `oneOf`, it has been forcibly converted * to `oneOf` type by {@link OpenApi.convert OpenAPI conversion}. */ - export interface IOneOf extends __IAttribute { + export interface IOneOf + extends __IAttribute { /** * List of the union types. */ - oneOf: ILlmSchema[]; + oneOf: Exclude>[]; } /** @@ -419,112 +404,5 @@ export namespace ILlmSchema { * Whether the type is deprecated or not. */ deprecated?: boolean; - - /** - * Placeholder value for frontend application. - * - * Placeholder means the value to be shown in the input field as a hint. - * For example, when an email input field exists, the placeholder value - * would be "Insert your email address here". - */ - "x-wrtn-placeholder"?: string; - - /** - * Prerequisite API endpoint for the schema. - * - * `x-wrtn-prerequisite` is a property representing the prerequisite API - * interaction. It means that, the endpoint API should be called before - * calling the target API, for composing some argument value. - * - * @reference https://github.com/wrtnio/decorators/blob/main/src/Prerequisite.ts - */ - "x-wrtn-prerequisite"?: { - /** - * HTTP method to call the endpoint. - */ - method: "get" | "post" | "patch" | "put" | "delete"; - - /** - * Path of the endpoint. - */ - path: string; - } & ( - | { - /** - * Function returning transformed values using JMESPath expression. - * - * `Prerequisite.Props.jmesPath` is a string typed property that extracts desired values - * from the prerequisite API response using a JMESPath expression. This property simplifies - * and replaces the `label`, `value`, and `array` properties. - * - * JMESPath expressions are used to extract the desired data based on the API response. - * The expression must always be a valid JMESPath syntax. - * - * - Type: `jmesPath: string` - * - Example: `"members[*].data.title"` - * - Usage: `jmespath.search(response, jmesPath)` - * - * Note: The `label`, `value`, and `array` properties are no longer in use. - */ - jmesPath: string; - } - | { - /** - * Transform function returning label. - * - * `Prerequisite.Props.label` is a string typed property representing - * a function returning the label from the element instance of the - * prerequisite API respond array. - * - * The function script must be a string value that can be parsed by - * `new Function(string)` statement. Also, its parameter names are - * always `elem`, `index` and `array`. Of course, the function's - * return type must be always `string`. - * - * - type: `label: (elem: Element, index: number, array: Element[]) => string` - * - example: `return elem.title` - * - how to use: `new Function("elem", "index", "array", labelScript)(...)` - */ - label: string; - - /** - * Transform function returning target value. - * - * `Prerequisite.Props.value` is a string typed property representing - * a function returning the target value from the element instance of - * the prerequisite API respond array. If you've defined this `Prerequisite` - * type to a `number` type, the returned value must be actual number type. - * - * The function script must be a string value that can be parsed by - * `new Function(string)` statement. Also, its parameter names are always - * `elem`, `index` and `array`. - * - * - type: `value: (elem: Element, index: number, array: Element[]) => Value` - * - example: `return elem.no` - * - how to use: `new Function("elem", "index", "array", valueScript)(...)` - */ - value: string; - - /** - * Transform function returning array instance. - * - * `Prerequisite.Props.array` is a string typed property representing - * a function returning an array instance from the response of the - * prerequisite API. - * - * The function script must be a string value that can be parsed by - * `new Function(string)` statement. Also, its parameter name is - * always `response`. - * - * If the prerequisite API responses an array and it is the desired one, - * you don't need to specify this property. - * - * - type: `array: (response: Response) => Elemenet[]` - * - example: `return response.members.map(m => m.data)` - * - how to use: `new Function("response", arrayScript)(response)` - */ - array?: string; - } - ); } } diff --git a/test/controllers/AppController.ts b/test/controllers/AppController.ts index 9b2554f..351514e 100644 --- a/test/controllers/AppController.ts +++ b/test/controllers/AppController.ts @@ -10,42 +10,42 @@ import { tags } from "typia"; @Controller() export class AppController { - @TypedRoute.Get(":a/:b/:c/parameters") + @TypedRoute.Get(":index/:level/:optimal/parameters") public parameters( - @TypedParam("a") a: string, - @TypedParam("b") b: number, - @TypedParam("c") c: boolean, + @TypedParam("index") index: string, + @TypedParam("level") level: number, + @TypedParam("optimal") optimal: boolean, ) { - return { a, b, c }; + return { index, level, optimal }; } - @TypedRoute.Get(":a/:b/:c/query") + @TypedRoute.Get(":index/:level/:optimal/query") public query( - @TypedParam("a") a: string, - @TypedParam("b") b: number, - @TypedParam("c") c: boolean, + @TypedParam("index") index: string, + @TypedParam("level") level: number, + @TypedParam("optimal") optimal: boolean, @TypedQuery() query: IQuery, ) { - return { a, b, c, query }; + return { index, level, optimal, query }; } - @TypedRoute.Post(":a/:b/:c/body") + @TypedRoute.Post(":index/:level/:optimal/body") public body( - @TypedParam("a") a: string, - @TypedParam("b") b: number, - @TypedParam("c") c: boolean, + @TypedParam("index") index: string, + @TypedParam("level") level: number, + @TypedParam("optimal") optimal: boolean, @TypedBody() body: IBody, ) { - return { a, b, c, body }; + return { index, level, optimal, body }; } - @TypedRoute.Post(":a/:b/:c/query/body") + @TypedRoute.Post(":index/:level/:optimal/query/body") public query_body( - @TypedParam("a") a: string, - @TypedParam("b") b: number, - @TypedParam("c") c: boolean, + @TypedParam("index") index: string, + @TypedParam("level") level: number, + @TypedParam("optimal") optimal: boolean, @Query("thumbnail") thumbnail: string & tags.Format<"uri"> & tags.ContentMediaType<"image/*">, @TypedQuery() @@ -56,9 +56,9 @@ export class AppController { body: IBody, ) { return { - a, - b, - c, + index, + level, + optimal, query: { ...query, thumbnail, @@ -67,19 +67,19 @@ export class AppController { }; } - @TypedRoute.Post(":a/:b/:c/multipart") + @TypedRoute.Post(":index/:level/:optimal/multipart") public query_multipart( - @TypedParam("a") a: string, - @TypedParam("b") b: number, - @TypedParam("c") c: boolean, + @TypedParam("index") index: string, + @TypedParam("level") level: number, + @TypedParam("optimal") optimal: boolean, @TypedQuery() query: IQuery, @TypedFormData.Body() body: IMultipart, ) { return { - a, - b, - c, + index, + level, + optimal, query, body: { ...body, diff --git a/test/controllers/AppFilter.ts b/test/controllers/AppFilter.ts index 354b8b5..f4d7b93 100644 --- a/test/controllers/AppFilter.ts +++ b/test/controllers/AppFilter.ts @@ -1,11 +1,8 @@ -import { ArgumentsHost, Catch, HttpException, Logger } from "@nestjs/common"; -import { HttpArgumentsHost } from "@nestjs/common/interfaces"; +import { ArgumentsHost, Catch, HttpException } from "@nestjs/common"; import { BaseExceptionFilter } from "@nestjs/core"; @Catch() export class AppFilter extends BaseExceptionFilter { - private readonly logger = new Logger(AppFilter.name); - public async catch(exception: HttpException | Error, host: ArgumentsHost) { const status: number = exception instanceof HttpException ? exception.getStatus() : 500; diff --git a/test/features/llm/test_http_llm_application_keyword.ts b/test/features/llm/test_http_llm_application_keyword.ts new file mode 100644 index 0000000..b4d705c --- /dev/null +++ b/test/features/llm/test_http_llm_application_keyword.ts @@ -0,0 +1,36 @@ +import { TestValidator } from "@nestia/e2e"; +import { + HttpLanguageModel, + IHttpLlmApplication, + IHttpMigrateRoute, + ILlmSchema, + LlmTypeChecker, + OpenApi, +} from "@samchon/openapi"; + +import swagger from "../../swagger.json"; + +export const test_http_llm_application_keyword = (): void => { + const document: OpenApi.IDocument = OpenApi.convert(swagger as any); + const application: IHttpLlmApplication = HttpLanguageModel.application( + document, + { + keyword: true, + }, + ); + for (const func of application.functions) { + const route: IHttpMigrateRoute = func.route(); + TestValidator.equals("length")(1)(func.parameters.length); + TestValidator.equals("properties")([ + ...route.parameters.map((p) => p.key), + ...(route.query ? ["query"] : []), + ...(route.body ? ["body"] : []), + ])( + (() => { + const schema: ILlmSchema = func.parameters[0]; + if (!LlmTypeChecker.isObject(schema)) return []; + return Object.keys(schema.properties ?? {}); + })(), + ); + } +}; diff --git a/test/features/llm/test_http_llm_application_positional.ts b/test/features/llm/test_http_llm_application_positional.ts new file mode 100644 index 0000000..775e438 --- /dev/null +++ b/test/features/llm/test_http_llm_application_positional.ts @@ -0,0 +1,25 @@ +import { TestValidator } from "@nestia/e2e"; +import { + HttpLanguageModel, + IHttpLlmApplication, + IHttpMigrateRoute, + OpenApi, +} from "@samchon/openapi"; + +import swagger from "../../swagger.json"; + +export const test_http_llm_application_positional = (): void => { + const document: OpenApi.IDocument = OpenApi.convert(swagger as any); + const application: IHttpLlmApplication = HttpLanguageModel.application( + document, + { + keyword: false, + }, + ); + for (const func of application.functions) { + const route: IHttpMigrateRoute = func.route(); + TestValidator.equals("length")(func.parameters.length)( + route.parameters.length + (route.query ? 1 : 0) + (route.body ? 1 : 0), + ); + } +}; diff --git a/test/features/llm/test_llm_merge_parameters.ts b/test/features/llm/test_llm_merge_parameters.ts new file mode 100644 index 0000000..f6e4b61 --- /dev/null +++ b/test/features/llm/test_llm_merge_parameters.ts @@ -0,0 +1,42 @@ +import { TestValidator } from "@nestia/e2e"; +import { LlmMerger } from "@samchon/openapi/lib/converters/LlmMerger"; + +export const test_llm_merge_parameters = (): void => { + TestValidator.equals("atomics")( + LlmMerger.parameters({ + function: { + name: "test", + parameters: [ + { type: "boolean" }, + { type: "number" }, + { type: "string" }, + { type: "string" }, + ], + separated: { + human: [ + { + schema: { type: "boolean" }, + index: 0, + }, + { + schema: { type: "number" }, + index: 1, + }, + ], + llm: [ + { + schema: { type: "string" }, + index: 2, + }, + { + schema: { type: "string" }, + index: 3, + }, + ], + }, + }, + human: [false, 1], + llm: ["two", "three"], + }), + )([false, 1, "two", "three"]); +}; diff --git a/test/features/llm/test_llm_merge_value.ts b/test/features/llm/test_llm_merge_value.ts new file mode 100644 index 0000000..4f3c1f9 --- /dev/null +++ b/test/features/llm/test_llm_merge_value.ts @@ -0,0 +1,62 @@ +import { TestValidator } from "@nestia/e2e"; +import { LlmMerger } from "@samchon/openapi/lib/converters/LlmMerger"; + +export const test_llm_merge_parameters = (): void => { + TestValidator.equals("number")(LlmMerger.value(1, 2))(2); + TestValidator.equals("nullable")(LlmMerger.value(0, null))(0); + TestValidator.equals("optional")(LlmMerger.value(0, undefined))(0); + TestValidator.equals("object")( + LlmMerger.value( + { + a: "A", + array: [1, 2, 3], + nestedArray: [{ alpha: "alpha" }, { alpha: "alpha" }], + object: { x: "X" }, + }, + { + b: "B", + array: [3, 4, 5], + nestedArray: [{ beta: "beta" }, { beta: "beta" }], + object: { y: "Y" }, + }, + ), + )({ + a: "A", + b: "B", + array: [3, 4, 5], + nestedArray: [ + { + alpha: "alpha", + beta: "beta", + }, + { + alpha: "alpha", + beta: "beta", + }, + ], + object: { x: "X", y: "Y" }, + }); + TestValidator.equals("membership")( + LlmMerger.value( + { + name: "Samchon", + email: "samchon.github@gmail.com", + password: "1234", + age: 30, + gender: 1, + }, + { + homepage: "https://github.com/samchon", + secret: "something", + }, + ), + )({ + name: "Samchon", + email: "samchon.github@gmail.com", + password: "1234", + age: 30, + gender: 1, + homepage: "https://github.com/samchon", + secret: "something", + }); +}; diff --git a/test/features/llm/test_llm_schema_object.ts b/test/features/llm/test_llm_schema_object.ts new file mode 100644 index 0000000..02023e9 --- /dev/null +++ b/test/features/llm/test_llm_schema_object.ts @@ -0,0 +1,55 @@ +import { TestValidator } from "@nestia/e2e"; +import { HttpLanguageModel, ILlmSchema } from "@samchon/openapi"; +import typia, { IJsonApplication, tags } from "typia"; + +export const test_llm_schema_object = (): void => { + const app: IJsonApplication = typia.json.application<[First]>(); + const schema: ILlmSchema | null = HttpLanguageModel.schema({ + components: app.components, + schema: app.schemas[0], + }); + TestValidator.equals("schema")(schema)({ + type: "object", + required: ["second"], + properties: { + second: { + type: "object", + required: ["third"], + description: "The second property", + properties: { + third: { + type: "object", + required: ["id"], + description: "The third property", + properties: { + id: { + type: "string", + format: "uuid", + description: "Hello word", + }, + }, + }, + }, + }, + }, + }); +}; + +interface First { + /** + * The second property + */ + second: Second; +} +interface Second { + /** + * The third property + */ + third: Third; +} +interface Third { + /** + * Hello word + */ + id: string & tags.Format<"uuid">; +} diff --git a/test/features/llm/test_llm_schema_oneof.ts b/test/features/llm/test_llm_schema_oneof.ts new file mode 100644 index 0000000..f4fa154 --- /dev/null +++ b/test/features/llm/test_llm_schema_oneof.ts @@ -0,0 +1,77 @@ +import { TestValidator } from "@nestia/e2e"; +import { HttpLanguageModel, ILlmSchema } from "@samchon/openapi"; +import typia, { IJsonApplication } from "typia"; + +export const test_llm_schema_oneof = (): void => { + const app: IJsonApplication = + typia.json.application<[Circle | Triangle | Rectangle]>(); + const casted: ILlmSchema | null = HttpLanguageModel.schema({ + components: app.components, + schema: app.schemas[0], + }); + TestValidator.equals("oneOf")(casted)({ + oneOf: [ + { + type: "object", + properties: { + type: { + type: "string", + enum: ["circle"], + }, + radius: { + type: "number", + }, + }, + required: ["type", "radius"], + }, + { + type: "object", + properties: { + type: { + type: "string", + enum: ["triangle"], + }, + base: { + type: "number", + }, + height: { + type: "number", + }, + }, + required: ["type", "base", "height"], + }, + { + type: "object", + properties: { + type: { + type: "string", + enum: ["square"], + }, + width: { + type: "number", + }, + height: { + type: "number", + }, + }, + required: ["type", "width", "height"], + }, + ], + ...{ discriminator: undefined }, + }); +}; + +interface Circle { + type: "circle"; + radius: number; +} +interface Triangle { + type: "triangle"; + base: number; + height: number; +} +interface Rectangle { + type: "square"; + width: number; + height: number; +} diff --git a/test/features/llm/test_llm_schema_separate_array.ts b/test/features/llm/test_llm_schema_separate_array.ts new file mode 100644 index 0000000..c51bd4c --- /dev/null +++ b/test/features/llm/test_llm_schema_separate_array.ts @@ -0,0 +1,43 @@ +import { TestValidator } from "@nestia/e2e"; +import { + HttpLanguageModel, + ILlmSchema, + LlmTypeChecker, + OpenApi, +} from "@samchon/openapi"; +import { LlmSchemaSeparator } from "@samchon/openapi/lib/converters/LlmSchemaSeparator"; +import typia, { tags } from "typia"; + +export const test_llm_schema_separate_array = (): void => { + const separator = LlmSchemaSeparator.schema( + (s) => LlmTypeChecker.isString(s) && s.contentMediaType !== undefined, + ); + const member: ILlmSchema = schema(typia.json.application<[IMember[]]>()); + const upload: ILlmSchema = schema(typia.json.application<[IFileUpload[]]>()); + const combined: ILlmSchema = schema(typia.json.application<[ICombined[]]>()); + + TestValidator.equals("member")(separator(member))([member, null]); + TestValidator.equals("upload")(separator(upload))([null, upload]); + TestValidator.equals("combined")(separator(combined))([member, upload]); +}; + +interface IMember { + id: number; + name: string; +} +interface IFileUpload { + file: string & tags.ContentMediaType<"image/png">; +} +interface ICombined extends IMember, IFileUpload {} + +const schema = (props: { + components: OpenApi.IComponents; + schemas: OpenApi.IJsonSchema[]; +}): ILlmSchema => { + const schema: ILlmSchema | null = HttpLanguageModel.schema({ + components: props.components, + schema: props.schemas[0], + }); + if (schema === null) throw new Error("Invalid schema"); + return schema; +}; diff --git a/test/features/llm/test_llm_schema_separate_nested.ts b/test/features/llm/test_llm_schema_separate_nested.ts new file mode 100644 index 0000000..7fe5921 --- /dev/null +++ b/test/features/llm/test_llm_schema_separate_nested.ts @@ -0,0 +1,59 @@ +import { TestValidator } from "@nestia/e2e"; +import { + HttpLanguageModel, + ILlmSchema, + LlmTypeChecker, + OpenApi, +} from "@samchon/openapi"; +import { LlmSchemaSeparator } from "@samchon/openapi/lib/converters/LlmSchemaSeparator"; +import typia, { tags } from "typia"; + +export const test_llm_schema_separate_nested = (): void => { + const separator = LlmSchemaSeparator.schema( + (s) => LlmTypeChecker.isString(s) && s.contentMediaType !== undefined, + ); + const member: ILlmSchema = schema( + typia.json.application<[INested]>(), + ); + const upload: ILlmSchema = schema( + typia.json.application<[INested]>(), + ); + const combined: ILlmSchema = schema( + typia.json.application<[INested]>(), + ); + + TestValidator.equals("member")(separator(member))([member, null]); + TestValidator.equals("upload")(separator(upload))([null, upload]); + TestValidator.equals("combined")(separator(combined))([member, 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 = (props: { + components: OpenApi.IComponents; + schemas: OpenApi.IJsonSchema[]; +}): ILlmSchema => { + const schema: ILlmSchema | null = HttpLanguageModel.schema({ + components: props.components, + schema: props.schemas[0], + }); + if (schema === null) throw new Error("Invalid schema"); + return schema; +}; diff --git a/test/features/llm/test_llm_schema_separate_object.ts b/test/features/llm/test_llm_schema_separate_object.ts new file mode 100644 index 0000000..2c58b1c --- /dev/null +++ b/test/features/llm/test_llm_schema_separate_object.ts @@ -0,0 +1,43 @@ +import { TestValidator } from "@nestia/e2e"; +import { + HttpLanguageModel, + ILlmSchema, + LlmTypeChecker, + OpenApi, +} from "@samchon/openapi"; +import { LlmSchemaSeparator } from "@samchon/openapi/lib/converters/LlmSchemaSeparator"; +import typia, { tags } from "typia"; + +export const test_llm_schema_separate_object = (): void => { + const separator = LlmSchemaSeparator.schema( + (s) => LlmTypeChecker.isString(s) && s.contentMediaType !== undefined, + ); + const member: ILlmSchema = schema(typia.json.application<[IMember]>()); + const upload: ILlmSchema = schema(typia.json.application<[IFileUpload]>()); + const combined: ILlmSchema = schema(typia.json.application<[ICombined]>()); + + TestValidator.equals("member")(separator(member))([member, null]); + TestValidator.equals("upload")(separator(upload))([null, upload]); + TestValidator.equals("combined")(separator(combined))([member, 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 = (props: { + components: OpenApi.IComponents; + schemas: OpenApi.IJsonSchema[]; +}): ILlmSchema => { + const schema: ILlmSchema | null = HttpLanguageModel.schema({ + components: props.components, + schema: props.schemas[0], + }); + if (schema === null) throw new Error("Invalid schema"); + return schema; +}; diff --git a/test/features/llm/test_llm_schema_separate_string.ts b/test/features/llm/test_llm_schema_separate_string.ts new file mode 100644 index 0000000..87fafd7 --- /dev/null +++ b/test/features/llm/test_llm_schema_separate_string.ts @@ -0,0 +1,17 @@ +import { TestValidator } from "@nestia/e2e"; +import { ILlmSchema, LlmTypeChecker } from "@samchon/openapi"; +import { LlmSchemaSeparator } from "@samchon/openapi/lib/converters/LlmSchemaSeparator"; + +export const test_schema_separate_string = (): void => { + const separator = LlmSchemaSeparator.schema( + (s) => LlmTypeChecker.isString(s) && s.contentMediaType !== undefined, + ); + const plain: ILlmSchema = { type: "string" }; + const upload: ILlmSchema = { + type: "string", + format: "uri", + contentMediaType: "image/png", + }; + TestValidator.equals("plain")(separator(plain))([plain, null]); + TestValidator.equals("upload")(separator(upload))([null, upload]); +}; diff --git a/test/features/migrate/test_http_migrate_fetch_body.ts b/test/features/migrate/test_http_migrate_fetch_body.ts index 5316f2b..71866ce 100644 --- a/test/features/migrate/test_http_migrate_fetch_body.ts +++ b/test/features/migrate/test_http_migrate_fetch_body.ts @@ -1,6 +1,6 @@ import { TestValidator } from "@nestia/e2e"; import { - HttpMigrateRouteFetcher, + HttpMigration, IHttpMigrateApplication, IHttpMigrateRoute, } from "@samchon/openapi"; @@ -14,19 +14,19 @@ export const test_http_migrate_fetch_body = async ( connection: IHttpConnection, ): Promise => { const document: OpenApi.IDocument = OpenApi.convert(swagger as any); - const app: IHttpMigrateApplication = OpenApi.migrate(document); + const app: IHttpMigrateApplication = HttpMigration.application(document); const route: IHttpMigrateRoute | undefined = app.routes.find( - (r) => r.path === "/{a}/{b}/{c}/body" && r.method === "post", + (r) => r.path === "/{index}/{level}/{optimal}/body" && r.method === "post", ); if (route === undefined) throw new Error("Route not found"); - const response: IHttpResponse = await HttpMigrateRouteFetcher.propagate({ + const response: IHttpResponse = await HttpMigration.propagate({ connection, route, parameters: { - a: "string", - b: 123, - c: true, + index: "string", + level: 123, + optimal: true, }, body: { title: "some title", diff --git a/test/features/migrate/test_http_migrate_fetch_keyword_parameters.ts b/test/features/migrate/test_http_migrate_fetch_keyword_parameters.ts index 0384e02..55f3833 100644 --- a/test/features/migrate/test_http_migrate_fetch_keyword_parameters.ts +++ b/test/features/migrate/test_http_migrate_fetch_keyword_parameters.ts @@ -1,10 +1,10 @@ import { - HttpMigrateRouteFetcher, + HttpMigration, + IHttpConnection, IHttpMigrateApplication, IHttpMigrateRoute, OpenApi, } from "@samchon/openapi"; -import { IHttpConnection } from "@samchon/openapi/lib/structures/IHttpConnection"; import swagger from "../../swagger.json"; @@ -12,19 +12,20 @@ export const test_http_migrate_fetch_keyword_parameters = async ( connection: IHttpConnection, ): Promise => { const document: OpenApi.IDocument = OpenApi.convert(swagger as any); - const app: IHttpMigrateApplication = OpenApi.migrate(document); + const app: IHttpMigrateApplication = HttpMigration.application(document); const route: IHttpMigrateRoute | undefined = app.routes.find( - (r) => r.path === "/{a}/{b}/{c}/parameters" && r.method === "get", + (r) => + r.path === "/{index}/{level}/{optimal}/parameters" && r.method === "get", ); if (route === undefined) throw new Error("Route not found"); - await HttpMigrateRouteFetcher.request({ + await HttpMigration.request({ connection, route, parameters: { - a: "three", - b: 2, - c: true, + index: "three", + level: 2, + optimal: true, }, }); }; diff --git a/test/features/migrate/test_http_migrate_fetch_multipart.ts b/test/features/migrate/test_http_migrate_fetch_multipart.ts index 60a873c..b03556b 100644 --- a/test/features/migrate/test_http_migrate_fetch_multipart.ts +++ b/test/features/migrate/test_http_migrate_fetch_multipart.ts @@ -1,10 +1,10 @@ import { - HttpMigrateRouteFetcher, + HttpMigration, + IHttpConnection, IHttpMigrateApplication, IHttpMigrateRoute, OpenApi, } from "@samchon/openapi"; -import { IHttpConnection } from "@samchon/openapi/lib/structures/IHttpConnection"; import swagger from "../../swagger.json"; @@ -12,19 +12,20 @@ export const test_http_migrate_fetch_multipart = async ( connection: IHttpConnection, ): Promise => { const document: OpenApi.IDocument = OpenApi.convert(swagger as any); - const app: IHttpMigrateApplication = OpenApi.migrate(document); + const app: IHttpMigrateApplication = HttpMigration.application(document); const route: IHttpMigrateRoute | undefined = app.routes.find( - (r) => r.path === "/{a}/{b}/{c}/multipart" && r.method === "post", + (r) => + r.path === "/{index}/{level}/{optimal}/multipart" && r.method === "post", ); if (route === undefined) throw new Error("Route not found"); - await HttpMigrateRouteFetcher.request({ + await HttpMigration.request({ connection, route, parameters: { - a: "three", - b: 2, - c: true, + index: "three", + level: 2, + optimal: true, }, query: { summary: "some summary", diff --git a/test/features/migrate/test_http_migrate_fetch_positional_parameters.ts b/test/features/migrate/test_http_migrate_fetch_positional_parameters.ts index b3259b8..02fa1ae 100644 --- a/test/features/migrate/test_http_migrate_fetch_positional_parameters.ts +++ b/test/features/migrate/test_http_migrate_fetch_positional_parameters.ts @@ -1,10 +1,10 @@ import { - HttpMigrateRouteFetcher, + HttpMigration, + IHttpConnection, IHttpMigrateApplication, IHttpMigrateRoute, OpenApi, } from "@samchon/openapi"; -import { IHttpConnection } from "@samchon/openapi/lib/structures/IHttpConnection"; import swagger from "../../swagger.json"; @@ -12,13 +12,14 @@ export const test_http_migrate_fetch_positional_parameters = async ( connection: IHttpConnection, ): Promise => { const document: OpenApi.IDocument = OpenApi.convert(swagger as any); - const app: IHttpMigrateApplication = OpenApi.migrate(document); + const app: IHttpMigrateApplication = HttpMigration.application(document); const route: IHttpMigrateRoute | undefined = app.routes.find( - (r) => r.path === "/{a}/{b}/{c}/parameters" && r.method === "get", + (r) => + r.path === "/{index}/{level}/{optimal}/parameters" && r.method === "get", ); if (route === undefined) throw new Error("Route not found"); - await HttpMigrateRouteFetcher.request({ + await HttpMigration.request({ connection, route, parameters: ["three", 2, true], diff --git a/test/features/migrate/test_http_migrate_fetch_propagate.ts b/test/features/migrate/test_http_migrate_fetch_propagate.ts index 1648e35..b757b87 100644 --- a/test/features/migrate/test_http_migrate_fetch_propagate.ts +++ b/test/features/migrate/test_http_migrate_fetch_propagate.ts @@ -1,12 +1,12 @@ import { TestValidator } from "@nestia/e2e"; import { - HttpMigrateRouteFetcher, + HttpMigration, + IHttpConnection, IHttpMigrateApplication, IHttpMigrateRoute, + IHttpResponse, OpenApi, } from "@samchon/openapi"; -import { IHttpConnection } from "@samchon/openapi/lib/structures/IHttpConnection"; -import { IHttpResponse } from "@samchon/openapi/lib/structures/IHttpResponse"; import swagger from "../../swagger.json"; @@ -14,13 +14,14 @@ export const test_http_migrate_fetch_propagate = async ( connection: IHttpConnection, ): Promise => { const document: OpenApi.IDocument = OpenApi.convert(swagger as any); - const app: IHttpMigrateApplication = OpenApi.migrate(document); + const app: IHttpMigrateApplication = HttpMigration.application(document); const route: IHttpMigrateRoute | undefined = app.routes.find( - (r) => r.path === "/{a}/{b}/{c}/parameters" && r.method === "get", + (r) => + r.path === "/{index}/{level}/{optimal}/parameters" && r.method === "get", ); if (route === undefined) throw new Error("Route not found"); - const response: IHttpResponse = await HttpMigrateRouteFetcher.propagate({ + const response: IHttpResponse = await HttpMigration.propagate({ connection, route, parameters: ["three", "two", "one"], diff --git a/test/features/migrate/test_http_migrate_fetch_query.ts b/test/features/migrate/test_http_migrate_fetch_query.ts index ccc58d7..ec1c694 100644 --- a/test/features/migrate/test_http_migrate_fetch_query.ts +++ b/test/features/migrate/test_http_migrate_fetch_query.ts @@ -1,10 +1,10 @@ import { - HttpMigrateRouteFetcher, + HttpMigration, + IHttpConnection, IHttpMigrateApplication, IHttpMigrateRoute, + OpenApi, } from "@samchon/openapi"; -import { OpenApi } from "@samchon/openapi/lib/OpenApi"; -import { IHttpConnection } from "@samchon/openapi/lib/structures/IHttpConnection"; import swagger from "../../swagger.json"; @@ -12,19 +12,19 @@ export const test_http_migrate_fetch_query = async ( connection: IHttpConnection, ): Promise => { const document: OpenApi.IDocument = OpenApi.convert(swagger as any); - const app: IHttpMigrateApplication = OpenApi.migrate(document); + const app: IHttpMigrateApplication = HttpMigration.application(document); const route: IHttpMigrateRoute | undefined = app.routes.find( - (r) => r.path === "/{a}/{b}/{c}/query" && r.method === "get", + (r) => r.path === "/{index}/{level}/{optimal}/query" && r.method === "get", ); if (route === undefined) throw new Error("Route not found"); - await HttpMigrateRouteFetcher.request({ + await HttpMigration.request({ connection, route, parameters: { - a: "string", - b: 123, - c: true, + index: "string", + level: 123, + optimal: true, }, query: { summary: "some summary", diff --git a/test/features/migrate/test_http_migrate_fetch_query_and_body.ts b/test/features/migrate/test_http_migrate_fetch_query_and_body.ts index 3ced63d..58fe43f 100644 --- a/test/features/migrate/test_http_migrate_fetch_query_and_body.ts +++ b/test/features/migrate/test_http_migrate_fetch_query_and_body.ts @@ -1,10 +1,10 @@ import { - HttpMigrateRouteFetcher, + HttpMigration, + IHttpConnection, IHttpMigrateApplication, IHttpMigrateRoute, + OpenApi, } from "@samchon/openapi"; -import { OpenApi } from "@samchon/openapi/lib/OpenApi"; -import { IHttpConnection } from "@samchon/openapi/lib/structures/IHttpConnection"; import swagger from "../../swagger.json"; @@ -12,19 +12,20 @@ export const test_http_migrate_fetch_query_and_body = async ( connection: IHttpConnection, ): Promise => { const document: OpenApi.IDocument = OpenApi.convert(swagger as any); - const app: IHttpMigrateApplication = OpenApi.migrate(document); + const app: IHttpMigrateApplication = HttpMigration.application(document); const route: IHttpMigrateRoute | undefined = app.routes.find( - (r) => r.path === "/{a}/{b}/{c}/query/body" && r.method === "post", + (r) => + r.path === "/{index}/{level}/{optimal}/query/body" && r.method === "post", ); if (route === undefined) throw new Error("Route not found"); - await HttpMigrateRouteFetcher.request({ + await HttpMigration.request({ connection, route, parameters: { - a: "string", - b: 123, - c: true, + index: "string", + level: 123, + optimal: true, }, query: { summary: "some summary", diff --git a/test/features/migrate/test_http_migrate_route_comment.ts b/test/features/migrate/test_http_migrate_route_comment.ts index 0c3951f..9750a09 100644 --- a/test/features/migrate/test_http_migrate_route_comment.ts +++ b/test/features/migrate/test_http_migrate_route_comment.ts @@ -1,5 +1,6 @@ import { TestValidator } from "@nestia/e2e"; import { + HttpMigration, IHttpMigrateApplication, IHttpMigrateRoute, OpenApi, @@ -15,7 +16,7 @@ export const test_http_migrate_route_comment = async (): Promise => { ), ), ); - const migrate: IHttpMigrateApplication = OpenApi.migrate(swagger); + const migrate: IHttpMigrateApplication = HttpMigration.application(swagger); const route: IHttpMigrateRoute | undefined = migrate.routes.find( (r) => r.path === "/shoppings/sellers/sales/{id}" && r.method === "put", ); diff --git a/test/features/migrate/test_http_migrate_v20.ts b/test/features/migrate/test_http_migrate_v20.ts index a39d91e..d95263d 100644 --- a/test/features/migrate/test_http_migrate_v20.ts +++ b/test/features/migrate/test_http_migrate_v20.ts @@ -1,4 +1,9 @@ -import { IHttpMigrateApplication, OpenApi, SwaggerV2 } from "@samchon/openapi"; +import { + HttpMigration, + IHttpMigrateApplication, + OpenApi, + SwaggerV2, +} from "@samchon/openapi"; import fs from "fs"; import typia from "typia"; @@ -10,7 +15,7 @@ export const test_http_migrate_v20 = async (): Promise => { JSON.parse(await fs.promises.readFile(`${path}/${file}`, "utf8")), ); const openapi: OpenApi.IDocument = OpenApi.convert(swagger); - const migrate: IHttpMigrateApplication = OpenApi.migrate(openapi); + const migrate: IHttpMigrateApplication = HttpMigration.application(openapi); typia.assert(migrate); } }; diff --git a/test/features/migrate/test_http_migrate_v30.ts b/test/features/migrate/test_http_migrate_v30.ts index 5b90a56..ecdf998 100644 --- a/test/features/migrate/test_http_migrate_v30.ts +++ b/test/features/migrate/test_http_migrate_v30.ts @@ -1,4 +1,9 @@ -import { IHttpMigrateApplication, OpenApi, OpenApiV3 } from "@samchon/openapi"; +import { + HttpMigration, + IHttpMigrateApplication, + OpenApi, + OpenApiV3, +} from "@samchon/openapi"; import fs from "fs"; import typia from "typia"; @@ -10,7 +15,7 @@ export const test_http_migrate_v30 = async (): Promise => { JSON.parse(await fs.promises.readFile(`${path}/${file}`, "utf8")), ); const openapi: OpenApi.IDocument = OpenApi.convert(swagger); - const migrate: IHttpMigrateApplication = OpenApi.migrate(openapi); + const migrate: IHttpMigrateApplication = HttpMigration.application(openapi); typia.assert(migrate); } }; diff --git a/test/features/migrate/test_http_migrate_v31.ts b/test/features/migrate/test_http_migrate_v31.ts index 6446ce0..0a9f3d8 100644 --- a/test/features/migrate/test_http_migrate_v31.ts +++ b/test/features/migrate/test_http_migrate_v31.ts @@ -1,4 +1,5 @@ import { + HttpMigration, IHttpMigrateApplication, OpenApi, OpenApiV3_1, @@ -14,7 +15,7 @@ export const test_http_migrate_v31 = async (): Promise => { JSON.parse(await fs.promises.readFile(`${path}/${file}`, "utf8")), ); const openapi: OpenApi.IDocument = OpenApi.convert(swagger); - const migrate: IHttpMigrateApplication = OpenApi.migrate(openapi); + const migrate: IHttpMigrateApplication = HttpMigration.application(openapi); typia.assert(migrate); } }; diff --git a/test/swagger.json b/test/swagger.json index ea3ada3..85d7a7d 100644 --- a/test/swagger.json +++ b/test/swagger.json @@ -15,12 +15,12 @@ } }, "paths": { - "/{a}/{b}/{c}/parameters": { + "/{index}/{level}/{optimal}/parameters": { "get": { "tags": [], "parameters": [ { - "name": "a", + "name": "index", "in": "path", "schema": { "type": "string" @@ -28,7 +28,7 @@ "required": true }, { - "name": "b", + "name": "level", "in": "path", "schema": { "type": "number" @@ -36,7 +36,7 @@ "required": true }, { - "name": "c", + "name": "optimal", "in": "path", "schema": { "type": "boolean" @@ -58,12 +58,12 @@ } } }, - "/{a}/{b}/{c}/query": { + "/{index}/{level}/{optimal}/query": { "get": { "tags": [], "parameters": [ { - "name": "a", + "name": "index", "in": "path", "schema": { "type": "string" @@ -71,7 +71,7 @@ "required": true }, { - "name": "b", + "name": "level", "in": "path", "schema": { "type": "number" @@ -79,7 +79,7 @@ "required": true }, { - "name": "c", + "name": "optimal", "in": "path", "schema": { "type": "boolean" @@ -87,26 +87,20 @@ "required": true }, { - "name": "flag", + "name": "summary", "in": "query", "schema": { - "type": "boolean" - }, - "required": true - }, - { - "name": "value", - "in": "query", - "schema": { - "type": "number" + "type": "string" }, "required": true }, { - "name": "text", + "name": "thumbnail", "in": "query", "schema": { - "type": "string" + "type": "string", + "format": "uri", + "contentMediaType": "image/*" }, "required": true } @@ -125,12 +119,12 @@ } } }, - "/{a}/{b}/{c}/body": { + "/{index}/{level}/{optimal}/body": { "post": { "tags": [], "parameters": [ { - "name": "a", + "name": "index", "in": "path", "schema": { "type": "string" @@ -138,7 +132,7 @@ "required": true }, { - "name": "b", + "name": "level", "in": "path", "schema": { "type": "number" @@ -146,7 +140,7 @@ "required": true }, { - "name": "c", + "name": "optimal", "in": "path", "schema": { "type": "boolean" @@ -158,23 +152,7 @@ "content": { "application/json": { "schema": { - "type": "object", - "properties": { - "flag": { - "type": "boolean" - }, - "value": { - "type": "number" - }, - "text": { - "type": "string" - } - }, - "required": [ - "flag", - "value", - "text" - ] + "$ref": "#/components/schemas/IBody" } } }, @@ -194,12 +172,12 @@ } } }, - "/{a}/{b}/{c}/query/body": { + "/{index}/{level}/{optimal}/query/body": { "post": { "tags": [], "parameters": [ { - "name": "a", + "name": "index", "in": "path", "schema": { "type": "string" @@ -207,7 +185,7 @@ "required": true }, { - "name": "b", + "name": "level", "in": "path", "schema": { "type": "number" @@ -215,7 +193,7 @@ "required": true }, { - "name": "c", + "name": "optimal", "in": "path", "schema": { "type": "boolean" @@ -223,23 +201,17 @@ "required": true }, { - "name": "name", - "in": "query", - "schema": { - "type": "string" - }, - "required": true - }, - { - "name": "reference", + "name": "thumbnail", "in": "query", "schema": { - "type": "string" + "type": "string", + "format": "uri", + "contentMediaType": "image/*" }, "required": true }, { - "name": "memo", + "name": "summary", "in": "query", "schema": { "type": "string" @@ -251,23 +223,7 @@ "content": { "application/json": { "schema": { - "type": "object", - "properties": { - "flag": { - "type": "boolean" - }, - "value": { - "type": "number" - }, - "text": { - "type": "string" - } - }, - "required": [ - "flag", - "value", - "text" - ] + "$ref": "#/components/schemas/IBody" } } }, @@ -283,12 +239,12 @@ "query": { "type": "object", "properties": { - "memo": { + "summary": { "type": "string" } }, "required": [ - "memo" + "summary" ] } }, @@ -302,12 +258,12 @@ } } }, - "/{a}/{b}/{c}/multipart": { + "/{index}/{level}/{optimal}/multipart": { "post": { "tags": [], "parameters": [ { - "name": "a", + "name": "index", "in": "path", "schema": { "type": "string" @@ -315,7 +271,7 @@ "required": true }, { - "name": "b", + "name": "level", "in": "path", "schema": { "type": "number" @@ -323,7 +279,7 @@ "required": true }, { - "name": "c", + "name": "optimal", "in": "path", "schema": { "type": "boolean" @@ -331,26 +287,20 @@ "required": true }, { - "name": "flag", + "name": "summary", "in": "query", "schema": { - "type": "boolean" - }, - "required": true - }, - { - "name": "value", - "in": "query", - "schema": { - "type": "number" + "type": "string" }, "required": true }, { - "name": "text", + "name": "thumbnail", "in": "query", "schema": { - "type": "string" + "type": "string", + "format": "uri", + "contentMediaType": "image/*" }, "required": true } @@ -359,24 +309,7 @@ "content": { "multipart/form-data": { "schema": { - "type": "object", - "properties": { - "name": { - "type": "string" - }, - "reference": { - "type": "string" - }, - "file": { - "type": "string", - "format": "binary" - } - }, - "required": [ - "name", - "reference", - "file" - ] + "$ref": "#/components/schemas/IMultipart" } } }, @@ -392,20 +325,24 @@ "body": { "type": "object", "properties": { - "name": { + "file": { "type": "string" }, - "reference": { + "title": { "type": "string" }, - "file": { + "body": { "type": "string" + }, + "draft": { + "type": "boolean" } }, "required": [ - "name", - "reference", - "file" + "file", + "title", + "body", + "draft" ] } }, @@ -421,7 +358,68 @@ } }, "components": { - "schemas": {} + "schemas": { + "IQuery": { + "type": "object", + "properties": { + "summary": { + "type": "string" + }, + "thumbnail": { + "type": "string", + "format": "uri", + "contentMediaType": "image/*" + } + }, + "required": [ + "summary", + "thumbnail" + ] + }, + "IBody": { + "type": "object", + "properties": { + "title": { + "type": "string" + }, + "body": { + "type": "string" + }, + "draft": { + "type": "boolean" + } + }, + "required": [ + "title", + "body", + "draft" + ] + }, + "IMultipart": { + "type": "object", + "properties": { + "file": { + "type": "string", + "format": "binary" + }, + "title": { + "type": "string" + }, + "body": { + "type": "string" + }, + "draft": { + "type": "boolean" + } + }, + "required": [ + "file", + "title", + "body", + "draft" + ] + } + } }, "tags": [], "x-samchon-emended": true diff --git a/tsconfig.json b/tsconfig.json index 26ffa38..c21c8ed 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -94,8 +94,8 @@ // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ - // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ - // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ + "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ + "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ From 438848f3198d5aa26b8706135a8c4678c50efda0 Mon Sep 17 00:00:00 2001 From: Jeongho Nam Date: Thu, 5 Sep 2024 03:20:49 +0900 Subject: [PATCH 07/16] Completed everything --- package.json | 2 +- src/{HttpLanguageModel.ts => HttpLlm.ts} | 32 +++++----- src/HttpMigration.ts | 6 +- .../{LlmComposer.ts => HttpLlmConverter.ts} | 6 +- src/http/HttpLlmFunctionFetcher.ts | 38 +++++++----- src/index.ts | 2 +- .../LlmMerger.ts => utils/LlmDataMerger.ts} | 4 +- .../LlmSchemaSeparator.ts | 2 +- test/controllers/AppController.ts | 29 +++++---- .../llm/test_http_llm_application_keyword.ts | 11 ++-- .../test_http_llm_application_positional.ts | 11 ++-- .../llm/test_http_llm_fetcher_keyword_body.ts | 53 ++++++++++++++++ ...est_http_llm_fetcher_keyword_parameters.ts | 49 +++++++++++++++ .../test_http_llm_fetcher_keyword_query.ts | 54 +++++++++++++++++ ...http_llm_fetcher_keyword_query_and_body.ts | 60 +++++++++++++++++++ .../test_http_llm_fetcher_positional_body.ts | 47 +++++++++++++++ ..._http_llm_fetcher_positional_parameters.ts | 40 +++++++++++++ .../test_http_llm_fetcher_positional_query.ts | 50 ++++++++++++++++ ...p_llm_fetcher_positional_query_and_body.ts | 56 +++++++++++++++++ .../features/llm/test_llm_merge_parameters.ts | 4 +- test/features/llm/test_llm_merge_value.ts | 12 ++-- test/features/llm/test_llm_schema_object.ts | 4 +- test/features/llm/test_llm_schema_oneof.ts | 4 +- .../llm/test_llm_schema_separate_array.ts | 11 +--- .../llm/test_llm_schema_separate_nested.ts | 11 +--- .../llm/test_llm_schema_separate_object.ts | 11 +--- .../llm/test_llm_schema_separate_string.ts | 2 +- .../migrate/test_http_migrate_fetch_body.ts | 2 +- ...t_http_migrate_fetch_keyword_parameters.ts | 2 +- .../test_http_migrate_fetch_multipart.ts | 2 +- ...ttp_migrate_fetch_positional_parameters.ts | 2 +- .../test_http_migrate_fetch_propagate.ts | 2 +- .../migrate/test_http_migrate_fetch_query.ts | 2 +- .../test_http_migrate_fetch_query_and_body.ts | 2 +- test/swagger.json | 20 +++++-- 35 files changed, 527 insertions(+), 118 deletions(-) rename src/{HttpLanguageModel.ts => HttpLlm.ts} (69%) rename src/converters/{LlmComposer.ts => HttpLlmConverter.ts} (98%) rename src/{converters/LlmMerger.ts => utils/LlmDataMerger.ts} (96%) rename src/{converters => utils}/LlmSchemaSeparator.ts (98%) create mode 100644 test/features/llm/test_http_llm_fetcher_keyword_body.ts create mode 100644 test/features/llm/test_http_llm_fetcher_keyword_parameters.ts create mode 100644 test/features/llm/test_http_llm_fetcher_keyword_query.ts create mode 100644 test/features/llm/test_http_llm_fetcher_keyword_query_and_body.ts create mode 100644 test/features/llm/test_http_llm_fetcher_positional_body.ts create mode 100644 test/features/llm/test_http_llm_fetcher_positional_parameters.ts create mode 100644 test/features/llm/test_http_llm_fetcher_positional_query.ts create mode 100644 test/features/llm/test_http_llm_fetcher_positional_query_and_body.ts diff --git a/package.json b/package.json index f7b04bf..71da428 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@samchon/openapi", - "version": "0.5.0-dev.20240824", + "version": "0.5.0-dev.20240905", "description": "OpenAPI definitions and converters for 'typia' and 'nestia'.", "main": "./lib/index.js", "module": "./lib/index.mjs", diff --git a/src/HttpLanguageModel.ts b/src/HttpLlm.ts similarity index 69% rename from src/HttpLanguageModel.ts rename to src/HttpLlm.ts index f8a240b..2d76154 100644 --- a/src/HttpLanguageModel.ts +++ b/src/HttpLlm.ts @@ -1,16 +1,17 @@ import { HttpMigration } from "./HttpMigration"; import { OpenApi } from "./OpenApi"; -import { LlmComposer } from "./converters/LlmComposer"; -import { LlmMerger } from "./converters/LlmMerger"; +import { HttpLlmConverter } from "./converters/HttpLlmConverter"; import { HttpLlmFunctionFetcher } from "./http/HttpLlmFunctionFetcher"; import { IHttpConnection } from "./structures/IHttpConnection"; import { IHttpLlmApplication } from "./structures/IHttpLlmApplication"; import { IHttpLlmFunction } from "./structures/IHttpLlmFunction"; import { IHttpMigrateApplication } from "./structures/IHttpMigrateApplication"; import { IHttpResponse } from "./structures/IHttpResponse"; +import { ILlmFunction } from "./structures/ILlmFunction"; import { ILlmSchema } from "./structures/ILlmSchema"; +import { LlmDataMerger } from "./utils/LlmDataMerger"; -export namespace HttpLanguageModel { +export namespace HttpLlm { export const application = < Schema extends ILlmSchema, Operation extends OpenApi.IOperation, @@ -25,7 +26,7 @@ export namespace HttpLanguageModel { document = HttpMigration.application( document as OpenApi.IDocument, ); - return LlmComposer.compose( + return HttpLlmConverter.compose( document as IHttpMigrateApplication, { keyword: options?.keyword ?? false, @@ -37,18 +38,18 @@ export namespace HttpLanguageModel { export const schema = (props: { components: OpenApi.IComponents; schema: OpenApi.IJsonSchema; - }): ILlmSchema | null => LlmComposer.schema(props); + }): ILlmSchema | null => HttpLlmConverter.schema(props); - export interface IExecutionProps { + export interface IFetchProps { /** * Document of the OpenAI function call schemas. */ - document: IHttpLlmApplication; + application: IHttpLlmApplication; /** - * Procedure schema to call. + * Function schema to call. */ - procedure: IHttpLlmFunction; + function: IHttpLlmFunction; /** * Connection info to the server. @@ -60,17 +61,18 @@ export namespace HttpLanguageModel { */ arguments: any[]; } - export const execute = (props: IExecutionProps): Promise => + export const execute = (props: IFetchProps): Promise => HttpLlmFunctionFetcher.execute(props); - - export const propagate = (props: IExecutionProps): Promise => + export const propagate = (props: IFetchProps): Promise => HttpLlmFunctionFetcher.propagate(props); export interface IMergeProps { - function: IHttpLlmFunction; + function: ILlmFunction; llm: unknown[]; human: unknown[]; } - export const merge = (props: IMergeProps): unknown[] => - LlmMerger.parameters(props); + export const mergeParameters = (props: IMergeProps): unknown[] => + LlmDataMerger.parameters(props); + export const mergeValue = (x: unknown, y: unknown): unknown => + LlmDataMerger.value(x, y); } diff --git a/src/HttpMigration.ts b/src/HttpMigration.ts index cd91fe1..b90f538 100644 --- a/src/HttpMigration.ts +++ b/src/HttpMigration.ts @@ -15,7 +15,7 @@ export namespace HttpMigration { ): IHttpMigrateApplication => MigrateConverter.convert(document); - export interface IProps { + export interface IFetchProps { connection: IHttpConnection; route: IHttpMigrateRoute; parameters: @@ -24,8 +24,8 @@ export namespace HttpMigration { query?: object | undefined; body?: object | undefined; } - export const request = (props: IProps): Promise => + export const request = (props: IFetchProps): Promise => HttpMigrateRouteFetcher.request(props); - export const propagate = (props: IProps): Promise => + export const propagate = (props: IFetchProps): Promise => HttpMigrateRouteFetcher.propagate(props); } diff --git a/src/converters/LlmComposer.ts b/src/converters/HttpLlmConverter.ts similarity index 98% rename from src/converters/LlmComposer.ts rename to src/converters/HttpLlmConverter.ts index 92872e3..07d2fb3 100644 --- a/src/converters/LlmComposer.ts +++ b/src/converters/HttpLlmConverter.ts @@ -4,12 +4,12 @@ import { IHttpLlmFunction } from "../structures/IHttpLlmFunction"; import { IHttpMigrateApplication } from "../structures/IHttpMigrateApplication"; import { IHttpMigrateRoute } from "../structures/IHttpMigrateRoute"; import { ILlmSchema } from "../structures/ILlmSchema"; +import { LlmSchemaSeparator } from "../utils/LlmSchemaSeparator"; import { LlmTypeChecker } from "../utils/LlmTypeChecker"; import { OpenApiTypeChecker } from "../utils/OpenApiTypeChecker"; -import { LlmSchemaSeparator } from "./LlmSchemaSeparator"; import { OpenApiV3Downgrader } from "./OpenApiV3Downgrader"; -export namespace LlmComposer { +export namespace HttpLlmConverter { export const compose = ( migrate: IHttpMigrateApplication, options: IHttpLlmApplication.IOptions, @@ -78,7 +78,7 @@ const composeFunction = (route: IHttpMigrateRoute): IHttpLlmFunction | null => { // CAST SCHEMA TYPES const cast = (s: OpenApi.IJsonSchema) => - LlmComposer.schema({ + HttpLlmConverter.schema({ components, schema: s, }); diff --git a/src/http/HttpLlmFunctionFetcher.ts b/src/http/HttpLlmFunctionFetcher.ts index ac2c715..7f08c35 100644 --- a/src/http/HttpLlmFunctionFetcher.ts +++ b/src/http/HttpLlmFunctionFetcher.ts @@ -8,14 +8,14 @@ import { HttpMigrateRouteFetcher } from "./HttpMigrateRouteFetcher"; export namespace HttpLlmFunctionFetcher { export interface IProps { /** - * Document of the OpenAI function call schemas. + * Application of the OpenAI function call schemas. */ - document: IHttpLlmApplication; + application: IHttpLlmApplication; /** - * Procedure schema to call. + * Function schema to call. */ - procedure: IHttpLlmFunction; + function: IHttpLlmFunction; /** * Connection info to the server. @@ -29,22 +29,32 @@ export namespace HttpLlmFunctionFetcher { } export const execute = async (props: IProps): Promise => - HttpMigrateRouteFetcher.request(getFetchArguments(props)); + HttpMigrateRouteFetcher.request(getFetchArguments("execute", props)); export const propagate = async (props: IProps): Promise => - HttpMigrateRouteFetcher.propagate(getFetchArguments(props)); + HttpMigrateRouteFetcher.propagate(getFetchArguments("propagate", props)); - const getFetchArguments = (props: IProps): HttpMigrateRouteFetcher.IProps => { - const route: IHttpMigrateRoute = props.procedure.route(); - if (props.document.options.keyword === true) { - const input: Pick< - HttpMigrateRouteFetcher.IProps, - "parameters" | "query" | "body" - > = props.arguments[0]; + const getFetchArguments = ( + from: string, + props: IProps, + ): HttpMigrateRouteFetcher.IProps => { + const route: IHttpMigrateRoute = props.function.route(); + if (props.application.options.keyword === true) { + const input: Record = props.arguments[0]; + const valid: boolean = + props.arguments.length === 1 && + typeof input === "object" && + input !== null; + if (valid === false) + throw new Error( + `Error on HttpLlmFunctionFetcher.${from}(): keyworded arguments must be an object`, + ); return { connection: props.connection, route, - parameters: input.parameters, + parameters: Object.fromEntries( + route.parameters.map((p) => [p.key, input[p.key]] as const), + ), query: input.query, body: input.body, }; diff --git a/src/index.ts b/src/index.ts index ba5e544..2537442 100644 --- a/src/index.ts +++ b/src/index.ts @@ -17,5 +17,5 @@ export * from "./OpenApi"; export * from "./SwaggerV2"; export * from "./OpenApiV3"; export * from "./OpenApiV3_1"; -export * from "./HttpLanguageModel"; +export * from "./HttpLlm"; export * from "./HttpMigration"; diff --git a/src/converters/LlmMerger.ts b/src/utils/LlmDataMerger.ts similarity index 96% rename from src/converters/LlmMerger.ts rename to src/utils/LlmDataMerger.ts index ea966d6..00729a8 100644 --- a/src/converters/LlmMerger.ts +++ b/src/utils/LlmDataMerger.ts @@ -6,7 +6,7 @@ import { ILlmFunction } from "../structures/ILlmFunction"; * * @author Samchon */ -export namespace LlmMerger { +export namespace LlmDataMerger { /** * Properties of {@link parameters} function. */ @@ -36,7 +36,7 @@ export namespace LlmMerger { * and human. * * In that case, you can combine both LLM and human composed arguments into one - * by utilizing this {@link LlmMerger.parameters} function, referencing + * by utilizing this {@link LlmDataMerger.parameters} function, referencing * the target function metadata {@link IOpenAiFunction.separated}. * * @param props Properties to combine LLM and human arguments with metadata. diff --git a/src/converters/LlmSchemaSeparator.ts b/src/utils/LlmSchemaSeparator.ts similarity index 98% rename from src/converters/LlmSchemaSeparator.ts rename to src/utils/LlmSchemaSeparator.ts index fe092f7..66bf824 100644 --- a/src/converters/LlmSchemaSeparator.ts +++ b/src/utils/LlmSchemaSeparator.ts @@ -1,6 +1,6 @@ import { IHttpLlmFunction } from "../structures/IHttpLlmFunction"; import { ILlmSchema } from "../structures/ILlmSchema"; -import { LlmTypeChecker } from "../utils/LlmTypeChecker"; +import { LlmTypeChecker } from "./LlmTypeChecker"; export namespace LlmSchemaSeparator { export interface IProps { diff --git a/test/controllers/AppController.ts b/test/controllers/AppController.ts index 351514e..5a8b718 100644 --- a/test/controllers/AppController.ts +++ b/test/controllers/AppController.ts @@ -12,7 +12,8 @@ import { tags } from "typia"; export class AppController { @TypedRoute.Get(":index/:level/:optimal/parameters") public parameters( - @TypedParam("index") index: string, + @TypedParam("index") + index: string & tags.Format<"uri"> & tags.ContentMediaType<"text/html">, @TypedParam("level") level: number, @TypedParam("optimal") optimal: boolean, ) { @@ -21,39 +22,36 @@ export class AppController { @TypedRoute.Get(":index/:level/:optimal/query") public query( - @TypedParam("index") index: string, + @TypedParam("index") + index: string & tags.Format<"uri"> & tags.ContentMediaType<"text/html">, @TypedParam("level") level: number, @TypedParam("optimal") optimal: boolean, - @TypedQuery() - query: IQuery, + @TypedQuery() query: IQuery, ) { return { index, level, optimal, query }; } @TypedRoute.Post(":index/:level/:optimal/body") public body( - @TypedParam("index") index: string, + @TypedParam("index") + index: string & tags.Format<"uri"> & tags.ContentMediaType<"text/html">, @TypedParam("level") level: number, @TypedParam("optimal") optimal: boolean, - @TypedBody() - body: IBody, + @TypedBody() body: IBody, ) { return { index, level, optimal, body }; } @TypedRoute.Post(":index/:level/:optimal/query/body") public query_body( - @TypedParam("index") index: string, + @TypedParam("index") + index: string & tags.Format<"uri"> & tags.ContentMediaType<"text/html">, @TypedParam("level") level: number, @TypedParam("optimal") optimal: boolean, @Query("thumbnail") thumbnail: string & tags.Format<"uri"> & tags.ContentMediaType<"image/*">, - @TypedQuery() - query: { - summary: string; - }, - @TypedBody() - body: IBody, + @TypedQuery() query: { summary: string }, + @TypedBody() body: IBody, ) { return { index, @@ -69,7 +67,8 @@ export class AppController { @TypedRoute.Post(":index/:level/:optimal/multipart") public query_multipart( - @TypedParam("index") index: string, + @TypedParam("index") + index: string & tags.Format<"uri"> & tags.ContentMediaType<"text/html">, @TypedParam("level") level: number, @TypedParam("optimal") optimal: boolean, @TypedQuery() query: IQuery, diff --git a/test/features/llm/test_http_llm_application_keyword.ts b/test/features/llm/test_http_llm_application_keyword.ts index b4d705c..9127744 100644 --- a/test/features/llm/test_http_llm_application_keyword.ts +++ b/test/features/llm/test_http_llm_application_keyword.ts @@ -1,6 +1,6 @@ import { TestValidator } from "@nestia/e2e"; import { - HttpLanguageModel, + HttpLlm, IHttpLlmApplication, IHttpMigrateRoute, ILlmSchema, @@ -12,12 +12,9 @@ import swagger from "../../swagger.json"; export const test_http_llm_application_keyword = (): void => { const document: OpenApi.IDocument = OpenApi.convert(swagger as any); - const application: IHttpLlmApplication = HttpLanguageModel.application( - document, - { - keyword: true, - }, - ); + const application: IHttpLlmApplication = HttpLlm.application(document, { + keyword: true, + }); for (const func of application.functions) { const route: IHttpMigrateRoute = func.route(); TestValidator.equals("length")(1)(func.parameters.length); diff --git a/test/features/llm/test_http_llm_application_positional.ts b/test/features/llm/test_http_llm_application_positional.ts index 775e438..074ef13 100644 --- a/test/features/llm/test_http_llm_application_positional.ts +++ b/test/features/llm/test_http_llm_application_positional.ts @@ -1,6 +1,6 @@ import { TestValidator } from "@nestia/e2e"; import { - HttpLanguageModel, + HttpLlm, IHttpLlmApplication, IHttpMigrateRoute, OpenApi, @@ -10,12 +10,9 @@ import swagger from "../../swagger.json"; export const test_http_llm_application_positional = (): void => { const document: OpenApi.IDocument = OpenApi.convert(swagger as any); - const application: IHttpLlmApplication = HttpLanguageModel.application( - document, - { - keyword: false, - }, - ); + const application: IHttpLlmApplication = HttpLlm.application(document, { + keyword: false, + }); for (const func of application.functions) { const route: IHttpMigrateRoute = func.route(); TestValidator.equals("length")(func.parameters.length)( diff --git a/test/features/llm/test_http_llm_fetcher_keyword_body.ts b/test/features/llm/test_http_llm_fetcher_keyword_body.ts new file mode 100644 index 0000000..a6c03bd --- /dev/null +++ b/test/features/llm/test_http_llm_fetcher_keyword_body.ts @@ -0,0 +1,53 @@ +import { TestValidator } from "@nestia/e2e"; +import { + HttpLlm, + IHttpConnection, + IHttpLlmApplication, + IHttpLlmFunction, + IHttpResponse, + LlmTypeChecker, + OpenApi, +} from "@samchon/openapi"; + +import swagger from "../../swagger.json"; + +export const test_http_llm_fetcher_keyword_body = async ( + connection: IHttpConnection, +): Promise => { + const document: OpenApi.IDocument = OpenApi.convert(swagger as any); + const application: IHttpLlmApplication = HttpLlm.application(document, { + keyword: true, + separate: (schema) => + LlmTypeChecker.isString(schema) && !!schema.contentMediaType, + }); + 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"); + + const response: IHttpResponse = await HttpLlm.propagate({ + connection, + application, + function: func, + arguments: HttpLlm.mergeParameters({ + function: func, + llm: [ + { + level: 123, + optimal: true, + body: { + title: "some title", + body: "some body", + draft: false, + }, + }, + ], + human: [ + { + index: "https://some.url/index.html", + }, + ], + }), + }); + TestValidator.equals("response.status")(response.status)(201); +}; diff --git a/test/features/llm/test_http_llm_fetcher_keyword_parameters.ts b/test/features/llm/test_http_llm_fetcher_keyword_parameters.ts new file mode 100644 index 0000000..50d1a26 --- /dev/null +++ b/test/features/llm/test_http_llm_fetcher_keyword_parameters.ts @@ -0,0 +1,49 @@ +import { TestValidator } from "@nestia/e2e"; +import { + HttpLlm, + IHttpConnection, + IHttpLlmApplication, + IHttpLlmFunction, + IHttpResponse, + LlmTypeChecker, + OpenApi, +} from "@samchon/openapi"; + +import swagger from "../../swagger.json"; + +export const test_http_llm_fetcher_keyword_parameters = async ( + connection: IHttpConnection, +): Promise => { + const document: OpenApi.IDocument = OpenApi.convert(swagger as any); + const application: IHttpLlmApplication = HttpLlm.application(document, { + keyword: true, + separate: (schema) => + LlmTypeChecker.isString(schema) && !!schema.contentMediaType, + }); + const func: IHttpLlmFunction | undefined = application.functions.find( + (f) => + f.path === "/{index}/{level}/{optimal}/parameters" && f.method === "get", + ); + if (func === undefined) throw new Error("Function not found"); + + const response: IHttpResponse = await HttpLlm.propagate({ + connection, + application, + function: func, + arguments: HttpLlm.mergeParameters({ + function: func, + llm: [ + { + level: 123, + optimal: true, + }, + ], + human: [ + { + index: "https://some.url/index.html", + }, + ], + }), + }); + TestValidator.equals("response.status")(response.status)(200); +}; diff --git a/test/features/llm/test_http_llm_fetcher_keyword_query.ts b/test/features/llm/test_http_llm_fetcher_keyword_query.ts new file mode 100644 index 0000000..cd81620 --- /dev/null +++ b/test/features/llm/test_http_llm_fetcher_keyword_query.ts @@ -0,0 +1,54 @@ +import { TestValidator } from "@nestia/e2e"; +import { + HttpLlm, + IHttpConnection, + IHttpLlmApplication, + IHttpLlmFunction, + IHttpResponse, + LlmTypeChecker, + OpenApi, +} from "@samchon/openapi"; + +import swagger from "../../swagger.json"; + +export const test_http_llm_fetcher_keyword_query = async ( + connection: IHttpConnection, +): Promise => { + const document: OpenApi.IDocument = OpenApi.convert(swagger as any); + const application: IHttpLlmApplication = HttpLlm.application(document, { + keyword: true, + separate: (schema) => + LlmTypeChecker.isString(schema) && !!schema.contentMediaType, + }); + 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"); + + const response: IHttpResponse = await HttpLlm.propagate({ + connection, + application, + function: func, + arguments: HttpLlm.mergeParameters({ + function: func, + llm: [ + { + level: 123, + optimal: true, + query: { + summary: "some summary", + }, + }, + ], + human: [ + { + index: "https://some.url/index.html", + query: { + thumbnail: "https://some.url/file.jpg", + }, + }, + ], + }), + }); + TestValidator.equals("response.status")(response.status)(200); +}; diff --git a/test/features/llm/test_http_llm_fetcher_keyword_query_and_body.ts b/test/features/llm/test_http_llm_fetcher_keyword_query_and_body.ts new file mode 100644 index 0000000..2dcf74e --- /dev/null +++ b/test/features/llm/test_http_llm_fetcher_keyword_query_and_body.ts @@ -0,0 +1,60 @@ +import { TestValidator } from "@nestia/e2e"; +import { + HttpLlm, + IHttpConnection, + IHttpLlmApplication, + IHttpLlmFunction, + IHttpResponse, + LlmTypeChecker, + OpenApi, +} from "@samchon/openapi"; + +import swagger from "../../swagger.json"; + +export const test_http_llm_fetcher_keyword_query_and_body = async ( + connection: IHttpConnection, +): Promise => { + const document: OpenApi.IDocument = OpenApi.convert(swagger as any); + const application: IHttpLlmApplication = HttpLlm.application(document, { + keyword: true, + separate: (schema) => + LlmTypeChecker.isString(schema) && !!schema.contentMediaType, + }); + const func: IHttpLlmFunction | undefined = application.functions.find( + (f) => + f.path === "/{index}/{level}/{optimal}/query/body" && f.method === "post", + ); + if (func === undefined) throw new Error("Function not found"); + + const response: IHttpResponse = await HttpLlm.propagate({ + connection, + application, + function: func, + arguments: HttpLlm.mergeParameters({ + function: func, + llm: [ + { + level: 123, + optimal: true, + query: { + summary: "some summary", + }, + body: { + title: "some title", + body: "some body", + draft: false, + }, + }, + ], + human: [ + { + index: "https://some.url/index.html", + query: { + thumbnail: "https://some.url/file.jpg", + }, + }, + ], + }), + }); + TestValidator.equals("response.status")(response.status)(201); +}; diff --git a/test/features/llm/test_http_llm_fetcher_positional_body.ts b/test/features/llm/test_http_llm_fetcher_positional_body.ts new file mode 100644 index 0000000..7932fac --- /dev/null +++ b/test/features/llm/test_http_llm_fetcher_positional_body.ts @@ -0,0 +1,47 @@ +import { TestValidator } from "@nestia/e2e"; +import { + HttpLlm, + IHttpConnection, + IHttpLlmApplication, + IHttpLlmFunction, + IHttpResponse, + LlmTypeChecker, + OpenApi, +} from "@samchon/openapi"; + +import swagger from "../../swagger.json"; + +export const test_http_llm_fetcher_positional_body = async ( + connection: IHttpConnection, +): Promise => { + const document: OpenApi.IDocument = OpenApi.convert(swagger as any); + const application: IHttpLlmApplication = HttpLlm.application(document, { + keyword: false, + separate: (schema) => + LlmTypeChecker.isString(schema) && !!schema.contentMediaType, + }); + 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"); + + const response: IHttpResponse = await HttpLlm.propagate({ + connection, + application, + function: func, + arguments: HttpLlm.mergeParameters({ + function: func, + llm: [ + 123, + true, + { + title: "some title", + body: "some body", + draft: false, + }, + ], + human: ["https://some.url/index.html"], + }), + }); + TestValidator.equals("response.status")(response.status)(201); +}; diff --git a/test/features/llm/test_http_llm_fetcher_positional_parameters.ts b/test/features/llm/test_http_llm_fetcher_positional_parameters.ts new file mode 100644 index 0000000..03d19b6 --- /dev/null +++ b/test/features/llm/test_http_llm_fetcher_positional_parameters.ts @@ -0,0 +1,40 @@ +import { TestValidator } from "@nestia/e2e"; +import { + HttpLlm, + IHttpConnection, + IHttpLlmApplication, + IHttpLlmFunction, + IHttpResponse, + LlmTypeChecker, + OpenApi, +} from "@samchon/openapi"; + +import swagger from "../../swagger.json"; + +export const test_http_llm_fetcher_keyword_parameters = async ( + connection: IHttpConnection, +): Promise => { + const document: OpenApi.IDocument = OpenApi.convert(swagger as any); + const application: IHttpLlmApplication = HttpLlm.application(document, { + keyword: false, + separate: (schema) => + LlmTypeChecker.isString(schema) && !!schema.contentMediaType, + }); + const func: IHttpLlmFunction | undefined = application.functions.find( + (f) => + f.path === "/{index}/{level}/{optimal}/parameters" && f.method === "get", + ); + if (func === undefined) throw new Error("Function not found"); + + const response: IHttpResponse = await HttpLlm.propagate({ + connection, + application, + function: func, + arguments: HttpLlm.mergeParameters({ + function: func, + llm: [123, true], + human: ["https://some.url/index.html"], + }), + }); + TestValidator.equals("response.status")(response.status)(200); +}; diff --git a/test/features/llm/test_http_llm_fetcher_positional_query.ts b/test/features/llm/test_http_llm_fetcher_positional_query.ts new file mode 100644 index 0000000..2bbfb23 --- /dev/null +++ b/test/features/llm/test_http_llm_fetcher_positional_query.ts @@ -0,0 +1,50 @@ +import { TestValidator } from "@nestia/e2e"; +import { + HttpLlm, + IHttpConnection, + IHttpLlmApplication, + IHttpLlmFunction, + IHttpResponse, + LlmTypeChecker, + OpenApi, +} from "@samchon/openapi"; + +import swagger from "../../swagger.json"; + +export const test_http_llm_fetcher_keyword_query = async ( + connection: IHttpConnection, +): Promise => { + const document: OpenApi.IDocument = OpenApi.convert(swagger as any); + const application: IHttpLlmApplication = HttpLlm.application(document, { + keyword: false, + separate: (schema) => + LlmTypeChecker.isString(schema) && !!schema.contentMediaType, + }); + 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"); + + const response: IHttpResponse = await HttpLlm.propagate({ + connection, + application, + function: func, + arguments: HttpLlm.mergeParameters({ + function: func, + llm: [ + 123, + true, + { + summary: "some summary", + }, + ], + human: [ + "https://some.url/index.html", + { + thumbnail: "https://some.url/file.jpg", + }, + ], + }), + }); + TestValidator.equals("response.status")(response.status)(200); +}; diff --git a/test/features/llm/test_http_llm_fetcher_positional_query_and_body.ts b/test/features/llm/test_http_llm_fetcher_positional_query_and_body.ts new file mode 100644 index 0000000..147d5ba --- /dev/null +++ b/test/features/llm/test_http_llm_fetcher_positional_query_and_body.ts @@ -0,0 +1,56 @@ +import { TestValidator } from "@nestia/e2e"; +import { + HttpLlm, + IHttpConnection, + IHttpLlmApplication, + IHttpLlmFunction, + IHttpResponse, + LlmTypeChecker, + OpenApi, +} from "@samchon/openapi"; + +import swagger from "../../swagger.json"; + +export const test_http_llm_fetcher_keyword_query_and_body = async ( + connection: IHttpConnection, +): Promise => { + const document: OpenApi.IDocument = OpenApi.convert(swagger as any); + const application: IHttpLlmApplication = HttpLlm.application(document, { + keyword: false, + separate: (schema) => + LlmTypeChecker.isString(schema) && !!schema.contentMediaType, + }); + const func: IHttpLlmFunction | undefined = application.functions.find( + (f) => + f.path === "/{index}/{level}/{optimal}/query/body" && f.method === "post", + ); + if (func === undefined) throw new Error("Function not found"); + + const response: IHttpResponse = await HttpLlm.propagate({ + connection, + application, + function: func, + arguments: HttpLlm.mergeParameters({ + function: func, + llm: [ + 123, + true, + { + summary: "some summary", + }, + { + title: "some title", + body: "some body", + draft: false, + }, + ], + human: [ + "https://some.url/index.html", + { + thumbnail: "https://some.url/file.jpg", + }, + ], + }), + }); + TestValidator.equals("response.status")(response.status)(201); +}; diff --git a/test/features/llm/test_llm_merge_parameters.ts b/test/features/llm/test_llm_merge_parameters.ts index f6e4b61..759a6a6 100644 --- a/test/features/llm/test_llm_merge_parameters.ts +++ b/test/features/llm/test_llm_merge_parameters.ts @@ -1,9 +1,9 @@ import { TestValidator } from "@nestia/e2e"; -import { LlmMerger } from "@samchon/openapi/lib/converters/LlmMerger"; +import { HttpLlm } from "@samchon/openapi"; export const test_llm_merge_parameters = (): void => { TestValidator.equals("atomics")( - LlmMerger.parameters({ + HttpLlm.mergeParameters({ function: { name: "test", parameters: [ diff --git a/test/features/llm/test_llm_merge_value.ts b/test/features/llm/test_llm_merge_value.ts index 4f3c1f9..e7239ea 100644 --- a/test/features/llm/test_llm_merge_value.ts +++ b/test/features/llm/test_llm_merge_value.ts @@ -1,12 +1,12 @@ import { TestValidator } from "@nestia/e2e"; -import { LlmMerger } from "@samchon/openapi/lib/converters/LlmMerger"; +import { HttpLlm } from "@samchon/openapi"; export const test_llm_merge_parameters = (): void => { - TestValidator.equals("number")(LlmMerger.value(1, 2))(2); - TestValidator.equals("nullable")(LlmMerger.value(0, null))(0); - TestValidator.equals("optional")(LlmMerger.value(0, undefined))(0); + TestValidator.equals("number")(HttpLlm.mergeValue(1, 2))(2); + TestValidator.equals("nullable")(HttpLlm.mergeValue(0, null))(0); + TestValidator.equals("optional")(HttpLlm.mergeValue(0, undefined))(0); TestValidator.equals("object")( - LlmMerger.value( + HttpLlm.mergeValue( { a: "A", array: [1, 2, 3], @@ -37,7 +37,7 @@ export const test_llm_merge_parameters = (): void => { object: { x: "X", y: "Y" }, }); TestValidator.equals("membership")( - LlmMerger.value( + HttpLlm.mergeValue( { name: "Samchon", email: "samchon.github@gmail.com", diff --git a/test/features/llm/test_llm_schema_object.ts b/test/features/llm/test_llm_schema_object.ts index 02023e9..0847f2e 100644 --- a/test/features/llm/test_llm_schema_object.ts +++ b/test/features/llm/test_llm_schema_object.ts @@ -1,10 +1,10 @@ import { TestValidator } from "@nestia/e2e"; -import { HttpLanguageModel, ILlmSchema } from "@samchon/openapi"; +import { HttpLlm, ILlmSchema } from "@samchon/openapi"; import typia, { IJsonApplication, tags } from "typia"; export const test_llm_schema_object = (): void => { const app: IJsonApplication = typia.json.application<[First]>(); - const schema: ILlmSchema | null = HttpLanguageModel.schema({ + const schema: ILlmSchema | null = HttpLlm.schema({ components: app.components, schema: app.schemas[0], }); diff --git a/test/features/llm/test_llm_schema_oneof.ts b/test/features/llm/test_llm_schema_oneof.ts index f4fa154..d03153c 100644 --- a/test/features/llm/test_llm_schema_oneof.ts +++ b/test/features/llm/test_llm_schema_oneof.ts @@ -1,11 +1,11 @@ import { TestValidator } from "@nestia/e2e"; -import { HttpLanguageModel, ILlmSchema } from "@samchon/openapi"; +import { HttpLlm, ILlmSchema } from "@samchon/openapi"; import typia, { IJsonApplication } from "typia"; export const test_llm_schema_oneof = (): void => { const app: IJsonApplication = typia.json.application<[Circle | Triangle | Rectangle]>(); - const casted: ILlmSchema | null = HttpLanguageModel.schema({ + const casted: ILlmSchema | null = HttpLlm.schema({ components: app.components, schema: app.schemas[0], }); diff --git a/test/features/llm/test_llm_schema_separate_array.ts b/test/features/llm/test_llm_schema_separate_array.ts index c51bd4c..eb7fc1f 100644 --- a/test/features/llm/test_llm_schema_separate_array.ts +++ b/test/features/llm/test_llm_schema_separate_array.ts @@ -1,11 +1,6 @@ import { TestValidator } from "@nestia/e2e"; -import { - HttpLanguageModel, - ILlmSchema, - LlmTypeChecker, - OpenApi, -} from "@samchon/openapi"; -import { LlmSchemaSeparator } from "@samchon/openapi/lib/converters/LlmSchemaSeparator"; +import { HttpLlm, ILlmSchema, LlmTypeChecker, OpenApi } from "@samchon/openapi"; +import { LlmSchemaSeparator } from "@samchon/openapi/lib/utils/LlmSchemaSeparator"; import typia, { tags } from "typia"; export const test_llm_schema_separate_array = (): void => { @@ -34,7 +29,7 @@ const schema = (props: { components: OpenApi.IComponents; schemas: OpenApi.IJsonSchema[]; }): ILlmSchema => { - const schema: ILlmSchema | null = HttpLanguageModel.schema({ + const schema: ILlmSchema | null = HttpLlm.schema({ components: props.components, schema: props.schemas[0], }); diff --git a/test/features/llm/test_llm_schema_separate_nested.ts b/test/features/llm/test_llm_schema_separate_nested.ts index 7fe5921..816d8a2 100644 --- a/test/features/llm/test_llm_schema_separate_nested.ts +++ b/test/features/llm/test_llm_schema_separate_nested.ts @@ -1,11 +1,6 @@ import { TestValidator } from "@nestia/e2e"; -import { - HttpLanguageModel, - ILlmSchema, - LlmTypeChecker, - OpenApi, -} from "@samchon/openapi"; -import { LlmSchemaSeparator } from "@samchon/openapi/lib/converters/LlmSchemaSeparator"; +import { HttpLlm, ILlmSchema, LlmTypeChecker, OpenApi } from "@samchon/openapi"; +import { LlmSchemaSeparator } from "@samchon/openapi/lib/utils/LlmSchemaSeparator"; import typia, { tags } from "typia"; export const test_llm_schema_separate_nested = (): void => { @@ -50,7 +45,7 @@ const schema = (props: { components: OpenApi.IComponents; schemas: OpenApi.IJsonSchema[]; }): ILlmSchema => { - const schema: ILlmSchema | null = HttpLanguageModel.schema({ + const schema: ILlmSchema | null = HttpLlm.schema({ components: props.components, schema: props.schemas[0], }); diff --git a/test/features/llm/test_llm_schema_separate_object.ts b/test/features/llm/test_llm_schema_separate_object.ts index 2c58b1c..abe7802 100644 --- a/test/features/llm/test_llm_schema_separate_object.ts +++ b/test/features/llm/test_llm_schema_separate_object.ts @@ -1,11 +1,6 @@ import { TestValidator } from "@nestia/e2e"; -import { - HttpLanguageModel, - ILlmSchema, - LlmTypeChecker, - OpenApi, -} from "@samchon/openapi"; -import { LlmSchemaSeparator } from "@samchon/openapi/lib/converters/LlmSchemaSeparator"; +import { HttpLlm, ILlmSchema, LlmTypeChecker, OpenApi } from "@samchon/openapi"; +import { LlmSchemaSeparator } from "@samchon/openapi/lib/utils/LlmSchemaSeparator"; import typia, { tags } from "typia"; export const test_llm_schema_separate_object = (): void => { @@ -34,7 +29,7 @@ const schema = (props: { components: OpenApi.IComponents; schemas: OpenApi.IJsonSchema[]; }): ILlmSchema => { - const schema: ILlmSchema | null = HttpLanguageModel.schema({ + const schema: ILlmSchema | null = HttpLlm.schema({ components: props.components, schema: props.schemas[0], }); diff --git a/test/features/llm/test_llm_schema_separate_string.ts b/test/features/llm/test_llm_schema_separate_string.ts index 87fafd7..ba4c6e1 100644 --- a/test/features/llm/test_llm_schema_separate_string.ts +++ b/test/features/llm/test_llm_schema_separate_string.ts @@ -1,6 +1,6 @@ import { TestValidator } from "@nestia/e2e"; import { ILlmSchema, LlmTypeChecker } from "@samchon/openapi"; -import { LlmSchemaSeparator } from "@samchon/openapi/lib/converters/LlmSchemaSeparator"; +import { LlmSchemaSeparator } from "@samchon/openapi/lib/utils/LlmSchemaSeparator"; export const test_schema_separate_string = (): void => { const separator = LlmSchemaSeparator.schema( diff --git a/test/features/migrate/test_http_migrate_fetch_body.ts b/test/features/migrate/test_http_migrate_fetch_body.ts index 71866ce..57fbecf 100644 --- a/test/features/migrate/test_http_migrate_fetch_body.ts +++ b/test/features/migrate/test_http_migrate_fetch_body.ts @@ -24,7 +24,7 @@ export const test_http_migrate_fetch_body = async ( connection, route, parameters: { - index: "string", + index: "https://some.url/index.html", level: 123, optimal: true, }, diff --git a/test/features/migrate/test_http_migrate_fetch_keyword_parameters.ts b/test/features/migrate/test_http_migrate_fetch_keyword_parameters.ts index 55f3833..8c94ce9 100644 --- a/test/features/migrate/test_http_migrate_fetch_keyword_parameters.ts +++ b/test/features/migrate/test_http_migrate_fetch_keyword_parameters.ts @@ -23,7 +23,7 @@ export const test_http_migrate_fetch_keyword_parameters = async ( connection, route, parameters: { - index: "three", + index: "https://some.url/index.html", level: 2, optimal: true, }, diff --git a/test/features/migrate/test_http_migrate_fetch_multipart.ts b/test/features/migrate/test_http_migrate_fetch_multipart.ts index b03556b..cfbae26 100644 --- a/test/features/migrate/test_http_migrate_fetch_multipart.ts +++ b/test/features/migrate/test_http_migrate_fetch_multipart.ts @@ -23,7 +23,7 @@ export const test_http_migrate_fetch_multipart = async ( connection, route, parameters: { - index: "three", + index: "https://some.url/index.html", level: 2, optimal: true, }, diff --git a/test/features/migrate/test_http_migrate_fetch_positional_parameters.ts b/test/features/migrate/test_http_migrate_fetch_positional_parameters.ts index 02fa1ae..5dcf8a6 100644 --- a/test/features/migrate/test_http_migrate_fetch_positional_parameters.ts +++ b/test/features/migrate/test_http_migrate_fetch_positional_parameters.ts @@ -22,6 +22,6 @@ export const test_http_migrate_fetch_positional_parameters = async ( await HttpMigration.request({ connection, route, - parameters: ["three", 2, true], + parameters: ["https://some.url/index.html", 2, true], }); }; diff --git a/test/features/migrate/test_http_migrate_fetch_propagate.ts b/test/features/migrate/test_http_migrate_fetch_propagate.ts index b757b87..06755a5 100644 --- a/test/features/migrate/test_http_migrate_fetch_propagate.ts +++ b/test/features/migrate/test_http_migrate_fetch_propagate.ts @@ -24,7 +24,7 @@ export const test_http_migrate_fetch_propagate = async ( const response: IHttpResponse = await HttpMigration.propagate({ connection, route, - parameters: ["three", "two", "one"], + parameters: ["https://some.url/index.html", "two", "one"], }); TestValidator.equals("status")(response.status)(400); }; diff --git a/test/features/migrate/test_http_migrate_fetch_query.ts b/test/features/migrate/test_http_migrate_fetch_query.ts index ec1c694..46ad613 100644 --- a/test/features/migrate/test_http_migrate_fetch_query.ts +++ b/test/features/migrate/test_http_migrate_fetch_query.ts @@ -22,7 +22,7 @@ export const test_http_migrate_fetch_query = async ( connection, route, parameters: { - index: "string", + index: "https://some.url/index.html", level: 123, optimal: true, }, diff --git a/test/features/migrate/test_http_migrate_fetch_query_and_body.ts b/test/features/migrate/test_http_migrate_fetch_query_and_body.ts index 58fe43f..453a623 100644 --- a/test/features/migrate/test_http_migrate_fetch_query_and_body.ts +++ b/test/features/migrate/test_http_migrate_fetch_query_and_body.ts @@ -23,7 +23,7 @@ export const test_http_migrate_fetch_query_and_body = async ( connection, route, parameters: { - index: "string", + index: "https://some.url/index.html", level: 123, optimal: true, }, diff --git a/test/swagger.json b/test/swagger.json index 85d7a7d..efdd7be 100644 --- a/test/swagger.json +++ b/test/swagger.json @@ -23,7 +23,9 @@ "name": "index", "in": "path", "schema": { - "type": "string" + "type": "string", + "format": "uri", + "contentMediaType": "text/html" }, "required": true }, @@ -66,7 +68,9 @@ "name": "index", "in": "path", "schema": { - "type": "string" + "type": "string", + "format": "uri", + "contentMediaType": "text/html" }, "required": true }, @@ -127,7 +131,9 @@ "name": "index", "in": "path", "schema": { - "type": "string" + "type": "string", + "format": "uri", + "contentMediaType": "text/html" }, "required": true }, @@ -180,7 +186,9 @@ "name": "index", "in": "path", "schema": { - "type": "string" + "type": "string", + "format": "uri", + "contentMediaType": "text/html" }, "required": true }, @@ -266,7 +274,9 @@ "name": "index", "in": "path", "schema": { - "type": "string" + "type": "string", + "format": "uri", + "contentMediaType": "text/html" }, "required": true }, From a2840c80ed5ca9356ffc7761d7fd3c9a9e25b17f Mon Sep 17 00:00:00 2001 From: Jeongho Nam Date: Fri, 6 Sep 2024 20:06:28 +0900 Subject: [PATCH 08/16] Add `ILlmApplication` type. --- package.json | 2 +- src/converters/HttpLlmConverter.ts | 2 +- src/index.ts | 1 + src/structures/IHttpLlmApplication.ts | 2 +- src/structures/ILlmApplication.ts | 48 +++++++++++++++++++++++++++ src/structures/ILlmSchema.ts | 21 +++++++----- 6 files changed, 65 insertions(+), 11 deletions(-) create mode 100644 src/structures/ILlmApplication.ts diff --git a/package.json b/package.json index 71da428..d3a5906 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@samchon/openapi", - "version": "0.5.0-dev.20240905", + "version": "0.5.0-dev.20240906-2", "description": "OpenAPI definitions and converters for 'typia' and 'nestia'.", "main": "./lib/index.js", "module": "./lib/index.mjs", diff --git a/src/converters/HttpLlmConverter.ts b/src/converters/HttpLlmConverter.ts index 07d2fb3..c863d74 100644 --- a/src/converters/HttpLlmConverter.ts +++ b/src/converters/HttpLlmConverter.ts @@ -152,7 +152,7 @@ const composeFunction = ? (OpenApiV3Downgrader.downgradeSchema({ original: {}, downgraded: {}, - })(output) as ILlmSchema) + })(output as any) as ILlmSchema) : undefined, description: (() => { if (operation.summary && operation.description) { diff --git a/src/index.ts b/src/index.ts index 2537442..a2d2ec3 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,6 +5,7 @@ export * from "./structures/IHttpLlmFunction"; export * from "./structures/IHttpMigrateRoute"; export * from "./structures/IHttpMigrateApplication"; export * from "./structures/IHttpResponse"; +export * from "./structures/ILlmApplication"; export * from "./structures/ILlmSchema"; // UTILS diff --git a/src/structures/IHttpLlmApplication.ts b/src/structures/IHttpLlmApplication.ts index 5a2d8a3..c974f48 100644 --- a/src/structures/IHttpLlmApplication.ts +++ b/src/structures/IHttpLlmApplication.ts @@ -34,7 +34,7 @@ export interface IHttpLlmApplication< * Options for the document. * * Adjusted options when composing the document through - * {@link OpenApi.llm} function. + * {@link HttpLlm.application} function. */ options: IHttpLlmApplication.IOptions; } diff --git a/src/structures/ILlmApplication.ts b/src/structures/ILlmApplication.ts new file mode 100644 index 0000000..014b8c7 --- /dev/null +++ b/src/structures/ILlmApplication.ts @@ -0,0 +1,48 @@ +import { ILlmFunction } from "./ILlmFunction"; +import { ILlmSchema } from "./ILlmSchema"; + +export interface ILlmApplication { + /** + * List of function metadata. + * + * List of function metadata that can be used for the LLM function call. + */ + functions: ILlmFunction[]; + + /** + * Options for the document. + */ + options: ILlmApplication.IOptions; +} +export namespace ILlmApplication { + export interface IOptions { + /** + * Separator function for the parameters. + * + * When composing parameter arguments through LLM function call, + * there can be a case that some parameters must be composed by human, + * or LLM cannot understand the parameter. For example, if the + * parameter type has configured + * {@link ILlmSchema.IString.contentMediaType} which indicates file + * uploading, it must be composed by human, not by LLM + * (Large Language Model). + * + * In that case, if you configure this property with a function that + * predicating whether the schema value must be composed by human or + * not, the parameters would be separated into two parts. + * + * - {@link ILlmFunction.separated.llm} + * - {@link ILlmFunction.separated.human} + * + * When writing the function, note that returning value `true` means + * to be a human composing the value, and `false` means to LLM + * composing the value. Also, when predicating the schema, it would + * better to utilize the {@link LlmTypeChecker} features. + * + * @param schema Schema to be separated. + * @returns Whether the schema value must be composed by human or not. + * @default null + */ + separate: null | ((schema: Schema) => boolean); + } +} diff --git a/src/structures/ILlmSchema.ts b/src/structures/ILlmSchema.ts index f35e78d..ac388be 100644 --- a/src/structures/ILlmSchema.ts +++ b/src/structures/ILlmSchema.ts @@ -35,12 +35,12 @@ export namespace ILlmSchema { /** * Default value. */ - default?: boolean; + default?: boolean | null; /** * Enumeration values. */ - enum?: boolean[]; + enum?: Array; } /** @@ -52,14 +52,14 @@ export namespace ILlmSchema { * * @type int64 */ - default?: number; + default?: number | null; /** * Enumeration values. * * @type int64 */ - enum?: number[]; + enum?: Array; /** * Minimum value restriction. @@ -111,12 +111,12 @@ export namespace ILlmSchema { /** * Default value. */ - default?: number; + default?: number | null; /** * Enumeration values. */ - enum?: number[]; + enum?: Array; /** * Minimum value restriction. @@ -163,12 +163,12 @@ export namespace ILlmSchema { /** * Default value. */ - default?: string; + default?: string | null; /** * Enumeration values. */ - enum?: string[]; + enum?: Array; /** * Format restriction. @@ -352,6 +352,11 @@ export namespace ILlmSchema { * Type is always `null`. */ type: "null"; + + /** + * Default value. + */ + default?: null; } /** From f8065394c166d47305e038e780e036dc5196c706 Mon Sep 17 00:00:00 2001 From: Jeongho Nam Date: Sun, 8 Sep 2024 02:42:30 +0900 Subject: [PATCH 09/16] Description comments of LLM function calling schemas. --- src/HttpLlm.ts | 115 ++++++++++++++++++++++++++ src/structures/IHttpLlmApplication.ts | 77 ++++++++++++++--- src/structures/IHttpLlmFunction.ts | 34 ++++---- src/structures/ILlmFunction.ts | 12 +-- 4 files changed, 206 insertions(+), 32 deletions(-) diff --git a/src/HttpLlm.ts b/src/HttpLlm.ts index 2d76154..9ff129c 100644 --- a/src/HttpLlm.ts +++ b/src/HttpLlm.ts @@ -11,7 +11,57 @@ import { ILlmFunction } from "./structures/ILlmFunction"; import { ILlmSchema } from "./structures/ILlmSchema"; import { LlmDataMerger } from "./utils/LlmDataMerger"; +/** + * LLM function calling application composer from OpenAPI document. + * + * `HttpLlm` is a module for composing LLM (Large Language Model) function calling + * application by the {@link OpenApi.IDocument OpenAPI document}, and also for + * LLM function call execution and parameter merging. + * + * At first, you can construct the LLM function calling application by the + * {@link HttpLlm.application HttpLlm.application()} function. And then the LLM + * has selected a {@link IHttpLlmFunction function} to call and composes its + * arguments, you can execute the function by + * {@link HttpLlm.execute HttpLlm.execute()} or + * {@link HttpLlm.propagate HttpLlm.propagate()}. + * + * By the way, if you have configured the {@link IHttpLlmApplication.IOptions.separate} + * option to separate the parameters into human and LLM sides, you can merge these + * human and LLM sides' parameters into one through + * {@link HttpLlm.mergeParameters HttpLlm.mergeParameters()} before the actual LLM + * function call execution. + * + * @author Jeongho Nam - https://github.com/samchon + */ export namespace HttpLlm { + /* ----------------------------------------------------------- + COMPOSERS + ----------------------------------------------------------- */ + /** + * Convert OpenAPI document to LLM function calling application. + * + * Converts {@link OpenApi.IDocument OpenAPI document} or + * {@link IHttpMigrateApplication migrated application} to the + * {@link IHttpLlmApplication LLM function calling application}. Every + * {@link OpenApi.IOperation API operations} in the OpenAPI document are converted + * to the {@link IHttpLlmFunction LLM function} type, and they would be used for + * the LLM function calling. + * + * If you have configured the {@link IHttpLlmApplication.IOptions.separate} option, + * every parameters in the {@link IHttpLlmFunction} would be separated into both + * human and LLM sides. In that case, you can merge these human and LLM sides' + * parameters into one through {@link HttpLlm.mergeParameters} before the actual + * LLM function call execution. + * + * Additionally, if you have configured the {@link IHttpLlmApplication.IOptions.keyword} + * as `true`, the number of {@link IHttpLlmFunction.parameters} are always 1 and the + * first parameter type is always {@link ILlmSchema.IObject}. I recommend this option + * because LLM can understand the keyword arguments more easily. + * + * @param document Target OpenAPI document to convert (or migrate application) + * @param options Options for the LLM function calling application conversion + * @returns LLM function calling application + */ export const application = < Schema extends ILlmSchema, Operation extends OpenApi.IOperation, @@ -35,11 +85,30 @@ export namespace HttpLlm { ); }; + /** + * Convert JSON schema to LLM schema. + * + * Converts {@link OpenApi.IJsonSchema JSON schema} to {@link ILlmSchema LLM schema}. + * + * By the way, if the target JSON schema has some recursive references, the + * conversion would be failed and `null` value would be returned. It's because + * the LLM schema does not support the reference type embodied by the + * {@link OpenApi.IJsonSchema.IReference} type. + * + * @param props Schema to convert and components to refer + * @returns LLM schema or null value + */ export const schema = (props: { components: OpenApi.IComponents; schema: OpenApi.IJsonSchema; }): ILlmSchema | null => HttpLlmConverter.schema(props); + /* ----------------------------------------------------------- + FETCHERS + ----------------------------------------------------------- */ + /** + * Properties for the LLM function call. + */ export interface IFetchProps { /** * Document of the OpenAI function call schemas. @@ -61,18 +130,64 @@ export namespace HttpLlm { */ arguments: any[]; } + + /** + * Execute the LLM function call. + * + * @param props + * @returns + */ export const execute = (props: IFetchProps): Promise => HttpLlmFunctionFetcher.execute(props); + + /** + * Propagate the LLM function call. + * + * @param props + * @returns + */ export const propagate = (props: IFetchProps): Promise => HttpLlmFunctionFetcher.propagate(props); + /* ----------------------------------------------------------- + MERGERS + ----------------------------------------------------------- */ + /** + * Properties for the parameters' merging. + */ export interface IMergeProps { + /** + * Metadata of the target function. + */ function: ILlmFunction; + + /** + * Arguments composed by the LLM. + */ llm: unknown[]; + + /** + * Arguments composed by the human. + */ human: unknown[]; } + + /** + * Merge the parameters. + * + * @param props + * @returns + */ export const mergeParameters = (props: IMergeProps): unknown[] => LlmDataMerger.parameters(props); + + /** + * Merge two values. + * + * @param x Value X to merge + * @param y Value Y to merge + * @returns Merged value + */ export const mergeValue = (x: unknown, y: unknown): unknown => LlmDataMerger.value(x, y); } diff --git a/src/structures/IHttpLlmApplication.ts b/src/structures/IHttpLlmApplication.ts index c974f48..98ba33d 100644 --- a/src/structures/IHttpLlmApplication.ts +++ b/src/structures/IHttpLlmApplication.ts @@ -3,6 +3,69 @@ import { IHttpLlmFunction } from "./IHttpLlmFunction"; import { IHttpMigrateRoute } from "./IHttpMigrateRoute"; import { ILlmSchema } from "./ILlmSchema"; +/** + * Application of LLM function call from OpenAPI document. + * + * `IHttpLlmApplication` is a data structure representing collection of + * {@link IHttpLlmFunction LLM function calling schemas} composed from the + * {@link OpenApi.IDocument OpenAPI document} and its {@link OpenApi.IOperation operation} + * metadata. It also contains {@link IHttpLlmApplication.errors failed operations}, and + * adjusted {@link IHttpLlmApplication.options options} during the `IHttpLlmApplication` + * construction. + * + * About the {@link OpenApi.IOperation API operations}, they are converted to + * {@link IHttpLlmFunction} type which represents LLM function calling schema. + * By the way, if tehre're some recursive types which can't escape the + * {@link OpenApi.IJsonSchema.IReference} type, the operation would be failed and + * pushed into the {@link IHttpLlmApplication.errors}. Otherwise not, the operation + * would be successfully converted to {@link IHttpLlmFunction} and its type schemas + * are downgraded to {@link OpenApiV3.IJsonSchema} and converted to {@link ILlmSchema}. + * + * About the options, if you've configured {@link IHttpLlmApplication.options.keyword} + * (as `true`), number of {@link IHttpLlmFunction.parameters} are always 1 and the first + * parameter type is always {@link ILlmSchema.IObject}. Otherwise, the parameters would + * be multiple, and the sequence of the parameters are following below rules. + * + * - `pathParameters`: Path parameters of {@link IHttpMigrateRoute.parameters} + * - `query`: Query parameter of {@link IHttpMigrateRoute.query} + * - `body`: Body parameter of {@link IHttpMigrateRoute.body} + * + * ```typescript + * // KEYWORD TRUE + * { + * ...pathParameters, + * query, + * body, + * } + * + * // KEYWORD FALSE + * [ + * ...pathParameters, + * ...(query ? [query] : []), + * ...(body ? [body] : []), + * ] + * ``` + * + * By the way, there can be some parameters (or their nested properties) which must be + * composed by human, not by LLM. File uploading feature or some sensitive information + * like secrety key (password) are the examples. In that case, you can separate the + * function parameters to both LLM and human sides by configuring the + * {@link IHttpLlmApplication.IOptions.separate} property. The separated parameters are + * assigned to the {@link IHttpLlmFunction.separated} property. + * + * For reference, the actual function call execution is not by LLM, but by you. + * When the LLM selects the proper function and fills the arguments, you just call + * the function by {@link HttpLlm.execute} with the LLM prepared arguments. And then + * informs the return value to the LLM by system prompt. The LLM will continue the next + * conversation based on the return value. + * + * Additionally, if you've configured {@link IHttpLlmApplication.IOptions.separate}, + * so that the parameters are separated to human and LLM sides, you can merge these + * humand and LLM sides' parameters into one through {@link HttpLlm.mergeParameters} + * before the actual LLM function call execution. + * + * @author Jeongho Nam - https://github.com/samchon + */ export interface IHttpLlmApplication< Schema extends ILlmSchema = ILlmSchema, Operation extends OpenApi.IOperation = OpenApi.IOperation, @@ -91,25 +154,19 @@ export namespace IHttpLlmApplication { * If this property value is `true`, length of the * {@link IHttpLlmApplication.IFunction.parameters} is always 1, and type of * the pararameter is always {@link ILlmSchema.IObject} type. - * Also, its properties are following below rules: * - * - `pathParameters`: Path parameters of {@link IHttpMigrateRoute.parameters} - * - `query`: Query parameter of {@link IHttpMigrateRoute.query} - * - `body`: Body parameter of {@link IHttpMigrateRoute.body} + * Otherwise, the parameters would be multiple, and the sequence of the parameters + * are following below rules. * * ```typescript + * // KEYWORD TRUE * { * ...pathParameters, * query, * body, * } - * ``` - * - * Otherwise (this property value is `false`), length of the - * {@link IHttpLlmFunction.parameters} is variable, and sequence of the - * parameters are following below rules. * - * ```typescript + * // KEYWORD FALSE * [ * ...pathParameters, * ...(query ? [query] : []), diff --git a/src/structures/IHttpLlmFunction.ts b/src/structures/IHttpLlmFunction.ts index dc9c527..a36e99d 100644 --- a/src/structures/IHttpLlmFunction.ts +++ b/src/structures/IHttpLlmFunction.ts @@ -3,27 +3,25 @@ import { IHttpMigrateRoute } from "./IHttpMigrateRoute"; import { ILlmSchema } from "./ILlmSchema"; /** - * LLM function metadata from HTTP (OpenAPI) operation. + * LLM function calling schema from HTTP (OpenAPI) operation. * - * `IHttpLlmFunction` is a data structure representing a procedure converted - * from the OpenAPI operation, used for the LLM (Large Language Model) - * function calling. It's a typical RPC (Remote Procedure Call) structure - * containing the procedure {@link name}, {@link parameters}, and + * `IHttpLlmFunction` is a data structure representing a function converted + * from the {@link OpenApi.IOperation OpenAPI operation}, used for the LLM + * (Large Language Model) function calling. It's a typical RPC (Remote Procedure Call) + * structure containing the function {@link name}, {@link parameters}, and * {@link output return type}. * - * If you provide this `IHttpLlmFunction` data to the LLM like "OpenAI", - * the "OpenAI" will compose a function arguments by analyzing - * conversations with the user. With the LLM composed arguments, you can - * execute the procedure through {@link LlmFetcher.execute} and get the - * result. + * If you provide this `IHttpLlmFunction` data to the LLM provider like "OpenAI", + * the "OpenAI" will compose a function arguments by analyzing conversations with + * the user. With the LLM composed arguments, you can execute the function through + * {@link LlmFetcher.execute} and get the result. * * For reference, different between `IHttpLlmFunction` and its origin source - * {@link OpenApi.IOperation} is, `IHttpLlmFunction` has converted every type - * schema informations from {@link OpenApi.IJsonSchema} to {@link ILlmSchema} - * to escape {@link OpenApi.IJsonSchema.IReference reference types}, and - * downgrade the version of the JSON schema to OpenAPI 3.0. It's because - * LLM function call feature cannot understand both reference types and - * OpenAPI 3.1 specification. + * {@link OpenApi.IOperation} is, `IHttpLlmFunction` has converted every type schema + * informations from {@link OpenApi.IJsonSchema} to {@link ILlmSchema} to escape + * {@link OpenApi.IJsonSchema.IReference reference types}, and downgrade the version + * of the JSON schema to OpenAPI 3.0. It's because LLM function call feature cannot + * understand both reference types and OpenAPI 3.1 specification. * * Additionally, if you've composed `IHttpLlmFunction` with * {@link IHttpLlmApplication.IOptions.keyword} configuration as `true`, number of @@ -163,7 +161,7 @@ export interface IHttpLlmFunction< output?: Schema | undefined; /** - * Description of the procedure. + * Description of the function. * * `IHttpLlmFunction.description` is composed by below rule: * @@ -228,6 +226,8 @@ export namespace IHttpLlmFunction { export interface ISeparatedParameter { /** * Index of the parameter. + * + * @type uint */ index: number; diff --git a/src/structures/ILlmFunction.ts b/src/structures/ILlmFunction.ts index 7b13072..e0cef71 100644 --- a/src/structures/ILlmFunction.ts +++ b/src/structures/ILlmFunction.ts @@ -8,12 +8,12 @@ import { ILlmSchema } from "./ILlmSchema"; * calling. Also, it's a function structure containing the function * {@link name}, {@link parameters} and {@link output return type}. * - * If you provide this `ILlmFunction` data to the LLM like "OpenAI", - * the "OpenAI" will compose a function arguments by analyzing - * conversations with the user. With the LLM composed arguments, you can - * execute the function and get the result. + * If you provide this `ILlmFunction` data to the LLM provider like "OpenAI", + * the "OpenAI" will compose a function arguments by analyzing conversations + * with the user. With the LLM composed arguments, you can execute the function + * and get the result. * - * By the way, do not sure that LLM will always provide the correct + * By the way, do not ensure that LLM will always provide the correct * arguments. The LLM of present age is not perfect, so that you would * better to validate the arguments before executing the function. * I recommend you to validate the arguments before execution by using @@ -81,6 +81,8 @@ export namespace ILlmFunction { export interface ISeparatedParameter { /** * Index of the parameter. + * + * @type uint */ index: number; From 8620a64454284126da95e65eb9f283b8a3077345 Mon Sep 17 00:00:00 2001 From: Jeongho Nam Date: Sun, 8 Sep 2024 02:56:39 +0900 Subject: [PATCH 10/16] Description about `HttpLlm` module. --- src/HttpLlm.ts | 64 +++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 58 insertions(+), 6 deletions(-) diff --git a/src/HttpLlm.ts b/src/HttpLlm.ts index 9ff129c..537b285 100644 --- a/src/HttpLlm.ts +++ b/src/HttpLlm.ts @@ -134,8 +134,27 @@ export namespace HttpLlm { /** * Execute the LLM function call. * - * @param props - * @returns + * `HttmLlm.execute()` is a function executing the target + * {@link OpenApi.IOperation API endpoint} with with the connection information + * and arguments composed by Large Language Model like OpenAI (+human sometimes). + * + * By the way, if you've configured the {@link IHttpLlmApplication.IOptions.separate}, + * so that the parameters are separated to human and LLM sides, you have to merge + * these humand and LLM sides' parameters into one through + * {@link HttpLlm.mergeParameters} function. + * + * About the {@link IHttpLlmApplication.IOptions.keyword} option, don't worry anything. + * This `HttmLlm.execute()` function will automatically recognize the keyword arguments + * and convert them to the proper sequence. + * + * For reference, if the target API endpoinnt responds none 200/201 status, this + * would be considered as an error and the {@link HttpError} would be thrown. + * Otherwise you don't want such rule, you can use the {@link HttpLlm.propagate} + * function instead. + * + * @param props Properties for the LLM function call + * @returns Return value from the API endpoint + * @throws HttpError when the API endpoint responds none 200/201 status */ export const execute = (props: IFetchProps): Promise => HttpLlmFunctionFetcher.execute(props); @@ -143,8 +162,26 @@ export namespace HttpLlm { /** * Propagate the LLM function call. * - * @param props - * @returns + * `HttmLlm.propagate()` is a function propagating the target + * {@link OpenApi.IOperation API endpoint} with with the connection information + * and arguments composed by Large Language Model like OpenAI (+human sometimes). + * + * By the way, if you've configured the {@link IHttpLlmApplication.IOptions.separate}, + * so that the parameters are separated to human and LLM sides, you have to merge + * these humand and LLM sides' parameters into one through + * {@link HttpLlm.mergeParameters} function. + * + * About the {@link IHttpLlmApplication.IOptions.keyword} option, don't worry anything. + * This `HttmLlm.propagate()` function will automatically recognize the keyword arguments + * and convert them to the proper sequence. + * + * For reference, the propagation means always returning the response from the API + * endpoint, even if the status is not 200/201. This is useful when you want to + * handle the response by yourself. + * + * @param props Properties for the LLM function call + * @returns Response from the API endpoint + * @throws Error only when the connection is failed */ export const propagate = (props: IFetchProps): Promise => HttpLlmFunctionFetcher.propagate(props); @@ -175,8 +212,17 @@ export namespace HttpLlm { /** * Merge the parameters. * - * @param props - * @returns + * If you've configured the {@link IHttpLlmApplication.IOptions.separate} option, + * so that the parameters are separated to human and LLM sides, you can merge these + * humand and LLM sides' parameters into one through this `HttpLlm.mergeParameters()` + * function before the actual LLM function call execution. + * + * On contrary, if you've not configured the + * {@link IHttpLlmApplication.IOptions.separate} option, this function would throw + * an error. + * + * @param props Properties for the parameters' merging + * @returns Merged parameter values */ export const mergeParameters = (props: IMergeProps): unknown[] => LlmDataMerger.parameters(props); @@ -184,6 +230,12 @@ export namespace HttpLlm { /** * Merge two values. * + * If both values are objects, then combines them in the properties level. + * + * Otherwise, returns the latter value if it's not null, otherwise the former value. + * + * - `return (y ?? x)` + * * @param x Value X to merge * @param y Value Y to merge * @returns Merged value From 8217f7fb0da67365975d4015acc305634382a6fb Mon Sep 17 00:00:00 2001 From: Jeongho Nam Date: Mon, 9 Sep 2024 00:57:38 +0900 Subject: [PATCH 11/16] Completetion of description comments writing. --- package.json | 2 +- src/HttpLlm.ts | 6 +- src/HttpMigration.ts | 131 +++++++++++++++++- src/http/HttpLlmFunctionFetcher.ts | 39 ++---- src/http/HttpMigrateRouteFetcher.ts | 25 ++-- src/structures/IHttpMigrateApplication.ts | 5 +- src/structures/IHttpMigrateRoute.ts | 4 +- src/structures/IHttpResponse.ts | 20 +++ ...t_http_migrate_fetch_keyword_parameters.ts | 2 +- .../test_http_migrate_fetch_multipart.ts | 2 +- ...ttp_migrate_fetch_positional_parameters.ts | 2 +- .../migrate/test_http_migrate_fetch_query.ts | 2 +- .../test_http_migrate_fetch_query_and_body.ts | 2 +- 13 files changed, 181 insertions(+), 61 deletions(-) diff --git a/package.json b/package.json index d3a5906..ccde257 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@samchon/openapi", - "version": "0.5.0-dev.20240906-2", + "version": "1.0.0-dev.20240908", "description": "OpenAPI definitions and converters for 'typia' and 'nestia'.", "main": "./lib/index.js", "module": "./lib/index.mjs", diff --git a/src/HttpLlm.ts b/src/HttpLlm.ts index 537b285..de94dec 100644 --- a/src/HttpLlm.ts +++ b/src/HttpLlm.ts @@ -15,7 +15,7 @@ import { LlmDataMerger } from "./utils/LlmDataMerger"; * LLM function calling application composer from OpenAPI document. * * `HttpLlm` is a module for composing LLM (Large Language Model) function calling - * application by the {@link OpenApi.IDocument OpenAPI document}, and also for + * application from the {@link OpenApi.IDocument OpenAPI document}, and also for * LLM function call execution and parameter merging. * * At first, you can construct the LLM function calling application by the @@ -121,7 +121,7 @@ export namespace HttpLlm { function: IHttpLlmFunction; /** - * Connection info to the server. + * Connection info to the HTTP server. */ connection: IHttpConnection; @@ -153,7 +153,7 @@ export namespace HttpLlm { * function instead. * * @param props Properties for the LLM function call - * @returns Return value from the API endpoint + * @returns Return value (response body) from the API endpoint * @throws HttpError when the API endpoint responds none 200/201 status */ export const execute = (props: IFetchProps): Promise => diff --git a/src/HttpMigration.ts b/src/HttpMigration.ts index b90f538..653808c 100644 --- a/src/HttpMigration.ts +++ b/src/HttpMigration.ts @@ -6,7 +6,72 @@ import { IHttpMigrateApplication } from "./structures/IHttpMigrateApplication"; import { IHttpMigrateRoute } from "./structures/IHttpMigrateRoute"; import { IHttpResponse } from "./structures/IHttpResponse"; +/** + * HTTP migration application composer from OpenAPI document. + * + * `HttpMigration` is a module for composing HTTP migration application from the + * {@link OpenApi.IDocument OpenAPI document}. It is designed for helping the OpenAPI + * generator libraries, which converts {@link OpenApi.IOperation OpenAPI operations} to + * an RPC (Remote Procedure Call) function. + * + * The key feature of the `HttpModule` is the {@link HttpMigration.application} function. + * It converts the {@link OpenApi.IOperation OpenAPI operations} to the + * {@link IHttpMigrateRoute HTTP migration route}, and it normalizes the OpenAPI operations + * to the RPC function calling suitable route structure. + * + * The other functions, {@link HttpMigration.execute} and {@link HttpMigration.propagate}, + * are for executing the HTTP request to the HTTP server. The {@link HttpMigration.execute} + * function returns the response body from the API endpoint when the status code is `200` + * or `201`. Otherwise, it throws an {@link HttpError} when the status code is not `200` + * or `201`. The {@link HttpMigration.propagate} function returns the response information + * from the API endpoint, including the status code, headers, and response body. + * + * The {@link HttpLlm} module is a good example utilizing this `HttpMigration` module + * for composing RPC function calling application. The {@link HttpLlm} module composes + * LLM (Large Language Model) function calling application from the OpenAPI document + * bypassing through the {@link IHttpLlmApplication} type. + * + * @author Jeongho Nam - https://github.com/samchon + */ export namespace HttpMigration { + /* ----------------------------------------------------------- + COMPOSER + ----------------------------------------------------------- */ + /** + * Convert HTTP migration application from OpenAPI document. + * + * `HttpMigration.application()` is a function converting the + * {@link OpenApi.IDocument OpenAPI document} and its {@link OpenApi.IOperation operations} + * to the {@link IHttpMigrateApplication HTTP migration application}. + * + * The HTTP migration application is designed for helping the OpenAPI generator + * libraries, which converts OpenAPI operations to an RPC (Remote Procedure Call) + * function. To support the OpenAPI generator libraries, {@link IHttpMigrateRoute} + * takes below normalization rules: + * + * - Path parameters are separated to atomic level. + * - Query parameters are binded into one object. + * - Header parameters are binded into one object. + * - Allow only below HTTP methods + * - `head` + * - `get` + * - `post` + * - `put` + * - `patch` + * - `delete` + * - Allow only below content media types + * - `application/json` + * - `application/x-www-form-urlencoded` + * - `multipart/form-data` + * - `text/plain` + * + * If there're some {@link OpenApi.IOperation API operations} which canont adjust + * the above rules or there're some logically insensible, these operation would be + * failed to migrate and registered into the {@link IHttpMigrateApplication.errors}. + * + * @param document OpenAPI document to migrate. + * @returns Migrated application. + */ export const application = < Schema extends OpenApi.IJsonSchema = OpenApi.IJsonSchema, Operation extends OpenApi.IOperation = OpenApi.IOperation, @@ -15,17 +80,79 @@ export namespace HttpMigration { ): IHttpMigrateApplication => MigrateConverter.convert(document); + /** + * Properties for the request to the HTTP server. + */ export interface IFetchProps { + /** + * Connection info to the HTTP server. + */ connection: IHttpConnection; + + /** + * Route information for the migration. + */ route: IHttpMigrateRoute; + + /** + * Path parameters. + * + * Path parameters with sequenced array or key-value paired object. + */ parameters: | Array | Record; + + /** + * Query parameters as a key-value paired object. + */ query?: object | undefined; + + /** + * Request body data. + */ body?: object | undefined; } - export const request = (props: IFetchProps): Promise => - HttpMigrateRouteFetcher.request(props); + + /* ----------------------------------------------------------- + FETCHERS + ----------------------------------------------------------- */ + /** + * Execute the HTTP request. + * + * `HttpMigration.execute()` is a function executing the HTTP request to the HTTP server. + * + * It returns the response body from the API endpoint when the status code is `200` + * or `201`. Otherwise, it throws an {@link HttpError} when the status code is not + * `200` or `201`. + * + * If you want to get more information than the response body, or get the detailed + * response information even when the status code is `200` or `201`, use the + * {@link HttpMigration.propagate} function instead. + * + * @param props Properties for the request. + * @returns Return value (response body) from the API endpoint. + * @throws HttpError when the API endpoint responds none 200/201 status. + */ + export const execute = (props: IFetchProps): Promise => + HttpMigrateRouteFetcher.execute(props); + + /** + * Propagate the HTTP request. + * + * `HttpMigration.propagate()` is a function propagating the request to the HTTP server. + * + * It returns the response information from the API endpoint, including the status code, + * headers, and response body. + * + * Even if the status code is not `200` or `201`, this function + * would return the response information. By the way, if the connection to the HTTP server + * is failed, this function would throw an {@link Error}. + * + * @param props Properties for the request. + * @returns Response from the API endpoint. + * @throws Error when the connection is failed. + */ export const propagate = (props: IFetchProps): Promise => HttpMigrateRouteFetcher.propagate(props); } diff --git a/src/http/HttpLlmFunctionFetcher.ts b/src/http/HttpLlmFunctionFetcher.ts index 7f08c35..100b37e 100644 --- a/src/http/HttpLlmFunctionFetcher.ts +++ b/src/http/HttpLlmFunctionFetcher.ts @@ -1,43 +1,22 @@ -import { IHttpConnection } from "../structures/IHttpConnection"; -import { IHttpLlmApplication } from "../structures/IHttpLlmApplication"; -import { IHttpLlmFunction } from "../structures/IHttpLlmFunction"; +import type { HttpLlm } from "../HttpLlm"; +import type { HttpMigration } from "../HttpMigration"; import { IHttpMigrateRoute } from "../structures/IHttpMigrateRoute"; import { IHttpResponse } from "../structures/IHttpResponse"; import { HttpMigrateRouteFetcher } from "./HttpMigrateRouteFetcher"; export namespace HttpLlmFunctionFetcher { - export interface IProps { - /** - * Application of the OpenAI function call schemas. - */ - application: IHttpLlmApplication; + export const execute = async (props: HttpLlm.IFetchProps): Promise => + HttpMigrateRouteFetcher.execute(getFetchArguments("execute", props)); - /** - * Function schema to call. - */ - function: IHttpLlmFunction; - - /** - * Connection info to the server. - */ - connection: IHttpConnection; - - /** - * Arguments for the function call. - */ - arguments: any[]; - } - - export const execute = async (props: IProps): Promise => - HttpMigrateRouteFetcher.request(getFetchArguments("execute", props)); - - export const propagate = async (props: IProps): Promise => + export const propagate = async ( + props: HttpLlm.IFetchProps, + ): Promise => HttpMigrateRouteFetcher.propagate(getFetchArguments("propagate", props)); const getFetchArguments = ( from: string, - props: IProps, - ): HttpMigrateRouteFetcher.IProps => { + props: HttpLlm.IFetchProps, + ): HttpMigration.IFetchProps => { const route: IHttpMigrateRoute = props.function.route(); if (props.application.options.keyword === true) { const input: Record = props.arguments[0]; diff --git a/src/http/HttpMigrateRouteFetcher.ts b/src/http/HttpMigrateRouteFetcher.ts index c2551cc..71bd69f 100644 --- a/src/http/HttpMigrateRouteFetcher.ts +++ b/src/http/HttpMigrateRouteFetcher.ts @@ -1,20 +1,12 @@ +import type { HttpMigration } from "../HttpMigration"; import { IHttpConnection } from "../structures/IHttpConnection"; -import { IHttpMigrateRoute } from "../structures/IHttpMigrateRoute"; import { IHttpResponse } from "../structures/IHttpResponse"; import { HttpError } from "./HttpError"; export namespace HttpMigrateRouteFetcher { - export interface IProps { - connection: IHttpConnection; - route: IHttpMigrateRoute; - parameters: - | Array - | Record; - query?: object | undefined; - body?: object | undefined; - } - - export const request = async (props: IProps): Promise => { + export const execute = async ( + props: HttpMigration.IFetchProps, + ): Promise => { const result: IHttpResponse = await _Propagate("request", props); props.route.success?.media; if (result.status !== 200 && result.status !== 201) @@ -28,13 +20,14 @@ export namespace HttpMigrateRouteFetcher { return result.body; }; - export const propagate = (props: IProps): Promise => - _Propagate("propagate", props); + export const propagate = ( + props: HttpMigration.IFetchProps, + ): Promise => _Propagate("propagate", props); } const _Propagate = async ( from: string, - props: HttpMigrateRouteFetcher.IProps, + props: HttpMigration.IFetchProps, ): Promise => { // VALIDATE PARAMETERS const error = (message: string) => @@ -142,7 +135,7 @@ const _Propagate = async ( }; const getPath = ( - props: Pick, + props: Pick, ): string => { let path: string = props.route.emendedPath; props.route.parameters.forEach((p, i) => { diff --git a/src/structures/IHttpMigrateApplication.ts b/src/structures/IHttpMigrateApplication.ts index e88fa11..737d1ed 100644 --- a/src/structures/IHttpMigrateApplication.ts +++ b/src/structures/IHttpMigrateApplication.ts @@ -5,8 +5,9 @@ import { IHttpMigrateRoute } from "./IHttpMigrateRoute"; * Document of migration. * * The `IHttpMigrateApplication` interface is an application migrated from - * {@link OpenAPI.IDocument OpenAPI document} to RPC (Remote Procedure Call) - * functions; {@link IHttpMigrateRoute}. + * {@link OpenAPI.IDocument OpenAPI document} for supporting the OpenAPI generator + * libraries which compose RPC (Remote Procedure Call) functions from the + * {@link OpenAPI.IOperation OpenAPI operations}. * * As the `IHttpMigrateApplication` and {@link IHttpMigrateRoute} have a lot of special * stories, when you're developing OpenAPI generator library, please read diff --git a/src/structures/IHttpMigrateRoute.ts b/src/structures/IHttpMigrateRoute.ts index c262fb0..5341273 100644 --- a/src/structures/IHttpMigrateRoute.ts +++ b/src/structures/IHttpMigrateRoute.ts @@ -4,8 +4,8 @@ import { OpenApi } from "../OpenApi"; * Route information for migration. * * The `IHttpMigrateRoute` is a structure representing a route information for - * OpenAPI generated RPC (Remote Procedure Call) function composed from the - * {@link OpenApi.IOperation OpenAPI operation}. + * OpenAPI generator libraries, which composes an RPC (Remote Procedure Call) function + * from the {@link OpenApi.IOperation OpenAPI operation}. * * As the `IHttpMigrateRoute` has a lot of speical stories, when you're developing * OpenAPI generator library, please read its description carefully including diff --git a/src/structures/IHttpResponse.ts b/src/structures/IHttpResponse.ts index cdd12e9..47684eb 100644 --- a/src/structures/IHttpResponse.ts +++ b/src/structures/IHttpResponse.ts @@ -1,5 +1,25 @@ +/** + * Represents an HTTP response. + * + * The `IHttpResponse` interface represents an HTTP response. + * + * It contains the {@link status} code, {@link headers}, and {@link body} of the response. + * + * @author Jeongho Nam - https://github.com/samchon + */ export interface IHttpResponse { + /** + * Status code of the response. + */ status: number; + + /** + * Headers of the response. + */ headers: Record; + + /** + * Body of the response. + */ body: unknown; } diff --git a/test/features/migrate/test_http_migrate_fetch_keyword_parameters.ts b/test/features/migrate/test_http_migrate_fetch_keyword_parameters.ts index 8c94ce9..6551139 100644 --- a/test/features/migrate/test_http_migrate_fetch_keyword_parameters.ts +++ b/test/features/migrate/test_http_migrate_fetch_keyword_parameters.ts @@ -19,7 +19,7 @@ export const test_http_migrate_fetch_keyword_parameters = async ( ); if (route === undefined) throw new Error("Route not found"); - await HttpMigration.request({ + await HttpMigration.execute({ connection, route, parameters: { diff --git a/test/features/migrate/test_http_migrate_fetch_multipart.ts b/test/features/migrate/test_http_migrate_fetch_multipart.ts index cfbae26..63f268e 100644 --- a/test/features/migrate/test_http_migrate_fetch_multipart.ts +++ b/test/features/migrate/test_http_migrate_fetch_multipart.ts @@ -19,7 +19,7 @@ export const test_http_migrate_fetch_multipart = async ( ); if (route === undefined) throw new Error("Route not found"); - await HttpMigration.request({ + await HttpMigration.execute({ connection, route, parameters: { diff --git a/test/features/migrate/test_http_migrate_fetch_positional_parameters.ts b/test/features/migrate/test_http_migrate_fetch_positional_parameters.ts index 5dcf8a6..f20d839 100644 --- a/test/features/migrate/test_http_migrate_fetch_positional_parameters.ts +++ b/test/features/migrate/test_http_migrate_fetch_positional_parameters.ts @@ -19,7 +19,7 @@ export const test_http_migrate_fetch_positional_parameters = async ( ); if (route === undefined) throw new Error("Route not found"); - await HttpMigration.request({ + await HttpMigration.execute({ connection, route, parameters: ["https://some.url/index.html", 2, true], diff --git a/test/features/migrate/test_http_migrate_fetch_query.ts b/test/features/migrate/test_http_migrate_fetch_query.ts index 46ad613..5df5243 100644 --- a/test/features/migrate/test_http_migrate_fetch_query.ts +++ b/test/features/migrate/test_http_migrate_fetch_query.ts @@ -18,7 +18,7 @@ export const test_http_migrate_fetch_query = async ( ); if (route === undefined) throw new Error("Route not found"); - await HttpMigration.request({ + await HttpMigration.execute({ connection, route, parameters: { diff --git a/test/features/migrate/test_http_migrate_fetch_query_and_body.ts b/test/features/migrate/test_http_migrate_fetch_query_and_body.ts index 453a623..8c1c1dd 100644 --- a/test/features/migrate/test_http_migrate_fetch_query_and_body.ts +++ b/test/features/migrate/test_http_migrate_fetch_query_and_body.ts @@ -19,7 +19,7 @@ export const test_http_migrate_fetch_query_and_body = async ( ); if (route === undefined) throw new Error("Route not found"); - await HttpMigration.request({ + await HttpMigration.execute({ connection, route, parameters: { From e03ce20d0e74c6d4bc26e5ed819ff5095d997078 Mon Sep 17 00:00:00 2001 From: Jeongho Nam Date: Mon, 9 Sep 2024 02:45:21 +0900 Subject: [PATCH 12/16] Re-writing README document. --- README.md | 196 +++++++++++++++++++++++++---------------- test/manual/example.ts | 50 +++++++++++ 2 files changed, 168 insertions(+), 78 deletions(-) create mode 100644 test/manual/example.ts diff --git a/README.md b/README.md index 4f3b3eb..1bd4755 100644 --- a/README.md +++ b/README.md @@ -1,116 +1,155 @@ # `@samchon/openapi` -![Nestia Editor](https://github.com/samchon/openapi/assets/13158709/350128f7-c159-4ba4-8f8c-743908ada8eb) +```mermaid +flowchart + subgraph "OpenAPI Specification" + v20("Swagger v2.0") --upgrades--> emended[["OpenAPI v3.1 (emended)"]] + v30("OpenAPI v3.0") --upgrades--> emended + v31("OpenAPI v3.1") --emends--> emended + end + subgraph "Ecosystem" + emended --normalizes--> migration[["Migration Schema"]] + migration --"Artificial Intelligence"--> lfc{{"LLM Function Calling Schema"}} + end +``` [![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/samchon/openapi/blob/master/LICENSE) [![npm version](https://img.shields.io/npm/v/@samchon/openapi.svg)](https://www.npmjs.com/package/@samchon/openapi) [![Downloads](https://img.shields.io/npm/dm/@samchon/openapi.svg)](https://www.npmjs.com/package/@samchon/openapi) [![Build Status](https://github.com/samchon/openapi/workflows/build/badge.svg)](https://github.com/samchon/openapi/actions?query=workflow%3Abuild) -OpenAPI definitions and converters. +OpenAPI definitions, converters and utillity functions. -`@samchon/openapi` is a collection of OpenAPI definitions of below versions. Those type definitions do not contain every properties of OpenAPI specification, but just have only significant features essentially required for `typia`, `nestia` (especially for [`@nestia/editor`](https://nestia.io/docs/editor/)). +`@samchon/openapi` is a collection of OpenAPI types for every versions, and converters for them. In the OpenAPI types, there is an "emended" OpenAPI v3.1 specification, which has removed ambiguous and duplicated expressions for the clarity. Every conversions are based on the emended OpenAPI v3.1 specification. 1. [Swagger v2.0](https://github.com/samchon/openapi/blob/master/src/SwaggerV2.ts) 2. [OpenAPI v3.0](https://github.com/samchon/openapi/blob/master/src/OpenApiV3.ts) 3. [OpenAPI v3.1](https://github.com/samchon/openapi/blob/master/src/OpenApiV3_1.ts) + 4. [**OpenAPI v3.1 emended**](https://github.com/samchon/openapi/blob/master/src/OpenApi.ts) -Also, `@samchon/openapi` provides emended OpenAPI v3.1 definition and its converter/inverter from above versions for convenient development. The keyword "emended" means that [`OpenApi`](https://github.com/samchon/openapi/blob/master/src/OpenApi.ts) is not a direct OpenAPI v3.1 specification ([`OpenApiV3_1`](https://github.com/samchon/openapi/blob/master/src/OpenApiV3_1.ts)), but a little bit shrinked to remove ambiguous and duplicated expressions of OpenAPI v3.1 for the convenience of `typia` and `nestia`. +`@samchon/openapi` also provides LLM (Large Language Model) function calling application composer from the OpenAPI document with many strategies. With the [`HttpLlm`](https://github.com/samchon/openapi/blob/master/src/HttpLlm.ts) module, you can perform the LLM funtion calling extremely easily just by delivering the OpenAPI (Swagger) document. -For example, when representing nullable type, OpenAPI v3.1 supports three ways. In that case, OpenApi remains only the third way, so that makes `typia` and `nestia` (especially for [`@nestia/editor`](https://nestia.io/docs/editor/)) to be simple and easy to implement. + - [`HttpLlm.application()`](https://github.com/samchon/openapi/blob/master/src/HttpLlm.ts) + - [`IHttpLlmApplication`](https://github.com/samchon/openapi/blob/master/src/structures/ILlmApplication.ts) + - [`IHttpLlmFunction`](https://github.com/samchon/openapi/blob/master/src/structures/ILlmFunction.ts) + - [`ILlmSchema`](https://github.com/samchon/openapi/blob/master/src/structures/ILlmSchema.ts) + - [`LlmTypeChecker`](https://github.com/samchon/openapi/blob/master/src/utils/LlmTypeChecker.ts) -- `{ type: ["string", "null"] }` -- `{ type: "string", nullable: true }` -- `{ oneOf: [{ type: "string" }, { type: "null" }] }` +> [!NOTE] +> +> LLM selects proper function and fill arguments. +> +> In nowadays, most LLM (Large Language Model) like OpenAI are supporting "function calling" feature. The "LLM function calling" means that LLM automatically selects a proper function and fills parameter values from conversation with the user (may by chatting text). +> +> https://platform.openai.com/docs/guides/function-calling -```mermaid -flowchart - v20(Swagger v2.0) --upgrades--> emended[["OpenAPI v3.1 (emended)"]] - v30(OpenAPI v3.0) --upgrades--> emended - v31(OpenAPI v3.1) --emends--> emended - emended --downgrades--> v20d(Swagger v2.0) - emended --downgrades--> v30d(Swagger v3.0) -``` - -Here is the entire list of differences between OpenAPI v3.1 and emended OpenApi. - -- Operation - - Merge `OpenApiV3_1.IPathItem.parameters` to `OpenApi.IOperation.parameters` - - Resolve references of `OpenApiV3_1.IOperation` members - - Escape references of `OpenApiV3_1.IComponents.examples` -- JSON Schema - - Decompose mixed type: `OpenApiV3_1.IJsonSchema.IMixed` - - Resolve nullable property: `OpenApiV3_1.IJsonSchema.__ISignificant.nullable` - - Array type utilizes only single `OpenAPI.IJsonSchema.IArray.items` - - Tuple type utilizes only `OpenApi.IJsonSchema.ITuple.prefixItems` - - Merge `OpenApiV3_1.IJsonSchema.IAnyOf` to `OpenApi.IJsonSchema.IOneOf` - - Merge `OpenApiV3_1.IJsonSchema.IRecursiveReference` to `OpenApi.IJsonSchema.IReference` - - Merge `OpenApiV3_1.IJsonSchema.IAllOf` to `OpenApi.IJsonSchema.IObject` - -Additionally, `@samchon/openapi` provides [`IHttpMigrateApplication`](https://github.com/samchon/openapi/blob/master/src/IHttpMigrateApplication.ts) for OpenAPI generators. - -If you're developing TypeScript, [`@nestia/editor`](https://nestia.io/docs/editor) would be the best project utilizing the [`IHttpMigrateApplication`](https://github.com/samchon/openapi/blob/master/src/IHttpMigrateApplication.ts) for the OpenAPI SDK generation. Otherwise, you wanna utilize OpenAPI document for OpenAI function calling, [`@wrtnio/openai-function-schema`](https://github.com/wrtnio/openai-function-schema/) has been prepared for you. - -```mermaid -flowchart - subgraph "OpenAPI Specification" - v20(Swagger v2.0) --upgrades--> emended[["OpenAPI v3.1 (emended)"]] - v30(OpenAPI v3.0) --upgrades--> emended - v31(OpenAPI v3.1) --emends--> emended - end - subgraph "OpenAPI Generators" - emended --normalizes--> migration[["Migration Schema"]] - migration --A.I.--> lfc{{"LLM Function Call"}} - migration --Uiltiy--> editor{{"@nestia/editor"}} - end -``` - -## How to use +## Setup ```bash npm install @samchon/openapi ``` +Just install by `npm i @samchon/openapi` command. + +Here is an example code utilizing the `@samchon/openapi` for LLM function calling purpose. + ```typescript import { + HttpLlm, + IHttpLlmApplication, + IHttpLlmFunction, OpenApi, - SwaggerV2, OpenApiV3, OpenApiV3_1, - IHttpMigrateApplication, + SwaggerV2, } from "@samchon/openapi"; +import fs from "fs"; +import typia from "typia"; -// original Swagger/OpenAPI document -const input: - | SwaggerV2.IDocument - | OpenApiV3.IDocument - | OpenApiV3_1.IDocument - | OpenApi.IDocument = { ... }; +const main = async (): Promise => { + // read swagger document and validate it + const swagger: + | SwaggerV2.IDocument + | OpenApiV3.IDocument + | OpenApiV3_1.IDocument = JSON.parse( + await fs.promises.readFile("swagger.json", "utf8"), + ); + typia.assert(swagger); // recommended + + // convert to emended OpenAPI document, + // and compose LLM function calling application + const document: OpenApi.IDocument = OpenApi.convert(swagger); + const application: IHttpLlmApplication = HttpLlm.application(document); + + // Let's imagine that LLM has selected a function to call + const func: IHttpLlmFunction | undefined = application.functions.find( + // (f) => f.name === "llm_selected_fuction_name" + (f) => f.path === "/bbs/articles" && f.method === "post", + ); + if (func === undefined) throw new Error("No matched function exists."); + + // actual execution is by yourself + const article = await HttpLlm.execute({ + connection: { + host: "http://localhost:3000", + }, + application, + function: func, + arguments: [ + { + title: "Hello, world!", + body: "Let's imagine that this argument is composed by LLM.", + }, + ], + }); + console.log("article", article); +}; +main().catch(console.error); +``` -// you can convert it to emended OpenAPI v3.1 -const output: OpenApi.IDocument = OpenApi.convert(input); -// it is possible to downgrade to Swagger v2 or OpenAPI v3 -const v2: SwaggerV2 = OpenApi.downgrade(output, "2.0"); -const v3: OpenApiV3 = OpenApi.downgrade(output, "3.0"); -// you can utilize it like below -OpenApi.downgrade(OpenApi.convert(v2), "3.0"); -OpenApi.downgrade(OpenApi.convert(v3), "2.0"); -// also helps openapi generator libraries -const migrate: IHttpMigrateApplication = OpenApi.migrate(output); +## OpenAPI Definitions +```mermaid +flowchart + v20(Swagger v2.0) --upgrades--> emended[["OpenAPI v3.1 (emended)"]] + v30(OpenAPI v3.0) --upgrades--> emended + v31(OpenAPI v3.1) --emends--> emended + emended --downgrades--> v20d(Swagger v2.0) + emended --downgrades--> v30d(Swagger v3.0) ``` -Just install `@samchon/openapi` library and import `OpenApi` module from there. +`@samchon/openapi` support every versions of OpenAPI specifications with detailed TypeScript types. + + - [Swagger v2.0](https://github.com/samchon/openapi/blob/master/src/SwaggerV2.ts) + - [OpenAPI v3.0](https://github.com/samchon/openapi/blob/master/src/OpenApiV3.ts) + - [OpenAPI v3.1](https://github.com/samchon/openapi/blob/master/src/OpenApiV3_1.ts) + - [**OpenAPI v3.1 emended**](https://github.com/samchon/openapi/blob/master/src/OpenApi.ts) + +Also, `@samchon/openapi` provides "emended OpenAPI v3.1 definition" which has removed ambiguous and duplicated expressions for clarity. It has emended original OpenAPI v3.1 specification like above. You can compose the "emended OpenAPI v3.1 document" by calling the `OpenApi.convert()` function. + + - Operation + - Merge `OpenApiV3_1.IPathItem.parameters` to `OpenApi.IOperation.parameters` + - Resolve references of `OpenApiV3_1.IOperation` members + - Escape references of `OpenApiV3_1.IComponents.examples` + - JSON Schema + - Decompose mixed type: `OpenApiV3_1.IJsonSchema.IMixed` + - Resolve nullable property: `OpenApiV3_1.IJsonSchema.__ISignificant.nullable` + - Array type utilizes only single `OpenAPI.IJsonSchema.IArray.items` + - Tuple type utilizes only `OpenApi.IJsonSchema.ITuple.prefixItems` + - Merge `OpenApiV3_1.IJsonSchema.IAnyOf` to `OpenApi.IJsonSchema.IOneOf` + - Merge `OpenApiV3_1.IJsonSchema.IRecursiveReference` to `OpenApi.IJsonSchema.IReference` + - Merge `OpenApiV3_1.IJsonSchema.IAllOf` to `OpenApi.IJsonSchema.IObject` -Every features you need from `@samchon/openapi` are in the `OpenApi` module. If you want to connvert emended OpenAPI v3.1 type, just call `OpenApi.convert()` function with your document. Otherwise you want to downgrade from the OpenAPI v3.1 emended specification, call `OpenApi.migrate()` instead. Of course, if you combine both `OpenApi.convert()` and `OpenApi.migrate()` functions, you can transform every OpenAPI versions. +Conversions to another version's OpenAPI document is also based on the "emended OpenAPI v3.1 specification" like above diagram. You can do it through `OpenApi.downgrade()` function. Therefore, if you want to convert Swagger v2.0 document to OpenAPI v3.0 document, you have to call two functions; `OpenApi.convert()` and then `OpenApi.downgrade()`. -By the way, if you want to validate whether your OpenAPI document is following the standard specification or not, you can do it on the playground website. Click one of below links, and paste your OpenAPI URL address. Of course, if you wanna validate in your local machine, just install [`typia`](https://github.com/samchon/typia) and write same code of playground. +At last, if you utilize `typia` library with `@samchon/openapi` types, you can validate whether your OpenAPI document is following the standard specification or not. Just visit one of below playground links, and paste your OpenAPI document URL address. This validation strategy would be superior than any other OpenAPI validator libraries. - - [💻 Type assertion](https://typia.io/playground/?script=JYWwDg9gTgLgBAbzgeTAUwHYEEzADQrra4BqAzAapjsOQPoCMBAygO4CGA5p2lCQExwAvnABmUCCDgAiAAIBndiADGACwgYA9BCLtc0gNwAoUJFhwYAT1zsxEqdKs3DRo8o3z4IdsAxwAvHDs8pYYynAAFACUAFxwAAr2wPJoADwAbhDAACYAfAH5CEZwcJqacADiAKIAKnAAmsgAqgBKKPFVAHJY8QCScAAiyADCTQCyXTXFcO4YnnBQaPKQc2hxLUsrKQFBHMDwomgwahHTJdKqMDBg8jFlUOysAHSc+6oArgBG7ylQszCYGBPdwgTSKFTqLQ6TB6YCabyeXiaNAADyUYAANktNOkyE8AAzaXTAJ4AK3kGmk0yixhKs3m2QgyneIEBcXYGEsO0ePngi2WHjQZIpGGixmmZTgNXqHTgWGYzCqLRqvWQnWmTmA7CewV+MAq73YUGyqTOcAAPoRqKQyIwnr0BkyWYCzZaqMRaHiHU7WRgYK64GwuDw+Px7Y7mb7-SVchFGZHATTXCVJcM1SQlXUasg4FUJp0BlUBtN6fA0L7smhsnF3TRwz7ATta7hgRp0rwYHGG36k3SPBAsU9fKIIBFy5hK9kk0JjN5fNFgexjqoIvSB0LeBIoDSgA) - - [💻 Detailed validation](https://typia.io/playground/?script=JYWwDg9gTgLgBAbzgeTAUwHYEEzADQrra4BqAzAapjsOQPoCMBAygO4CGA5p2lCQExwAvnABmUCCDgAiAAIBndiADGACwgYA9BCLtc0gNwAoUJFhwYAT1zsxEqdKs3DRo8o3z4IdsAxwAvHDs8pYYynAAFACUAFxwAAr2wPJoADwAbhDAACYAfAH5CEZwcJqacADiAKIAKnAAmsgAqgBKKPFVAHJY8QCScAAiyADCTQCyXTXFcO4YnnBQaPKQc2hxLUsrKQFBHMDwomgwahHTJdKqMDBg8jFlUOysAHSc+6oArgBG7ylQszCYGBPdwgTSKFTqLQ6TB6YCabyeXiaNAADyUYAANktNOkyE8AAzaXTAJ4AK3kGmk0yixhKs3m2QgyneIEBcXYGEsO0ePngi2WHjQZIpGGixmmZTgNXqHTgJCwABlegMsDVeshOtN6Xylu8MfBAk5gOwnul2BicuwAakznAAD6EaikMiMJ7KpkswG2h1UYi0PHu5msjAwb1wNhcHh8fhugYe4Ohkq5CKMoOAmnTYCiSL8vVA+TvZTKJbyAL+QKic0pKKIW30iBYp6+UQQCK5-VPXgSKDyDMlEqLGDvKAYWnCVwlSXDDUkKotOo1ZBwKoTToDKoDLUeeBoYPZNDZOK+mix+OAnbH3DAjTpXgwFNnkN9mYeBtC5ut3eYffZDNCYzeL40TAlaJz1o2XbQDSQA) + - Playground Links + - [💻 Type assertion](https://typia.io/playground/?script=JYWwDg9gTgLgBAbzgeTAUwHYEEzADQrra4BqAzAapjsOQPoCMBAygO4CGA5p2lCQExwAvnABmUCCDgAiAAIBndiADGACwgYA9BCLtc0gNwAoUJFhwYAT1zsxEqdKs3DRo8o3z4IdsAxwAvHDs8pYYynAAFACUAFxwAAr2wPJoADwAbhDAACYAfAH5CEZwcJqacADiAKIAKnAAmsgAqgBKKPFVAHJY8QCScAAiyADCTQCyXTXFcO4YnnBQaPKQc2hxLUsrKQFBHMDwomgwahHTJdKqMDBg8jFlUOysAHSc+6oArgBG7ylQszCYGBPdwgTSKFTqLQ6TB6YCabyeXiaNAADyUYAANktNOkyE8AAzaXTAJ4AK3kGmk0yixhKs3m2QgyneIEBcXYGEsO0ePngi2WHjQZIpGGixmmZTgNXqHTgWGYzCqLRqvWQnWmTmA7CewV+MAq73YUGyqTOcAAPoRqKQyIwnr0BkyWYCzZaqMRaHiHU7WRgYK64GwuDw+Px7Y7mb7-SVchFGZHATTXCVJcM1SQlXUasg4FUJp0BlUBtN6fA0L7smhsnF3TRwz7ATta7hgRp0rwYHGG36k3SPBAsU9fKIIBFy5hK9kk0JjN5fNFgexjqoIvSB0LeBIoDSgA) + - [💻 Detailed validation](https://typia.io/playground/?script=JYWwDg9gTgLgBAbzgeTAUwHYEEzADQrra4BqAzAapjsOQPoCMBAygO4CGA5p2lCQExwAvnABmUCCDgAiAAIBndiADGACwgYA9BCLtc0gNwAoUJFhwYAT1zsxEqdKs3DRo8o3z4IdsAxwAvHDs8pYYynAAFACUAFxwAAr2wPJoADwAbhDAACYAfAH5CEZwcJqacADiAKIAKnAAmsgAqgBKKPFVAHJY8QCScAAiyADCTQCyXTXFcO4YnnBQaPKQc2hxLUsrKQFBHMDwomgwahHTJdKqMDBg8jFlUOysAHSc+6oArgBG7ylQszCYGBPdwgTSKFTqLQ6TB6YCabyeXiaNAADyUYAANktNOkyE8AAzaXTAJ4AK3kGmk0yixhKs3m2QgyneIEBcXYGEsO0ePngi2WHjQZIpGGixmmZTgNXqHTgJCwABlegMsDVeshOtN6Xylu8MfBAk5gOwnul2BicuwAakznAAD6EaikMiMJ7KpkswG2h1UYi0PHu5msjAwb1wNhcHh8fhugYe4Ohkq5CKMoOAmnTYCiSL8vVA+TvZTKJbyAL+QKic0pKKIW30iBYp6+UQQCK5-VPXgSKDyDMlEqLGDvKAYWnCVwlSXDDUkKotOo1ZBwKoTToDKoDLUeeBoYPZNDZOK+mix+OAnbH3DAjTpXgwFNnkN9mYeBtC5ut3eYffZDNCYzeL40TAlaJz1o2XbQDSQA) ```typescript import { OpenApi, OpenApiV3, OpenApiV3_1, SwaggerV2 } from "@samchon/openapi"; @@ -144,7 +183,8 @@ main().catch(console.error); -## Related Projects -- `typia`: https://github.com/samchon/typia -- `nestia`: https://github.com/samchon/nestia -- `@wrtnio/openai-function-schema`: https://github.com/wrtnio/openai-function-schema \ No newline at end of file +## LLM Function Calling +### Preface +### Execution +### Keyword Parameter +### Separation \ No newline at end of file diff --git a/test/manual/example.ts b/test/manual/example.ts new file mode 100644 index 0000000..bb137af --- /dev/null +++ b/test/manual/example.ts @@ -0,0 +1,50 @@ +import { + HttpLlm, + IHttpLlmApplication, + IHttpLlmFunction, + OpenApi, + OpenApiV3, + OpenApiV3_1, + SwaggerV2, +} from "@samchon/openapi"; +import fs from "fs"; +import typia from "typia"; + +const main = async (): Promise => { + // read swagger document and validate it + const swagger: + | SwaggerV2.IDocument + | OpenApiV3.IDocument + | OpenApiV3_1.IDocument = JSON.parse( + await fs.promises.readFile("swagger.json", "utf8"), + ); + typia.assert(swagger); + + // convert to emended OpenAPI document, + // and compose LLM function calling application + const document: OpenApi.IDocument = OpenApi.convert(swagger); + const application: IHttpLlmApplication = HttpLlm.application(document); + + // Let's imagine that LLM has selected a function to call + const func: IHttpLlmFunction | undefined = application.functions.find( + (f) => f.path === "/bbs/articles" && f.method === "post", + ); + typia.assertGuard(func); + + // actual execution is by yourself + const article = await HttpLlm.execute({ + connection: { + host: "http://localhost:3000", + }, + application, + function: func, + arguments: [ + { + title: "Hello, world!", + body: "Let's imagine that this argument is composed by LLM.", + }, + ], + }); + console.log("article", article); +}; +main().catch(console.error); From 95aa03ab0e55511e69e074e927290532bd6661c7 Mon Sep 17 00:00:00 2001 From: Jeongho Nam Date: Mon, 9 Sep 2024 04:11:57 +0900 Subject: [PATCH 13/16] Finalize README --- README.md | 266 +++++++++++++++++- package.json | 4 +- src/HttpLlm.ts | 4 +- .../example.ts => examples/execute.ts} | 1 + test/examples/keyword.ts | 63 +++++ test/examples/separate.ts | 71 +++++ 6 files changed, 403 insertions(+), 6 deletions(-) rename test/{manual/example.ts => examples/execute.ts} (98%) create mode 100644 test/examples/keyword.ts create mode 100644 test/examples/separate.ts diff --git a/README.md b/README.md index 1bd4755..c9645f1 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ flowchart end subgraph "Ecosystem" emended --normalizes--> migration[["Migration Schema"]] - migration --"Artificial Intelligence"--> lfc{{"LLM Function Calling Schema"}} + migration --"Artificial Intelligence"--> lfc{{"LLM Function Calling Application"}} end ``` @@ -34,7 +34,7 @@ OpenAPI definitions, converters and utillity functions. - [`ILlmSchema`](https://github.com/samchon/openapi/blob/master/src/structures/ILlmSchema.ts) - [`LlmTypeChecker`](https://github.com/samchon/openapi/blob/master/src/utils/LlmTypeChecker.ts) -> [!NOTE] +> [!TIP] > > LLM selects proper function and fill arguments. > @@ -100,6 +100,7 @@ const main = async (): Promise => { { title: "Hello, world!", body: "Let's imagine that this argument is composed by LLM.", + thumbnail: null, }, ], }); @@ -185,6 +186,265 @@ main().catch(console.error); ## LLM Function Calling ### Preface +```mermaid +flowchart + subgraph "OpenAPI Specification" + v20("Swagger v2.0") --upgrades--> emended[["OpenAPI v3.1 (emended)"]] + v30("OpenAPI v3.0") --upgrades--> emended + v31("OpenAPI v3.1") --emends--> emended + end + subgraph "Ecosystem" + emended --normalizes--> migration[["Migration Schema"]] + migration --"Artificial Intelligence"--> lfc{{"LLM Function Calling Application"}} + end +``` + +LLM function calling application from OpenAPI document. + +`@samchon/openapi` provides LLM (Large Language Model) funtion calling application from the "emended OpenAPI v3.1 document". Therefore, if you have any HTTP backend server and succeeded to build an OpenAPI document, you can easily make the A.I. chatbot application. + +In the A.I. chatbot, LLM will select proper function to remotely call from the conversations with user, and fill arguments of the function automatically. If you actually execute the function call through the `HttpLlm.execute()` funtion, it is the "LLM function call." + +Let's enjoy the fantastic LLM function calling feature very easily with `@samchon/openapi`. + + - [`HttpLlm.application()`](https://github.com/samchon/openapi/blob/master/src/HttpLlm.ts) + - [`IHttpLlmApplication`](https://github.com/samchon/openapi/blob/master/src/structures/ILlmApplication.ts) + - [`IHttpLlmFunction`](https://github.com/samchon/openapi/blob/master/src/structures/ILlmFunction.ts) + - [`ILlmSchema`](https://github.com/samchon/openapi/blob/master/src/structures/ILlmSchema.ts) + - [`LlmTypeChecker`](https://github.com/samchon/openapi/blob/master/src/utils/LlmTypeChecker.ts) + +> [!NOTE] +> +> Preparing playground website utilizing [`web-llm`](https://github.com/mlc-ai/web-llm). + +> [!TIP] +> +> LLM selects proper function and fill arguments. +> +> In nowadays, most LLM (Large Language Model) like OpenAI are supporting "function calling" feature. The "LLM function calling" means that LLM automatically selects a proper function and fills parameter values from conversation with the user (may by chatting text). +> +> https://platform.openai.com/docs/guides/function-calling + ### Execution +Actual function call execution is by yourself. + +LLM (Large Language Model) providers like OpenAI selects a proper function to call from the conversations with users, and fill arguments of it. However, function calling feature supported by LLM providers do not perform the function call execution. The actual execution responsibility is on you. + +In `@samchon/openapi`, you can execute the LLM function calling by `HttpLlm.execute()` (or `HttpLlm.propagate()`) function. Here is an example code executing the LLM function calling through the `HttpLlm.execute()` function. As you can see, to execute the LLM function call, you have to deliver these informations: + + - Connection info to the HTTP server + - Application of the LLM fuction calling + - LLM function schema to call + - Arguments for the function call (maybe composed by LLM) + +```typescript +import { + HttpLlm, + IHttpLlmApplication, + IHttpLlmFunction, + OpenApi, + OpenApiV3, + OpenApiV3_1, + SwaggerV2, +} from "@samchon/openapi"; +import fs from "fs"; +import typia from "typia"; + +const main = async (): Promise => { + // read swagger document and validate it + const swagger: + | SwaggerV2.IDocument + | OpenApiV3.IDocument + | OpenApiV3_1.IDocument = JSON.parse( + await fs.promises.readFile("swagger.json", "utf8"), + ); + typia.assert(swagger); // recommended + + // convert to emended OpenAPI document, + // and compose LLM function calling application + const document: OpenApi.IDocument = OpenApi.convert(swagger); + const application: IHttpLlmApplication = HttpLlm.application(document); + + // Let's imagine that LLM has selected a function to call + const func: IHttpLlmFunction | undefined = application.functions.find( + // (f) => f.name === "llm_selected_fuction_name" + (f) => f.path === "/bbs/articles" && f.method === "post", + ); + if (func === undefined) throw new Error("No matched function exists."); + + // actual execution is by yourself + const article = await HttpLlm.execute({ + connection: { + host: "http://localhost:3000", + }, + application, + function: func, + arguments: [ + "general", + { + title: "Hello, world!", + body: "Let's imagine that this argument is composed by LLM.", + thumbnail: null, + }, + ], + }); + console.log("article", article); +}; +main().catch(console.error); +``` + ### Keyword Parameter -### Separation \ No newline at end of file +Combine parameters into single object. + +If you configure `keyword` option when composing the LLM (Large Language Model) function calling appliation, every parameters of OpenAPI operations would be combined to a single object type in the LLM funtion calling schema. This strategy is loved in many A.I. Chatbot developers, because LLM tends to a little professional in the single parameter function case. + +Also, do not worry about the function call execution case. You don't need to resolve the keyworded parameter manually. The `HttpLlm.execute()` and `HttpLlm.propagate()` functions will resolve the keyworded parameter automatically by analyzing the `IHttpLlmApplication.options` property. + +```typescript +import { + HttpLlm, + IHttpLlmApplication, + IHttpLlmFunction, + OpenApi, + OpenApiV3, + OpenApiV3_1, + SwaggerV2, +} from "@samchon/openapi"; +import fs from "fs"; +import typia from "typia"; +import { v4 } from "uuid"; + +const main = async (): Promise => { + // read swagger document and validate it + const swagger: + | SwaggerV2.IDocument + | OpenApiV3.IDocument + | OpenApiV3_1.IDocument = JSON.parse( + await fs.promises.readFile("swagger.json", "utf8"), + ); + typia.assert(swagger); // recommended + + // convert to emended OpenAPI document, + // and compose LLM function calling application + const document: OpenApi.IDocument = OpenApi.convert(swagger); + const application: IHttpLlmApplication = HttpLlm.application(document, { + keyword: true, + }); + + // Let's imagine that LLM has selected a function to call + const func: IHttpLlmFunction | undefined = application.functions.find( + // (f) => f.name === "llm_selected_fuction_name" + (f) => f.path === "/bbs/articles/{id}" && f.method === "put", + ); + if (func === undefined) throw new Error("No matched function exists."); + + // actual execution is by yourself + const article = await HttpLlm.execute({ + connection: { + host: "http://localhost:3000", + }, + application, + function: func, + arguments: [ + { + section: "general", + id: v4(), + query: { + language: "en-US", + format: "markdown", + }, + body: { + title: "Hello, world!", + body: "Let's imagine that this argument is composed by LLM.", + thumbnail: null, + }, + }, + ], + }); + console.log("article", article); +}; +main().catch(console.error); +``` + +### Separation +Arguments from both Human and LLM sides. + +When composing parameter arguments through the LLM (Large Language Model) function calling, there can be a case that some parameters (or nested properties) must be composed not by LLM, but by Human. File uploading feature, or sensitive information like secret key (password) cases are the representative examples. + +In that case, you can configure the LLM function calling schemas to exclude such Human side parameters (or nested properties) by `IHttpLlmApplication.options.separate` property. Instead, you have to merge both Human and LLM composed parameters into one by calling the `HttpLlm.mergeParameters()` before the LLM function call execution of `HttpLlm.execute()` function. + +Here is the example code separating the file uploading feature from the LLM function calling schema, and combining both Human and LLM composed parameters into one before the LLM function call execution. + +```typescript +import { + HttpLlm, + IHttpLlmApplication, + IHttpLlmFunction, + LlmTypeChecker, + OpenApi, + OpenApiV3, + OpenApiV3_1, + SwaggerV2, +} from "@samchon/openapi"; +import fs from "fs"; +import typia from "typia"; +import { v4 } from "uuid"; + +const main = async (): Promise => { + // read swagger document and validate it + const swagger: + | SwaggerV2.IDocument + | OpenApiV3.IDocument + | OpenApiV3_1.IDocument = JSON.parse( + await fs.promises.readFile("swagger.json", "utf8"), + ); + typia.assert(swagger); // recommended + + // convert to emended OpenAPI document, + // and compose LLM function calling application + const document: OpenApi.IDocument = OpenApi.convert(swagger); + const application: IHttpLlmApplication = HttpLlm.application(document, { + keyword: false, + separate: (schema) => + LlmTypeChecker.isString(schema) && schema.contentMediaType !== undefined, + }); + + // Let's imagine that LLM has selected a function to call + const func: IHttpLlmFunction | undefined = application.functions.find( + // (f) => f.name === "llm_selected_fuction_name" + (f) => f.path === "/bbs/articles/{id}" && f.method === "put", + ); + if (func === undefined) throw new Error("No matched function exists."); + + // actual execution is by yourself + const article = await HttpLlm.execute({ + connection: { + host: "http://localhost:3000", + }, + application, + function: func, + arguments: HttpLlm.mergeParameters({ + function: func, + llm: [ + // LLM composed parameter values + "general", + v4(), + { + language: "en-US", + format: "markdown", + }, + { + title: "Hello, world!", + content: "Let's imagine that this argument is composed by LLM.", + }, + ], + human: [ + // Human composed parameter values + { thumbnail: "https://example.com/thumbnail.jpg" }, + ], + }), + }); + console.log("article", article); +}; +main().catch(console.error); +``` \ No newline at end of file diff --git a/package.json b/package.json index ccde257..70f3183 100644 --- a/package.json +++ b/package.json @@ -44,6 +44,7 @@ "@trivago/prettier-plugin-sort-imports": "^4.3.0", "@types/js-yaml": "^4.0.9", "@types/node": "^20.12.7", + "@types/uuid": "^10.0.0", "chalk": "^4.1.2", "js-yaml": "^4.1.0", "nestia": "^6.0.1", @@ -54,7 +55,8 @@ "ts-patch": "^3.2.1", "typescript": "^5.5.3", "typescript-transform-paths": "^3.4.7", - "typia": "^6.9.0" + "typia": "^6.9.0", + "uuid": "^10.0.0" }, "files": [ "lib", diff --git a/src/HttpLlm.ts b/src/HttpLlm.ts index de94dec..ab625a3 100644 --- a/src/HttpLlm.ts +++ b/src/HttpLlm.ts @@ -111,12 +111,12 @@ export namespace HttpLlm { */ export interface IFetchProps { /** - * Document of the OpenAI function call schemas. + * Application of the LLM function calling. */ application: IHttpLlmApplication; /** - * Function schema to call. + * LLM function schema to call. */ function: IHttpLlmFunction; diff --git a/test/manual/example.ts b/test/examples/execute.ts similarity index 98% rename from test/manual/example.ts rename to test/examples/execute.ts index bb137af..1c47e4f 100644 --- a/test/manual/example.ts +++ b/test/examples/execute.ts @@ -39,6 +39,7 @@ const main = async (): Promise => { application, function: func, arguments: [ + "general", { title: "Hello, world!", body: "Let's imagine that this argument is composed by LLM.", diff --git a/test/examples/keyword.ts b/test/examples/keyword.ts new file mode 100644 index 0000000..e875f71 --- /dev/null +++ b/test/examples/keyword.ts @@ -0,0 +1,63 @@ +import { + HttpLlm, + IHttpLlmApplication, + IHttpLlmFunction, + OpenApi, + OpenApiV3, + OpenApiV3_1, + SwaggerV2, +} from "@samchon/openapi"; +import fs from "fs"; +import typia from "typia"; +import { v4 } from "uuid"; + +const main = async (): Promise => { + // read swagger document and validate it + const swagger: + | SwaggerV2.IDocument + | OpenApiV3.IDocument + | OpenApiV3_1.IDocument = JSON.parse( + await fs.promises.readFile("swagger.json", "utf8"), + ); + typia.assert(swagger); // recommended + + // convert to emended OpenAPI document, + // and compose LLM function calling application + const document: OpenApi.IDocument = OpenApi.convert(swagger); + const application: IHttpLlmApplication = HttpLlm.application(document, { + keyword: true, + }); + + // Let's imagine that LLM has selected a function to call + const func: IHttpLlmFunction | undefined = application.functions.find( + // (f) => f.name === "llm_selected_fuction_name" + (f) => f.path === "/bbs/articles/{id}" && f.method === "put", + ); + if (func === undefined) throw new Error("No matched function exists."); + + // actual execution is by yourself + const article = await HttpLlm.execute({ + connection: { + host: "http://localhost:3000", + }, + application, + function: func, + arguments: [ + { + section: "general", + id: v4(), + query: { + language: "en-US", + format: "markdown", + }, + body: { + title: "Hello, world!", + body: "Let's imagine that this argument is composed by LLM.", + thumbnail: null, + }, + }, + ], + }); + console.log("article", article); +}; +main().catch(console.error); diff --git a/test/examples/separate.ts b/test/examples/separate.ts new file mode 100644 index 0000000..feb6e3d --- /dev/null +++ b/test/examples/separate.ts @@ -0,0 +1,71 @@ +import { + HttpLlm, + IHttpLlmApplication, + IHttpLlmFunction, + LlmTypeChecker, + OpenApi, + OpenApiV3, + OpenApiV3_1, + SwaggerV2, +} from "@samchon/openapi"; +import fs from "fs"; +import typia from "typia"; +import { v4 } from "uuid"; + +const main = async (): Promise => { + // read swagger document and validate it + const swagger: + | SwaggerV2.IDocument + | OpenApiV3.IDocument + | OpenApiV3_1.IDocument = JSON.parse( + await fs.promises.readFile("swagger.json", "utf8"), + ); + typia.assert(swagger); // recommended + + // convert to emended OpenAPI document, + // and compose LLM function calling application + const document: OpenApi.IDocument = OpenApi.convert(swagger); + const application: IHttpLlmApplication = HttpLlm.application(document, { + keyword: false, + separate: (schema) => + LlmTypeChecker.isString(schema) && schema.contentMediaType !== undefined, + }); + + // Let's imagine that LLM has selected a function to call + const func: IHttpLlmFunction | undefined = application.functions.find( + // (f) => f.name === "llm_selected_fuction_name" + (f) => f.path === "/bbs/articles/{id}" && f.method === "put", + ); + if (func === undefined) throw new Error("No matched function exists."); + + // actual execution is by yourself + const article = await HttpLlm.execute({ + connection: { + host: "http://localhost:3000", + }, + application, + function: func, + arguments: HttpLlm.mergeParameters({ + function: func, + llm: [ + // LLM composed parameter values + "general", + v4(), + { + language: "en-US", + format: "markdown", + }, + { + title: "Hello, world!", + content: "Let's imagine that this argument is composed by LLM.", + }, + ], + human: [ + // Human composed parameter values + { thumbnail: "https://example.com/thumbnail.jpg" }, + ], + }), + }); + console.log("article", article); +}; +main().catch(console.error); From c55206ccea0c25aac40a7b0d2b3999d7351716dd Mon Sep 17 00:00:00 2001 From: Jeongho Nam Date: Mon, 9 Sep 2024 04:17:10 +0900 Subject: [PATCH 14/16] README styling. --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index c9645f1..51afd39 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ flowchart v31("OpenAPI v3.1") --emends--> emended end subgraph "Ecosystem" - emended --normalizes--> migration[["Migration Schema"]] + emended --normalizes--> migration[["Migration Schema"]] migration --"Artificial Intelligence"--> lfc{{"LLM Function Calling Application"}} end ``` @@ -194,8 +194,8 @@ flowchart v31("OpenAPI v3.1") --emends--> emended end subgraph "Ecosystem" - emended --normalizes--> migration[["Migration Schema"]] - migration --"Artificial Intelligence"--> lfc{{"LLM Function Calling Application"}} + emended --normalizes--> migration[["Migration Schema"]] + migration --"Artificial Intelligence"--> lfc{{"LLM Function Calling Application"}} end ``` From e2f90aafce59bb7dde4d501c813cb00a6df5f266 Mon Sep 17 00:00:00 2001 From: Jeongho Nam Date: Mon, 9 Sep 2024 04:20:51 +0900 Subject: [PATCH 15/16] README code editing --- README.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 51afd39..ce5af46 100644 --- a/README.md +++ b/README.md @@ -249,6 +249,7 @@ import { } from "@samchon/openapi"; import fs from "fs"; import typia from "typia"; +import { v4 } from "uuid"; const main = async (): Promise => { // read swagger document and validate it @@ -268,7 +269,7 @@ const main = async (): Promise => { // Let's imagine that LLM has selected a function to call const func: IHttpLlmFunction | undefined = application.functions.find( // (f) => f.name === "llm_selected_fuction_name" - (f) => f.path === "/bbs/articles" && f.method === "post", + (f) => f.path === "/bbs/{section}/articles/{id}" && f.method === "put", ); if (func === undefined) throw new Error("No matched function exists."); @@ -281,6 +282,7 @@ const main = async (): Promise => { function: func, arguments: [ "general", + v4(), { title: "Hello, world!", body: "Let's imagine that this argument is composed by LLM.", @@ -334,7 +336,7 @@ const main = async (): Promise => { // Let's imagine that LLM has selected a function to call const func: IHttpLlmFunction | undefined = application.functions.find( // (f) => f.name === "llm_selected_fuction_name" - (f) => f.path === "/bbs/articles/{id}" && f.method === "put", + (f) => f.path === "/bbs/{section}/articles/{id}" && f.method === "put", ); if (func === undefined) throw new Error("No matched function exists."); @@ -346,6 +348,7 @@ const main = async (): Promise => { application, function: func, arguments: [ + // one single object with key-value paired { section: "general", id: v4(), From 99f2f0572355987d52451de851815d4a1d09433c Mon Sep 17 00:00:00 2001 From: Jeongho Nam Date: Mon, 9 Sep 2024 04:26:25 +0900 Subject: [PATCH 16/16] Publish v1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 70f3183..b63cf87 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@samchon/openapi", - "version": "1.0.0-dev.20240908", + "version": "1.0.0", "description": "OpenAPI definitions and converters for 'typia' and 'nestia'.", "main": "./lib/index.js", "module": "./lib/index.mjs",