diff --git a/.gitignore b/.gitignore index f84e69a..6ff7771 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,5 @@ node_modules/ package-lock.json pnpm-lock.yaml -*.log \ No newline at end of file +*.log +.env \ No newline at end of file diff --git a/examples/function-calling/recursive.input.json b/examples/function-calling/recursive.input.json new file mode 100644 index 0000000..f6ee07f --- /dev/null +++ b/examples/function-calling/recursive.input.json @@ -0,0 +1,127 @@ +[ + { + "id": "1", + "name": "electronics", + "children": [ + { + "id": "1.1", + "name": "desktops", + "children": [] + }, + { + "id": "1.2", + "name": "laptops", + "children": [ + { + "id": "1.2.1", + "name": "ultrabooks", + "children": [] + }, + { + "id": "1.2.2", + "name": "macbooks", + "children": [] + }, + { + "id": "1.2.3", + "name": "desknotes", + "children": [] + }, + { + "id": "1.2.4", + "name": "2 in 1 laptops", + "children": [] + } + ] + }, + { + "id": "1.3", + "name": "tablets", + "children": [ + { + "id": "1.3.1", + "name": "ipads", + "children": [] + }, + { + "id": "1.3.2", + "name": "android tablets", + "children": [] + }, + { + "id": "1.3.3", + "name": "windows tablets", + "children": [] + } + ] + }, + { + "id": "1.4", + "name": "smartphones", + "children": [ + { + "id": "1.4.1", + "name": "mini smartphones", + "children": [] + }, + { + "id": "1.4.2", + "name": "phablets", + "children": [] + }, + { + "id": "1.4.3", + "name": "gaming smartphones", + "children": [] + }, + { + "id": "1.4.4", + "name": "rugged smartphones", + "children": [] + }, + { + "id": "1.4.5", + "name": "foldable smartphones", + "children": [] + } + ] + }, + { + "id": "1.5", + "name": "cameras", + "children": [] + }, + { + "id": "1.6", + "name": "televisions", + "children": [] + } + ] + }, + { + "id": "2", + "name": "furnitures", + "children": [] + }, + { + "id": "3", + "name": "accessories", + "children": [ + { + "id": "3.1", + "name": "jewelry", + "children": [] + }, + { + "id": "3.2", + "name": "clothing", + "children": [] + }, + { + "id": "3.3", + "name": "shoes", + "children": [] + } + ] + } +] \ No newline at end of file diff --git a/examples/function-calling/recursive.schema.json b/examples/function-calling/recursive.schema.json new file mode 100644 index 0000000..90ba9e1 --- /dev/null +++ b/examples/function-calling/recursive.schema.json @@ -0,0 +1,40 @@ +{ + "type": "object", + "properties": { + "input": { + "type": "array", + "items": { + "$ref": "#/$defs/IShoppingCategory" + } + } + }, + "required": [ + "input" + ], + "additionalProperties": false, + "$defs": { + "IShoppingCategory": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "children": { + "type": "array", + "items": { + "$ref": "#/$defs/IShoppingCategory" + } + } + }, + "required": [ + "id", + "name", + "children" + ], + "additionalProperties": false + } + } +} \ No newline at end of file diff --git a/examples/function-calling/sale.input.json b/examples/function-calling/sale.input.json new file mode 100644 index 0000000..fa23cb5 --- /dev/null +++ b/examples/function-calling/sale.input.json @@ -0,0 +1,244 @@ +{ + "section_code": "default", + "status": null, + "opened_at": null, + "closed_at": null, + "content": { + "title": "Surface Pro 8", + "format": "txt", + "body": "The best laptop for your daily needs.", + "files": [], + "thumbnails": [] + }, + "units": [ + { + "options": [ + { + "type": "select", + "name": "CPU", + "variable": true, + "candidates": [ + { + "name": "Intel Core i3" + }, + { + "name": "Intel Core i5" + }, + { + "name": "Intel Core i7" + } + ] + }, + { + "type": "select", + "name": "RAM", + "variable": true, + "candidates": [ + { + "name": "8 GB" + }, + { + "name": "16 GB" + }, + { + "name": "32 GB" + } + ] + }, + { + "type": "select", + "name": "Storage", + "variable": true, + "candidates": [ + { + "name": "128 GB" + }, + { + "name": "256 GB" + }, + { + "name": "512 GB" + } + ] + } + ], + "stocks": [ + { + "name": "(i3, 8 GB, 128 GB)", + "price": { + "nominal": 999, + "real": 899 + }, + "quantity": 1000, + "choices": [ + { + "option_index": 0, + "candidate_index": 0 + }, + { + "option_index": 1, + "candidate_index": 0 + }, + { + "option_index": 2, + "candidate_index": 0 + } + ] + }, + { + "name": "(i3, 16 GB, 256 GB)", + "price": { + "nominal": 1199, + "real": 1099 + }, + "quantity": 1000, + "choices": [ + { + "option_index": 0, + "candidate_index": 0 + }, + { + "option_index": 1, + "candidate_index": 1 + }, + { + "option_index": 2, + "candidate_index": 1 + } + ] + }, + { + "name": "(i3, 16 GB, 512 GB)", + "price": { + "nominal": 1399, + "real": 1299 + }, + "quantity": 1000, + "choices": [ + { + "option_index": 0, + "candidate_index": 0 + }, + { + "option_index": 1, + "candidate_index": 1 + }, + { + "option_index": 2, + "candidate_index": 2 + } + ] + }, + { + "name": "(i5, 16 GB, 256 GB)", + "price": { + "nominal": 1499, + "real": 1399 + }, + "quantity": 1000, + "choices": [ + { + "option_index": 0, + "candidate_index": 1 + }, + { + "option_index": 1, + "candidate_index": 1 + }, + { + "option_index": 2, + "candidate_index": 1 + } + ] + }, + { + "name": "(i5, 32 GB, 512 GB)", + "price": { + "nominal": 1799, + "real": 1699 + }, + "quantity": 1000, + "choices": [ + { + "option_index": 0, + "candidate_index": 1 + }, + { + "option_index": 1, + "candidate_index": 2 + }, + { + "option_index": 2, + "candidate_index": 2 + } + ] + }, + { + "name": "(i7, 16 GB, 512 GB)", + "price": { + "nominal": 1799, + "real": 1699 + }, + "quantity": 1000, + "choices": [ + { + "option_index": 0, + "candidate_index": 2 + }, + { + "option_index": 1, + "candidate_index": 1 + }, + { + "option_index": 2, + "candidate_index": 2 + } + ] + }, + { + "name": "(i7, 32 GB, 512 GB)", + "price": { + "nominal": 1999, + "real": 1899 + }, + "quantity": 1000, + "choices": [ + { + "option_index": 0, + "candidate_index": 2 + }, + { + "option_index": 1, + "candidate_index": 2 + }, + { + "option_index": 2, + "candidate_index": 2 + } + ] + } + ], + "name": "Surface Pro 8 Entity", + "required": true, + "primary": false + }, + { + "options": [], + "stocks": [ + { + "name": "Warranty Program", + "price": { + "nominal": 99, + "real": 89 + }, + "quantity": 1000, + "choices": [] + } + ], + "name": "Warranty Program", + "required": false, + "primary": false + } + ], + "tags": [] +} \ No newline at end of file diff --git a/examples/function-calling/sale.schema.json b/examples/function-calling/sale.schema.json new file mode 100644 index 0000000..f2e6af9 --- /dev/null +++ b/examples/function-calling/sale.schema.json @@ -0,0 +1,367 @@ +{ + "type": "object", + "properties": { + "input": { + "description": "Creation information of sale.", + "type": "object", + "properties": { + "section_code": { + "title": "Belonged section's {@link IShoppingSection.code}", + "description": "Belonged section's {@link IShoppingSection.code}.", + "type": "string" + }, + "status": { + "title": "Initial status of the sale", + "description": "Initial status of the sale.\n\n`null` or `undefined`: No restriction\n`paused`: Starts with {@link ITimestamps.paused_at paused} status\n`suspended`: Starts with {@link ITimestamps.suspended_at suspended} status", + "anyOf": [ + { + "type": "null" + }, + { + "type": "string", + "enum": [ + "paused", + "suspended" + ] + } + ] + }, + "opened_at": { + "title": "Opening time of the sale", + "description": "Opening time of the sale.", + "anyOf": [ + { + "type": "null" + }, + { + "type": "string" + } + ] + }, + "closed_at": { + "title": "Closing time of the sale", + "description": "Closing time of the sale.\n\nIf this value is `null`, the sale be continued forever.", + "anyOf": [ + { + "type": "null" + }, + { + "type": "string" + } + ] + }, + "content": { + "type": "object", + "properties": { + "title": { + "type": "string" + }, + "format": { + "type": "string", + "enum": [ + "html", + "md", + "txt" + ] + }, + "body": { + "type": "string" + }, + "files": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "title": "File name, except extension", + "description": "File name, except extension.\n\nIf there's file `.gitignore`, then its name is an empty string.", + "type": "string" + }, + "extension": { + "title": "Extension", + "description": "Extension.\n\nPossible to omit like `README` case.", + "anyOf": [ + { + "type": "null" + }, + { + "type": "string" + } + ] + }, + "url": { + "title": "URL path of the real file", + "description": "URL path of the real file.", + "type": "string" + } + }, + "required": [ + "name", + "extension", + "url" + ], + "additionalProperties": false + } + }, + "thumbnails": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "title": "File name, except extension", + "description": "File name, except extension.\n\nIf there's file `.gitignore`, then its name is an empty string.", + "type": "string" + }, + "extension": { + "title": "Extension", + "description": "Extension.\n\nPossible to omit like `README` case.", + "anyOf": [ + { + "type": "null" + }, + { + "type": "string" + } + ] + }, + "url": { + "title": "URL path of the real file", + "description": "URL path of the real file.", + "type": "string" + } + }, + "required": [ + "name", + "extension", + "url" + ], + "additionalProperties": false + } + } + }, + "required": [ + "title", + "format", + "body", + "files", + "thumbnails" + ], + "additionalProperties": false + }, + "units": { + "type": "array", + "items": { + "description": "Creation information of sale unit.", + "type": "object", + "properties": { + "options": { + "title": "List of options", + "description": "List of options.", + "type": "array", + "items": { + "anyOf": [ + { + "type": "object", + "properties": { + "type": { + "title": "Type of descriptive option", + "description": "Type of descriptive option.\n\nWhich typed value should be written when purchasing.", + "type": "string", + "enum": [ + "string", + "number", + "boolean" + ] + }, + "name": { + "title": "Readable name of the option", + "description": "Readable name of the option.", + "type": "string" + } + }, + "required": [ + "type", + "name" + ], + "description": "Creation information of the descriptive option.", + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "type": { + "title": "Discriminant for the type of selectable option", + "description": "Discriminant for the type of selectable option.", + "type": "string", + "enum": [ + "select" + ] + }, + "name": { + "title": "Represents the name of the option", + "description": "Represents the name of the option.", + "type": "string" + }, + "variable": { + "title": "Whether the option is variable or not", + "description": "Whether the option is variable or not.\n\nWhen type of current option is \"select\", this attribute means whether\nselecting different candidate value affects the final stock or not.", + "type": "boolean" + }, + "candidates": { + "title": "List of candidate values", + "description": "List of candidate values.", + "type": "array", + "items": { + "description": "Creation information of the candidate value.", + "type": "object", + "properties": { + "name": { + "title": "Represents the name of the candidate value", + "description": "Represents the name of the candidate value.", + "type": "string" + } + }, + "required": [ + "name" + ], + "additionalProperties": false + } + } + }, + "required": [ + "type", + "name", + "variable", + "candidates" + ], + "description": "Creation information of the selectable option.", + "additionalProperties": false + } + ] + } + }, + "stocks": { + "title": "List of final stocks", + "description": "List of final stocks.", + "type": "array", + "items": { + "description": "Creation information of the stock.", + "type": "object", + "properties": { + "name": { + "title": "Representative name of the stock", + "description": "Representative name of the stock.", + "type": "string" + }, + "price": { + "title": "Price of the stock", + "description": "Shopping price interface.", + "type": "object", + "properties": { + "nominal": { + "title": "Nominal price", + "description": "Nominal price.\n\nThis is not {@link real real price} to pay, but just a nominal price to show.\nIf this value is greater than the {@link real real price}, it would be shown\nlike {@link IShoppingSeller seller} is giving a discount.", + "type": "number" + }, + "real": { + "title": "Real price to pay", + "description": "Real price to pay.", + "type": "number" + } + }, + "required": [ + "nominal", + "real" + ], + "additionalProperties": false + }, + "quantity": { + "title": "Initial inventory quantity", + "description": "Initial inventory quantity.", + "type": "integer" + }, + "choices": { + "title": "List of choices", + "description": "List of choices.\n\nWhich candidate values being chosen for each option.", + "type": "array", + "items": { + "description": "Creation information of stock choice.", + "type": "object", + "properties": { + "option_index": { + "description": "Target option's index number in\n{@link IShoppingSaleUnit.ICreate.options}.", + "type": "integer" + }, + "candidate_index": { + "description": "Target candidate's index number in\n{@link IShoppingSaleUnitSelectableOption.ICreate.candidates}.", + "type": "integer" + } + }, + "required": [ + "option_index", + "candidate_index" + ], + "additionalProperties": false + } + } + }, + "required": [ + "name", + "price", + "quantity", + "choices" + ], + "additionalProperties": false + } + }, + "name": { + "title": "Representative name of the unit", + "description": "Representative name of the unit.", + "type": "string" + }, + "required": { + "title": "Whether the unit is required or not", + "description": "Whether the unit is required or not.\n\nWhen the unit is required, the customer must select the unit. If do not\nselect, customer can't buy it.\n\nFor example, if there's a sale \"Macbook Set\" and one of the unit is the\n\"Main Body\", is it possible to buy the \"Macbook Set\" without the\n\"Main Body\" unit? This property is for that case.", + "type": "boolean" + }, + "primary": { + "title": "Whether the unit is primary or not", + "description": "Whether the unit is primary or not.\n\nJust a labeling value.", + "type": "boolean" + } + }, + "required": [ + "options", + "stocks", + "name", + "required", + "primary" + ], + "additionalProperties": false + } + }, + "tags": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "section_code", + "status", + "opened_at", + "closed_at", + "content", + "units", + "tags" + ], + "additionalProperties": false + } + }, + "required": [ + "input" + ], + "additionalProperties": false +} \ No newline at end of file diff --git a/examples/function-calling/tags.input.json b/examples/function-calling/tags.input.json new file mode 100644 index 0000000..f580d57 --- /dev/null +++ b/examples/function-calling/tags.input.json @@ -0,0 +1,7 @@ +{ + "reasons": [ + "marketing sales" + ], + "temporary": false, + "time": null +} \ No newline at end of file diff --git a/examples/function-calling/tags.schema.json b/examples/function-calling/tags.schema.json new file mode 100644 index 0000000..0e3e313 --- /dev/null +++ b/examples/function-calling/tags.schema.json @@ -0,0 +1,44 @@ +{ + "type": "object", + "properties": { + "input": { + "$ref": "#/$defs/OpeningTime" + } + }, + "required": [ + "input" + ], + "additionalProperties": false, + "$defs": { + "OpeningTime": { + "type": "object", + "properties": { + "reasons": { + "type": "array", + "items": { + "type": "string" + } + }, + "temporary": { + "type": "boolean" + }, + "time": { + "anyOf": [ + { + "type": "null" + }, + { + "type": "string" + } + ] + } + }, + "required": [ + "reasons", + "temporary", + "time" + ], + "additionalProperties": false + } + } +} \ No newline at end of file diff --git a/package.json b/package.json index ab0a574..fa29ab1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@samchon/openapi", - "version": "2.0.0-dev.20241113", + "version": "2.0.0-dev.20241119-2", "description": "OpenAPI definitions and converters for 'typia' and 'nestia'.", "main": "./lib/index.js", "module": "./lib/index.mjs", @@ -46,14 +46,18 @@ "@types/node": "^20.12.7", "@types/uuid": "^10.0.0", "chalk": "^4.1.2", + "dotenv": "^16.4.5", + "dotenv-expand": "^12.0.0", "js-yaml": "^4.1.0", "nestia": "^6.0.1", + "openai": "^4.72.0", "prettier": "^3.2.5", "rimraf": "^5.0.5", "rollup": "^4.18.1", "source-map-support": "^0.5.21", "ts-node": "^10.9.2", "ts-patch": "^3.2.1", + "tstl": "^3.0.0", "typescript": "^5.6.3", "typescript-transform-paths": "^3.4.7", "typia": "7.0.0-dev.20241115", diff --git a/src/HttpLlm.ts b/src/HttpLlm.ts index d4e0606..cb194b6 100644 --- a/src/HttpLlm.ts +++ b/src/HttpLlm.ts @@ -68,54 +68,38 @@ export namespace HttpLlm { */ export const application = < Model extends IHttpLlmApplication.Model, - Schema extends - | ILlmSchemaV3 - | ILlmSchemaV3_1 - | IChatGptSchema - | IGeminiSchema = IHttpLlmApplication.ModelSchema[Model], + Parameters extends + IHttpLlmApplication.ModelParameters[Model] = IHttpLlmApplication.ModelParameters[Model], Operation extends OpenApi.IOperation = OpenApi.IOperation, >(props: { model: Model; document: | OpenApi.IDocument | IHttpMigrateApplication; - options?: Partial>; - }): IHttpLlmApplication => { + options?: Partial< + IHttpLlmApplication.IOptions + >; + }): IHttpLlmApplication => { // MIGRATE const migrate: IHttpMigrateApplication = (props.document as OpenApi.IDocument)["x-samchon-emended"] === true ? HttpMigration.application(props.document as OpenApi.IDocument) : (props.document as IHttpMigrateApplication); - return HttpLlmConverter.compose({ + return HttpLlmConverter.compose({ migrate, model: props.model, options: { - keyword: props.options?.keyword ?? false, separate: props.options?.separate ?? null, recursive: (props.model === "chatgpt" ? undefined : (props.options?.recursive ?? 3)) as IHttpLlmApplication.IOptions< Model, - Schema + Parameters["properties"][string] >["recursive"], }, }); }; - export const schema = < - Model extends IHttpLlmApplication.Model, - Schema extends - | ILlmSchemaV3 - | ILlmSchemaV3_1 - | IChatGptSchema - | IGeminiSchema = IHttpLlmApplication.ModelSchema[Model], - >(props: { - model: Model; - components: OpenApi.IComponents; - schema: OpenApi.IJsonSchema; - recursive: false | number; - }): Schema | null => HttpLlmConverter.schema(props); - /* ----------------------------------------------------------- FETCHERS ----------------------------------------------------------- */ @@ -124,11 +108,8 @@ export namespace HttpLlm { */ export interface IFetchProps< Model extends IHttpLlmApplication.Model, - Schema extends - | ILlmSchemaV3 - | ILlmSchemaV3_1 - | IChatGptSchema - | IGeminiSchema = IHttpLlmApplication.ModelSchema[Model], + Parameters extends + IHttpLlmApplication.ModelParameters[Model] = IHttpLlmApplication.ModelParameters[Model], Operation extends OpenApi.IOperation = OpenApi.IOperation, Route extends IHttpMigrateRoute = IHttpMigrateRoute< OpenApi.IJsonSchema, @@ -138,12 +119,12 @@ export namespace HttpLlm { /** * Application of the LLM function calling. */ - application: IHttpLlmApplication; + application: IHttpLlmApplication; /** * LLM function schema to call. */ - function: IHttpLlmFunction; + function: IHttpLlmFunction; /** * Connection info to the HTTP server. @@ -151,9 +132,9 @@ export namespace HttpLlm { connection: IHttpConnection; /** - * Arguments for the function call. + * Input arguments for the function call. */ - arguments: any[]; + input: object; } /** @@ -183,14 +164,11 @@ export namespace HttpLlm { */ export const execute = < Model extends IHttpLlmApplication.Model, - Schema extends - | ILlmSchemaV3 - | ILlmSchemaV3_1 - | IChatGptSchema - | IGeminiSchema = IHttpLlmApplication.ModelSchema[Model], + Parameters extends + IHttpLlmApplication.ModelParameters[Model] = IHttpLlmApplication.ModelParameters[Model], Operation extends OpenApi.IOperation = OpenApi.IOperation, >( - props: IFetchProps, + props: IFetchProps, ): Promise => HttpLlmFunctionFetcher.execute(props); /** @@ -219,14 +197,11 @@ export namespace HttpLlm { */ export const propagate = < Model extends IHttpLlmApplication.Model, - Schema extends - | ILlmSchemaV3 - | ILlmSchemaV3_1 - | IChatGptSchema - | IGeminiSchema = IHttpLlmApplication.ModelSchema[Model], + Parameters extends + IHttpLlmApplication.ModelParameters[Model] = IHttpLlmApplication.ModelParameters[Model], Operation extends OpenApi.IOperation = OpenApi.IOperation, >( - props: IFetchProps, + props: IFetchProps, ): Promise => HttpLlmFunctionFetcher.propagate(props); /* ----------------------------------------------------------- @@ -236,26 +211,26 @@ export namespace HttpLlm { * Properties for the parameters' merging. */ export interface IMergeProps< - Schema extends - | ILlmSchemaV3 - | ILlmSchemaV3_1 - | IChatGptSchema - | IGeminiSchema, + Parameters extends + | ILlmSchemaV3.IParameters + | ILlmSchemaV3_1.IParameters + | IChatGptSchema.IParameters + | IGeminiSchema.IParameters, > { /** * Metadata of the target function. */ - function: ILlmFunction; + function: ILlmFunction; /** * Arguments composed by the LLM. */ - llm: unknown[]; + llm: object | null; /** * Arguments composed by the human. */ - human: unknown[]; + human: object | null; } /** @@ -274,14 +249,14 @@ export namespace HttpLlm { * @returns Merged parameter values */ export const mergeParameters = < - Schema extends - | ILlmSchemaV3 - | ILlmSchemaV3_1 - | IChatGptSchema - | IGeminiSchema, + Parameters extends + | ILlmSchemaV3.IParameters + | ILlmSchemaV3_1.IParameters + | IChatGptSchema.IParameters + | IGeminiSchema.IParameters, >( - props: IMergeProps, - ): unknown[] => LlmDataMerger.parameters(props); + props: IMergeProps, + ): object => LlmDataMerger.parameters(props); /** * Merge two values. diff --git a/src/OpenApi.ts b/src/OpenApi.ts index b09d960..20564ed 100644 --- a/src/OpenApi.ts +++ b/src/OpenApi.ts @@ -945,7 +945,7 @@ export namespace OpenApi { * If you need additional properties that is represented by dynamic key, * you can use the {@link additionalProperties} instead. */ - properties?: Record; + properties: Record; /** * Additional properties' info. diff --git a/src/converters/ChatGptConverter.ts b/src/converters/ChatGptConverter.ts index 2d9f08b..16557f1 100644 --- a/src/converters/ChatGptConverter.ts +++ b/src/converters/ChatGptConverter.ts @@ -4,178 +4,277 @@ import { ChatGptTypeChecker } from "../utils/ChatGptTypeChecker"; import { OpenApiTypeChecker } from "../utils/OpenApiTypeChecker"; export namespace ChatGptConverter { - export const schema = (props: { + export const parameters = (props: { components: OpenApi.IComponents; - schema: OpenApi.IJsonSchema; - }): IChatGptSchema.ITop | null => { + schema: OpenApi.IJsonSchema.IObject; + escape?: boolean; + tag?: boolean; + }): IChatGptSchema.IParameters | null => { const $defs: Record = {}; - const schema: IChatGptSchema.ITop | null = convertSchema({ + const res: IChatGptSchema.IParameters | null = schema({ components: props.components, schema: props.schema, + escape: props.escape, + tag: props.tag, $defs, - }); - if (schema === null) return null; - else if (Object.keys($defs).length) schema.$defs = $defs; - return schema; + }) as IChatGptSchema.IParameters | null; + if (res === null) return null; + else if (Object.keys($defs).length) res.$defs = $defs; + return res; }; - const convertSchema = (props: { + export const schema = (props: { components: OpenApi.IComponents; - $defs: Record; schema: OpenApi.IJsonSchema; + $defs: Record; + escape?: boolean; + tag?: boolean; }): IChatGptSchema | null => { - if (OpenApiTypeChecker.isReference(props.schema)) { - const key: string = props.schema.$ref.split("#/components/schemas/")[1]; - const target: OpenApi.IJsonSchema | undefined = - props.components.schemas?.[key]; - if (target === undefined) return null; - - const out = () => ({ - ...props.schema, - $ref: `#/$defs/${key}`, - }); - if (props.$defs[key] !== undefined) return out(); - props.$defs[key] = {}; - const converted: IChatGptSchema | null = convertSchema({ - components: props.components, - $defs: props.$defs, - schema: target, - }); - if (converted === null) return null; - - props.$defs[key] = converted; - return out(); - } else if (OpenApiTypeChecker.isArray(props.schema)) { - const items: IChatGptSchema | null = convertSchema({ - components: props.components, - $defs: props.$defs, - schema: props.schema.items, - }); - if (items === null) return null; - return { - ...props.schema, - items, - }; - } else if (OpenApiTypeChecker.isTuple(props.schema)) { - const prefixItems: Array = - props.schema.prefixItems.map((item) => - convertSchema({ + const union: Array = []; + const attribute: IChatGptSchema.__IAttribute = { + title: props.schema.title, + description: props.schema.description, + example: props.schema.example, + examples: props.schema.examples, + ...Object.fromEntries( + Object.entries(props.schema).filter( + ([key, value]) => key.startsWith("x-") && value !== undefined, + ), + ), + }; + const visit = (input: OpenApi.IJsonSchema): number => { + if (OpenApiTypeChecker.isReference(input)) { + const key: string = input.$ref.split("#/components/schemas/")[1]; + const target: OpenApi.IJsonSchema | undefined = + props.components.schemas?.[key]; + if (target === undefined) return 0; + if ( + !!props.escape === false || + OpenApiTypeChecker.isRecursiveReference({ components: props.components, - $defs: props.$defs, - schema: item, - }), - ); - if (prefixItems.some((v) => v === null)) return null; - const additionalItems = - props.schema.additionalItems === undefined - ? false - : typeof props.schema.additionalItems === "object" && - props.schema.additionalItems !== null - ? convertSchema({ - components: props.components, - $defs: props.$defs, - schema: props.schema.additionalItems, - }) - : props.schema.additionalItems; - if (additionalItems === null) return null; - return { - ...props.schema, - prefixItems: prefixItems.filter((v) => v !== null), - additionalItems, - }; - } else if (OpenApiTypeChecker.isObject(props.schema)) { - const properties: Record = Object.entries( - props.schema.properties || {}, - ).reduce( - (acc, [key, value]) => { - const converted: IChatGptSchema | null = convertSchema({ + schema: input, + }) + ) { + const out = () => + union.push({ + ...input, + $ref: `#/$defs/${key}`, + title: undefined, + description: undefined, + } as IChatGptSchema); + if (props.$defs[key] !== undefined) return out(); + props.$defs[key] = {}; + const converted: IChatGptSchema | null = schema({ components: props.components, $defs: props.$defs, - schema: value, + escape: props.escape, + tag: props.tag, + schema: target, }); - if (converted === null) return acc; - acc[key] = converted; - return acc; - }, - {} as Record, - ); - if (Object.values(properties).some((v) => v === null)) return null; - const additionalProperties = - props.schema.additionalProperties === undefined - ? false - : typeof props.schema.additionalProperties === "object" && - props.schema.additionalProperties !== null - ? convertSchema({ + if (converted === null) return union.push(null); + props.$defs[key] = converted; + return out(); + } else return visit(target); + } else if (OpenApiTypeChecker.isArray(input)) { + const items: IChatGptSchema | null = schema({ + components: props.components, + $defs: props.$defs, + escape: props.escape, + tag: props.tag, + schema: input.items, + }); + if (items === null) return union.push(null); + return union.push({ + ...input, + items, + ...(!!props.tag + ? {} + : { + maxItems: undefined, + minItems: undefined, + }), + }); + } else if (OpenApiTypeChecker.isTuple(input)) { + const prefixItems: Array = input.prefixItems.map( + (item) => + schema({ + components: props.components, + $defs: props.$defs, + escape: props.escape, + tag: props.tag, + schema: item, + }), + ); + if (prefixItems.some((v) => v === null)) return union.push(null); + const additionalItems = + input.additionalItems === undefined + ? false + : typeof input.additionalItems === "object" && + input.additionalItems !== null + ? schema({ + components: props.components, + $defs: props.$defs, + escape: props.escape, + tag: props.tag, + schema: input.additionalItems, + }) + : input.additionalItems; + if (additionalItems === null) return union.push(null); + return union.push({ + ...input, + prefixItems: prefixItems.filter((v) => v !== null), + additionalItems, + }); + } else if (OpenApiTypeChecker.isObject(input)) { + const properties: Record = + Object.entries(input.properties || {}).reduce( + (acc, [key, value]) => { + const converted: IChatGptSchema | null = schema({ components: props.components, $defs: props.$defs, - schema: props.schema.additionalProperties, - }) - : props.schema.additionalProperties; - if (additionalProperties === null) return null; + escape: props.escape, + tag: props.tag, + schema: value, + }); + if (converted === null) return acc; + acc[key] = converted; + return acc; + }, + {} as Record, + ); + if (Object.values(properties).some((v) => v === null)) + return union.push(null); + const additionalProperties = + input.additionalProperties === undefined + ? false + : typeof input.additionalProperties === "object" && + input.additionalProperties !== null + ? schema({ + components: props.components, + $defs: props.$defs, + escape: props.escape, + tag: props.tag, + schema: input.additionalProperties, + }) + : input.additionalProperties; + if (additionalProperties === null) return union.push(null); + return union.push({ + ...input, + properties: properties as Record, + additionalProperties, + required: Object.keys(properties), + }); + } else if (OpenApiTypeChecker.isOneOf(input)) { + input.oneOf.forEach(visit); + return 0; + } else if (OpenApiTypeChecker.isConstant(input)) return 0; + else if (OpenApiTypeChecker.isString(input)) + return union.push({ + ...input, + ...(!!props.tag + ? {} + : { + contentMediaType: undefined, + minLength: undefined, + maxLength: undefined, + format: undefined, + pattern: undefined, + }), + }); + else if ( + OpenApiTypeChecker.isNumber(input) || + OpenApiTypeChecker.isInteger(input) + ) + return union.push({ + ...input, + ...(!!props.tag + ? {} + : { + maximum: undefined, + minimum: undefined, + exclusiveMaximum: undefined, + exclusiveMinimum: undefined, + multipleOf: undefined, + }), + }); + else if (OpenApiTypeChecker.isBoolean(input)) return union.push(input); + else return union.push(input); + }; + const visitConstant = (input: OpenApi.IJsonSchema): void => { + const insert = (value: any): void => { + const matched: IChatGptSchema.INumber | undefined = union.find( + (u) => + (u as IChatGptSchema.__ISignificant).type === typeof value, + ) as IChatGptSchema.INumber | undefined; + if (matched !== undefined) { + matched.enum ??= []; + matched.enum.push(value); + } else union.push({ type: typeof value as "number", enum: [value] }); + }; + if (OpenApiTypeChecker.isConstant(input)) insert(input.const); + else if (OpenApiTypeChecker.isOneOf(input)) + input.oneOf.forEach(visitConstant); + else if ( + !!props.escape === true && + OpenApiTypeChecker.isReference(input) && + OpenApiTypeChecker.isRecursiveReference({ + components: props.components, + schema: input, + }) === false + ) { + const target: OpenApi.IJsonSchema | undefined = + props.components.schemas?.[ + input.$ref.split("#/components/schemas/")[1] + ]; + if (target !== undefined) visitConstant(target); + } + }; + visit(props.schema); + visitConstant(props.schema); + + if (union.some((u) => u === null)) return null; + else if (union.length === 0) return { - ...props.schema, - properties: properties as Record, - additionalProperties, + ...attribute, + type: undefined, }; - } else if (OpenApiTypeChecker.isOneOf(props.schema)) { - const oneOf: Array = props.schema.oneOf.map( - (item) => - convertSchema({ - components: props.components, - $defs: props.$defs, - schema: item, - }), - ); - if (oneOf.some((v) => v === null)) return null; + else if (union.length === 1) return { - ...props.schema, - oneOf: oneOf.filter((v) => v !== null), - discriminator: props.schema.discriminator - ? { - propertyName: props.schema.discriminator.propertyName, - mapping: props.schema.discriminator.mapping - ? Object.fromEntries( - Object.entries(props.schema.discriminator.mapping).map( - ([key, value]) => [ - key, - value.replace("#/components/schemas/", "#/$defs/"), - ], - ), - ) - : undefined, - } - : undefined, + ...attribute, + ...union[0], }; - } - return props.schema; + return { + ...attribute, + anyOf: union as IChatGptSchema[], + }; }; export const separate = (props: { - top: IChatGptSchema.ITop; + $defs: Record; predicate: (schema: IChatGptSchema) => boolean; schema: IChatGptSchema; }): [IChatGptSchema | null, IChatGptSchema | null] => { if (props.predicate(props.schema) === true) return [null, props.schema]; else if ( ChatGptTypeChecker.isUnknown(props.schema) || - ChatGptTypeChecker.isOneOf(props.schema) + ChatGptTypeChecker.isAnyOf(props.schema) ) return [props.schema, null]; else if (ChatGptTypeChecker.isObject(props.schema)) return separateObject({ - top: props.top, + $defs: props.$defs, predicate: props.predicate, schema: props.schema, }); else if (ChatGptTypeChecker.isArray(props.schema)) return separateArray({ - top: props.top, + $defs: props.$defs, predicate: props.predicate, schema: props.schema, }); else if (ChatGptTypeChecker.isReference(props.schema)) return separateReference({ - top: props.top, + $defs: props.$defs, predicate: props.predicate, schema: props.schema, }); @@ -183,12 +282,12 @@ export namespace ChatGptConverter { }; const separateArray = (props: { - top: IChatGptSchema.ITop; + $defs: Record; predicate: (schema: IChatGptSchema) => boolean; schema: IChatGptSchema.IArray; }): [IChatGptSchema.IArray | null, IChatGptSchema.IArray | null] => { const [x, y] = separate({ - top: props.top, + $defs: props.$defs, predicate: props.predicate, schema: props.schema.items, }); @@ -209,7 +308,7 @@ export namespace ChatGptConverter { }; const separateObject = (props: { - top: IChatGptSchema.ITop; + $defs: Record; predicate: (schema: IChatGptSchema) => boolean; schema: IChatGptSchema.IObject; }): [IChatGptSchema.IObject | null, IChatGptSchema.IObject | null] => { @@ -223,7 +322,7 @@ export namespace ChatGptConverter { } satisfies IChatGptSchema.IObject; for (const [key, value] of Object.entries(props.schema.properties ?? {})) { const [x, y] = separate({ - top: props.top, + $defs: props.$defs, predicate: props.predicate, schema: value, }); @@ -235,7 +334,7 @@ export namespace ChatGptConverter { props.schema.additionalProperties !== null ) { const [x, y] = separate({ - top: props.top, + $defs: props.$defs, predicate: props.predicate, schema: props.schema.additionalProperties, }); @@ -258,22 +357,22 @@ export namespace ChatGptConverter { }; const separateReference = (props: { - top: IChatGptSchema.ITop; + $defs: Record; predicate: (schema: IChatGptSchema) => boolean; schema: IChatGptSchema.IReference; }): [IChatGptSchema.IReference | null, IChatGptSchema.IReference | null] => { const key: string = props.schema.$ref.split("#/$defs/")[1]; // FIND EXISTING - if (props.top.$defs?.[`${key}.Human`] || props.top.$defs?.[`${key}.Llm`]) + if (props.$defs?.[`${key}.Human`] || props.$defs?.[`${key}.Llm`]) return [ - props.top.$defs?.[`${key}.Llm`] + props.$defs?.[`${key}.Llm`] ? { ...props.schema, $ref: `#/$defs/${key}.Llm`, } : null, - props.top.$defs?.[`${key}.Human`] + props.$defs?.[`${key}.Human`] ? { ...props.schema, $ref: `#/$defs/${key}.Human`, @@ -282,20 +381,20 @@ export namespace ChatGptConverter { ]; // PRE-ASSIGNMENT - props.top.$defs![`${key}.Llm`] = {}; - props.top.$defs![`${key}.Human`] = {}; + props.$defs![`${key}.Llm`] = {}; + props.$defs![`${key}.Human`] = {}; // DO COMPOSE - const schema: IChatGptSchema = props.top.$defs?.[key]!; + const schema: IChatGptSchema = props.$defs?.[key]!; const [llm, human] = separate({ - top: props.top, + $defs: props.$defs, predicate: props.predicate, schema, }); - if (llm === null) delete props.top.$defs![`${key}.Llm`]; - else props.top.$defs![`${key}.Llm`] = llm; - if (human === null) delete props.top.$defs![`${key}.Human`]; - else props.top.$defs![`${key}.Human`] = human; + if (llm === null) delete props.$defs![`${key}.Llm`]; + else props.$defs![`${key}.Llm`] = llm; + if (human === null) delete props.$defs![`${key}.Human`]; + else props.$defs![`${key}.Human`] = human; // FINALIZE return [ diff --git a/src/converters/HttpLlmConverter.ts b/src/converters/HttpLlmConverter.ts index 7c99789..ade9df9 100644 --- a/src/converters/HttpLlmConverter.ts +++ b/src/converters/HttpLlmConverter.ts @@ -1,12 +1,9 @@ import { OpenApi } from "../OpenApi"; import { IChatGptSchema } from "../structures/IChatGptSchema"; -import { IGeminiSchema } from "../structures/IGeminiSchema"; import { IHttpLlmApplication } from "../structures/IHttpLlmApplication"; import { IHttpLlmFunction } from "../structures/IHttpLlmFunction"; import { IHttpMigrateApplication } from "../structures/IHttpMigrateApplication"; import { IHttpMigrateRoute } from "../structures/IHttpMigrateRoute"; -import { ILlmSchemaV3 } from "../structures/ILlmSchemaV3"; -import { ILlmSchemaV3_1 } from "../structures/ILlmSchemaV3_1"; import { ChatGptConverter } from "./ChatGptConverter"; import { GeminiConverter } from "./GeminiConverter"; import { LlmConverterV3 } from "./LlmConverterV3"; @@ -15,11 +12,8 @@ import { LlmConverterV3_1 } from "./LlmConverterV3_1"; export namespace HttpLlmConverter { export const compose = < Model extends IHttpLlmApplication.Model, - Schema extends - | ILlmSchemaV3 - | ILlmSchemaV3_1 - | IChatGptSchema - | IGeminiSchema = IHttpLlmApplication.ModelSchema[Model], + Parameters extends + IHttpLlmApplication.ModelParameters[Model] = IHttpLlmApplication.ModelParameters[Model], Operation extends OpenApi.IOperation = OpenApi.IOperation, Route extends IHttpMigrateRoute = IHttpMigrateRoute< OpenApi.IJsonSchema, @@ -28,8 +22,11 @@ export namespace HttpLlmConverter { >(props: { model: Model; migrate: IHttpMigrateApplication; - options: IHttpLlmApplication.IOptions; - }): IHttpLlmApplication => { + options: IHttpLlmApplication.IOptions< + Model, + Parameters["properties"][string] + >; + }): IHttpLlmApplication => { // COMPOSE FUNCTIONS const errors: IHttpLlmApplication.IError[] = props.migrate.errors.map((e) => ({ @@ -39,7 +36,7 @@ export namespace HttpLlmConverter { operation: () => e.operation(), route: () => undefined, })); - const functions: IHttpLlmFunction[] = + const functions: IHttpLlmFunction[] = props.migrate.routes .map((route) => { if (route.method === "head") { @@ -68,7 +65,7 @@ export namespace HttpLlmConverter { }); return null; } - const func: IHttpLlmFunction | null = composeFunction({ + const func: IHttpLlmFunction | null = composeFunction({ model: props.model, options: props.options, components: props.migrate.document().components, @@ -85,7 +82,8 @@ export namespace HttpLlmConverter { return func; }) .filter( - (v): v is IHttpLlmFunction => v !== null, + (v): v is IHttpLlmFunction => + v !== null, ); return { model: props.model, @@ -95,72 +93,36 @@ export namespace HttpLlmConverter { }; }; - export const schema = < - Model extends IHttpLlmApplication.Model, - Schema extends - | ILlmSchemaV3 - | ILlmSchemaV3_1 - | IChatGptSchema - | IGeminiSchema = IHttpLlmApplication.ModelSchema[Model], - >(props: { - model: Model; - components: OpenApi.IComponents; - schema: OpenApi.IJsonSchema; - recursive: false | number; - }): Schema | null => { - return CASTERS[props.model]({ - components: props.components, - recursive: props.recursive, - schema: props.schema, - }) as Schema | null; - }; - export const separateParameters = < Model extends IHttpLlmApplication.Model, - Schema extends - | ILlmSchemaV3 - | ILlmSchemaV3_1 - | IChatGptSchema - | IGeminiSchema, + Parameters extends + IHttpLlmApplication.ModelParameters[Model] = IHttpLlmApplication.ModelParameters[Model], >(props: { model: Model; - parameters: Schema[]; - predicate: (schema: Schema) => boolean; - }): IHttpLlmFunction.ISeparated => { + parameters: Parameters; + predicate: (schema: Parameters["properties"][string]) => boolean; + }): IHttpLlmFunction.ISeparated => { const separator: (props: { - predicate: (schema: Schema) => boolean; - schema: Schema; - }) => [Schema | null, Schema | null] = SEPARATORS[props.model] as any; - const indexes: Array<[Schema | null, Schema | null]> = props.parameters.map( - (schema) => - separator({ - predicate: props.predicate, - schema, - }), - ); + predicate: (schema: Parameters["properties"][string]) => boolean; + schema: Parameters["properties"][string]; + }) => [ + Parameters["properties"][string] | null, + Parameters["properties"][string] | null, + ] = SEPARATORS[props.model] as any; + const [llm, human] = separator({ + predicate: props.predicate, + schema: props.parameters as Parameters["properties"][string], + }); 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), + llm: llm as Parameters | null, + human: human as Parameters | null, }; }; const composeFunction = < Model extends IHttpLlmApplication.Model, - Schema extends - | ILlmSchemaV3 - | ILlmSchemaV3_1 - | IChatGptSchema - | IGeminiSchema = IHttpLlmApplication.ModelSchema[Model], + Parameters extends + IHttpLlmApplication.ModelParameters[Model] = IHttpLlmApplication.ModelParameters[Model], Operation extends OpenApi.IOperation = OpenApi.IOperation, Route extends IHttpMigrateRoute = IHttpMigrateRoute< OpenApi.IJsonSchema, @@ -170,20 +132,27 @@ export namespace HttpLlmConverter { model: Model; components: OpenApi.IComponents; route: IHttpMigrateRoute; - options: IHttpLlmApplication.IOptions; - }): IHttpLlmFunction | null => { - const cast = (s: OpenApi.IJsonSchema): Schema | null => + options: IHttpLlmApplication.IOptions< + Model, + Parameters["properties"][string] + >; + }): IHttpLlmFunction | null => { + const $defs: Record = {}; + const cast = ( + s: OpenApi.IJsonSchema, + ): Parameters["properties"][string] | null => CASTERS[props.model]({ components: props.components, recursive: props.options.recursive, schema: s, - }) as Schema | null; - const output: Schema | null | undefined = + $defs, + }) as Parameters["properties"][string] | null; + const output: Parameters["properties"][string] | null | undefined = props.route.success && props.route.success ? cast(props.route.success.schema) : undefined; if (output === null) return null; - const properties: [string, Schema | null][] = [ + const properties: [string, Parameters["properties"][string] | null][] = [ ...props.route.parameters.map((p) => ({ key: p.key, schema: { @@ -224,15 +193,16 @@ export namespace HttpLlmConverter { if (properties.some(([_k, v]) => v === null)) return null; // COMPOSE PARAMETERS - const parameters: Schema[] = props.options.keyword - ? [ - { - type: "object", - properties: Object.fromEntries(properties as [string, Schema][]), - additionalProperties: false, - } as any as Schema, - ] - : properties.map(([_k, v]) => v!); + const parameters: Parameters = { + type: "object", + properties: Object.fromEntries( + properties as [string, Parameters["properties"][string]][], + ), + additionalProperties: false, + required: properties.map(([k]) => k), + } as any as Parameters; + if (Object.keys($defs).length) + (parameters as any as IChatGptSchema.IParameters).$defs = $defs; const operation: OpenApi.IOperation = props.route.operation(); // FINALIZATION @@ -249,7 +219,7 @@ export namespace HttpLlmConverter { parameters, }) : undefined, - output, + output: output as any, description: (() => { if (operation.summary && operation.description) { return operation.description.startsWith(operation.summary) @@ -286,7 +256,12 @@ const CASTERS = { components: OpenApi.IComponents; schema: OpenApi.IJsonSchema; recursive: false | number; - }) => ChatGptConverter.schema(props), + $defs: Record; + }) => + ChatGptConverter.schema({ + ...props, + escape: false, + }), gemini: (props: { components: OpenApi.IComponents; schema: OpenApi.IJsonSchema; diff --git a/src/converters/OpenApiV3Converter.ts b/src/converters/OpenApiV3Converter.ts index 3113de9..4a6c38c 100644 --- a/src/converters/OpenApiV3Converter.ts +++ b/src/converters/OpenApiV3Converter.ts @@ -329,7 +329,7 @@ export namespace OpenApiV3Converter { convertSchema(components)(value), ]), ) - : undefined, + : {}, additionalProperties: schema.additionalProperties ? typeof schema.additionalProperties === "object" && schema.additionalProperties !== null diff --git a/src/converters/OpenApiV3_1Converter.ts b/src/converters/OpenApiV3_1Converter.ts index c1cd971..9da29a6 100644 --- a/src/converters/OpenApiV3_1Converter.ts +++ b/src/converters/OpenApiV3_1Converter.ts @@ -532,7 +532,7 @@ export namespace OpenApiV3_1Converter { [key, convertSchema(components)(value)] as const, ), ) - : undefined, + : {}, additionalProperties: schema.additionalProperties ? typeof schema.additionalProperties === "object" && schema.additionalProperties !== null diff --git a/src/converters/SwaggerV2Converter.ts b/src/converters/SwaggerV2Converter.ts index 95073fd..9e2806b 100644 --- a/src/converters/SwaggerV2Converter.ts +++ b/src/converters/SwaggerV2Converter.ts @@ -341,7 +341,7 @@ export namespace SwaggerV2Converter { .filter(([_, v]) => v !== undefined) .map(([key, value]) => [key, convertSchema(value)]), ) - : undefined, + : {}, additionalProperties: schema.additionalProperties ? typeof schema.additionalProperties === "object" && schema.additionalProperties !== null diff --git a/src/http/HttpLlmFunctionFetcher.ts b/src/http/HttpLlmFunctionFetcher.ts index b6dfdf5..0a37cf1 100644 --- a/src/http/HttpLlmFunctionFetcher.ts +++ b/src/http/HttpLlmFunctionFetcher.ts @@ -1,57 +1,44 @@ import type { HttpLlm } from "../HttpLlm"; import type { HttpMigration } from "../HttpMigration"; import { OpenApi } from "../OpenApi"; -import { IChatGptSchema } from "../structures/IChatGptSchema"; -import { IGeminiSchema } from "../structures/IGeminiSchema"; import { IHttpLlmApplication } from "../structures/IHttpLlmApplication"; import { IHttpMigrateRoute } from "../structures/IHttpMigrateRoute"; import { IHttpResponse } from "../structures/IHttpResponse"; -import { ILlmSchemaV3 } from "../structures/ILlmSchemaV3"; -import { ILlmSchemaV3_1 } from "../structures/ILlmSchemaV3_1"; import { HttpMigrateRouteFetcher } from "./HttpMigrateRouteFetcher"; export namespace HttpLlmFunctionFetcher { export const execute = async < Model extends IHttpLlmApplication.Model, - Schema extends - | ILlmSchemaV3 - | ILlmSchemaV3_1 - | IChatGptSchema - | IGeminiSchema = IHttpLlmApplication.ModelSchema[Model], + Parameters extends + IHttpLlmApplication.ModelParameters[Model] = IHttpLlmApplication.ModelParameters[Model], Operation extends OpenApi.IOperation = OpenApi.IOperation, Route extends IHttpMigrateRoute = IHttpMigrateRoute< OpenApi.IJsonSchema, Operation >, >( - props: HttpLlm.IFetchProps, + props: HttpLlm.IFetchProps, ): Promise => HttpMigrateRouteFetcher.execute(getFetchArguments("execute", props)); export const propagate = async < Model extends IHttpLlmApplication.Model, - Schema extends - | ILlmSchemaV3 - | ILlmSchemaV3_1 - | IChatGptSchema - | IGeminiSchema = IHttpLlmApplication.ModelSchema[Model], + Parameters extends + IHttpLlmApplication.ModelParameters[Model] = IHttpLlmApplication.ModelParameters[Model], Operation extends OpenApi.IOperation = OpenApi.IOperation, Route extends IHttpMigrateRoute = IHttpMigrateRoute< OpenApi.IJsonSchema, Operation >, >( - props: HttpLlm.IFetchProps, + props: HttpLlm.IFetchProps, ): Promise => HttpMigrateRouteFetcher.propagate(getFetchArguments("propagate", props)); const getFetchArguments = < Model extends IHttpLlmApplication.Model, - Schema extends - | ILlmSchemaV3 - | ILlmSchemaV3_1 - | IChatGptSchema - | IGeminiSchema = IHttpLlmApplication.ModelSchema[Model], + Parameters extends + IHttpLlmApplication.ModelParameters[Model] = IHttpLlmApplication.ModelParameters[Model], Operation extends OpenApi.IOperation = OpenApi.IOperation, Route extends IHttpMigrateRoute = IHttpMigrateRoute< OpenApi.IJsonSchema, @@ -59,43 +46,23 @@ export namespace HttpLlmFunctionFetcher { >, >( from: string, - props: HttpLlm.IFetchProps, + props: HttpLlm.IFetchProps, ): HttpMigration.IFetchProps => { const route: Route = 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: Object.fromEntries( - route.parameters.map((p) => [p.key, input[p.key]] as const), - ), - 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; + const input: Record = props.input; + const valid: boolean = 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, - query, - body, - } satisfies HttpMigration.IFetchProps; + parameters: Object.fromEntries( + route.parameters.map((p) => [p.key, input[p.key]] as const), + ), + query: input.query, + body: input.body, + }; }; } diff --git a/src/structures/IChatGptSchema.ts b/src/structures/IChatGptSchema.ts index caec003..4929e47 100644 --- a/src/structures/IChatGptSchema.ts +++ b/src/structures/IChatGptSchema.ts @@ -7,31 +7,34 @@ export type IChatGptSchema = | IChatGptSchema.ITuple | IChatGptSchema.IObject | IChatGptSchema.IReference - | IChatGptSchema.IOneOf + | IChatGptSchema.IAnyOf | IChatGptSchema.INull | IChatGptSchema.IUnknown; export namespace IChatGptSchema { /** - * The top level schema including `$defs`. + * Type of the function parameters. */ - export type ITop = Schema & { + export interface IParameters extends Omit { + /** + * Collection of the named types. + */ $defs?: Record; - }; - /** - * Constant value type. - */ - export interface IConstant extends __IAttribute { /** - * The constant value. + * Do not allow additional properties in the parameters. */ - const: boolean | number | string; + additionalProperties: false; } /** * Boolean type info. */ export interface IBoolean extends __ISignificant<"boolean"> { + /** + * Enumeration values. + */ + enum?: Array; + /** * The default value. */ @@ -42,6 +45,11 @@ export namespace IChatGptSchema { * Integer type info. */ export interface IInteger extends __ISignificant<"integer"> { + /** + * Enumeration values. + */ + enum?: Array; + /** * Default value. * @@ -96,6 +104,11 @@ export namespace IChatGptSchema { * Number (double) type info. */ export interface INumber extends __ISignificant<"number"> { + /** + * Enumeration values. + */ + enum?: Array; + /** * Default value. */ @@ -143,6 +156,11 @@ export namespace IChatGptSchema { * String type info. */ export interface IString extends __ISignificant<"string"> { + /** + * Enumeration values. + */ + enum?: Array; + /** * Default value. */ @@ -312,7 +330,7 @@ export namespace IChatGptSchema { * If you need additional properties that is represented by dynamic key, * you can use the {@link additionalProperties} instead. */ - properties?: Record; + properties: Record; /** * Additional properties' info. @@ -393,37 +411,11 @@ export namespace IChatGptSchema { * defined `anyOf` instead of the `oneOf`, {@link IChatGptSchema} forcibly * converts it to `oneOf` type. */ - export interface IOneOf extends __IAttribute { + export interface IAnyOf extends __IAttribute { /** * List of the union types. */ - oneOf: Exclude[]; - - /** - * Discriminator info of the union type. - */ - discriminator?: IOneOf.IDiscriminator; - } - export namespace IOneOf { - /** - * Discriminator info of the union type. - */ - export interface IDiscriminator { - /** - * Property name for the discriminator. - */ - propertyName: string; - - /** - * Mapping of the discriminator value to the schema name. - * - * This property is valid only for {@link IReference} typed - * {@link IOneOf.oneof} elements. Therefore, `key` of `mapping` is - * the discriminator value, and `value` of `mapping` is the - * schema name like `#/$defs/SomeObject`. - */ - mapping?: Record; - } + anyOf: Exclude[]; } /** diff --git a/src/structures/IGeminiSchema.ts b/src/structures/IGeminiSchema.ts index c7036ca..cdf42d2 100644 --- a/src/structures/IGeminiSchema.ts +++ b/src/structures/IGeminiSchema.ts @@ -42,6 +42,16 @@ export type IGeminiSchema = | IGeminiSchema.IUnknown | IGeminiSchema.INullOnly; export namespace IGeminiSchema { + /** + * Type of the function parameters. + */ + export interface IParameters extends Omit { + /** + * Do not allow additional properties in the parameters. + */ + additionalProperties: false; + } + /** * Boolean type schema info. */ @@ -325,7 +335,7 @@ export namespace IGeminiSchema { * 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. diff --git a/src/structures/IHttpLlmApplication.ts b/src/structures/IHttpLlmApplication.ts index 85b1a91..a50779a 100644 --- a/src/structures/IHttpLlmApplication.ts +++ b/src/structures/IHttpLlmApplication.ts @@ -71,11 +71,8 @@ import { ILlmSchemaV3_1 } from "./ILlmSchemaV3_1"; */ export interface IHttpLlmApplication< Model extends IHttpLlmApplication.Model, - Schema extends - | ILlmSchemaV3 - | ILlmSchemaV3_1 - | IChatGptSchema.ITop - | IGeminiSchema = IHttpLlmApplication.ModelSchema[Model], + Parameters extends + IHttpLlmApplication.ModelParameters[Model] = IHttpLlmApplication.ModelParameters[Model], Operation extends OpenApi.IOperation = OpenApi.IOperation, Route extends IHttpMigrateRoute = IHttpMigrateRoute, > { @@ -92,7 +89,7 @@ export interface IHttpLlmApplication< * When you want to execute the function with LLM constructed arguments, * you can do it through {@link LlmFetcher.execute} function. */ - functions: IHttpLlmFunction[]; + functions: IHttpLlmFunction[]; /** * List of errors occurred during the composition. @@ -105,14 +102,23 @@ export interface IHttpLlmApplication< * Adjusted options when composing the application through * {@link HttpLlm.application} function. */ - options: IHttpLlmApplication.IOptions; + options: IHttpLlmApplication.IOptions< + Model, + Parameters["properties"][string] + >; } export namespace IHttpLlmApplication { export type Model = "3.0" | "3.1" | "chatgpt" | "gemini"; + export type ModelParameters = { + "3.0": ILlmSchemaV3.IParameters; + "3.1": ILlmSchemaV3_1.IParameters; + chatgpt: IChatGptSchema.IParameters; + gemini: IGeminiSchema.IParameters; + }; export type ModelSchema = { "3.0": ILlmSchemaV3; "3.1": ILlmSchemaV3_1; - chatgpt: IChatGptSchema.ITop; + chatgpt: IChatGptSchema; gemini: IGeminiSchema; }; @@ -166,39 +172,9 @@ export namespace IHttpLlmApplication { Schema extends | ILlmSchemaV3 | ILlmSchemaV3_1 - | IChatGptSchema.ITop + | IChatGptSchema | IGeminiSchema = IHttpLlmApplication.ModelSchema[Model], > { - /** - * Whether the parameters are keyworded or not. - * - * If this property value is `true`, length of the - * {@link IHttpLlmApplication.IFunction.parameters} is always 1, and type of - * the pararameter is always {@link ILlmSchemaV3.IObject} type. - * - * Otherwise, the parameters would be multiple, and the sequence of the parameters - * are following below rules. - * - * ```typescript - * // KEYWORD TRUE - * { - * ...pathParameters, - * query, - * body, - * } - * - * // KEYWORD FALSE - * [ - * ...pathParameters, - * ...(query ? [query] : []), - * ...(body ? [body] : []), - * ] - * ``` - * - * @default false - */ - keyword: boolean; - /** * Whether to allow recursive types or not. * diff --git a/src/structures/IHttpLlmFunction.ts b/src/structures/IHttpLlmFunction.ts index 1866113..89064a1 100644 --- a/src/structures/IHttpLlmFunction.ts +++ b/src/structures/IHttpLlmFunction.ts @@ -58,11 +58,11 @@ import { ILlmSchemaV3_1 } from "./ILlmSchemaV3_1"; * @author Jeongho Nam - https://github.com/samchon */ export interface IHttpLlmFunction< - Schema extends - | ILlmSchemaV3 - | ILlmSchemaV3_1 - | IChatGptSchema.ITop - | IGeminiSchema, + Parameters extends + | ILlmSchemaV3.IParameters + | ILlmSchemaV3_1.IParameters + | IChatGptSchema.IParameters + | IGeminiSchema.IParameters, Operation extends OpenApi.IOperation = OpenApi.IOperation, Route extends IHttpMigrateRoute = IHttpMigrateRoute, > { @@ -150,14 +150,14 @@ export interface IHttpLlmFunction< * ] * ``` */ - parameters: Schema[]; + parameters: Parameters; /** * Collection of separated parameters. * * Filled only when {@link IHttpLlmApplication.IOptions.separate} is configured. */ - separated?: IHttpLlmFunction.ISeparated; + separated?: IHttpLlmFunction.ISeparated; /** * Expected return type. @@ -165,7 +165,7 @@ export interface IHttpLlmFunction< * If the target operation returns nothing (`void`), the `output` * would be `undefined`. */ - output?: Schema | undefined; + output?: Parameters | undefined; /** * Description of the function. @@ -232,43 +232,20 @@ export namespace IHttpLlmFunction { * Collection of separated parameters. */ export interface ISeparated< - Schema extends - | ILlmSchemaV3 - | ILlmSchemaV3_1 - | IChatGptSchema.ITop - | IGeminiSchema, + Parameters extends + | ILlmSchemaV3.IParameters + | ILlmSchemaV3_1.IParameters + | IChatGptSchema.IParameters + | IGeminiSchema.IParameters, > { /** * Parameters that would be composed by the LLM. */ - llm: ISeparatedParameter[]; + llm: Parameters | null; /** * Parameters that would be composed by the human. */ - human: ISeparatedParameter[]; - } - - /** - * Separated parameter. - */ - export interface ISeparatedParameter< - Schema extends - | ILlmSchemaV3 - | ILlmSchemaV3_1 - | IChatGptSchema.ITop - | IGeminiSchema, - > { - /** - * Index of the parameter. - * - * @type uint - */ - index: number; - - /** - * Type schema info of the parameter. - */ - schema: Schema; + human: Parameters | null; } } diff --git a/src/structures/ILlmApplication.ts b/src/structures/ILlmApplication.ts index 4d2f522..1a216a3 100644 --- a/src/structures/ILlmApplication.ts +++ b/src/structures/ILlmApplication.ts @@ -15,7 +15,7 @@ import { ILlmSchemaV3_1 } from "./ILlmSchemaV3_1"; * By the way, the LLM function calling application composition, converting * `ILlmApplication` instance from TypeScript interface (or class) type is not always * successful. As LLM provider like OpenAI cannot understand the recursive reference - * type that is embodied by {@link OpenApi.IJsonSchema.IReference}, if there're some + * type that is embodied by {@link IOpenApiSchemachema.IReference}, if there're some * recursive types in the TypeScript interface (or class) type, the conversion would * be failed. * @@ -36,11 +36,8 @@ import { ILlmSchemaV3_1 } from "./ILlmSchemaV3_1"; */ export interface ILlmApplication< Model extends ILlmApplication.Model, - Schema extends - | ILlmSchemaV3 - | ILlmSchemaV3_1 - | IChatGptSchema.ITop - | IGeminiSchema = ILlmApplication.ModelSchema[Model], + Parameters extends + ILlmApplication.ModelParameters[Model] = ILlmApplication.ModelParameters[Model], > { /** * Model of the LLM. @@ -52,19 +49,25 @@ export interface ILlmApplication< * * List of function metadata that can be used for the LLM function call. */ - functions: ILlmFunction[]; + functions: ILlmFunction[]; /** * Options for the application. */ - options: ILlmApplication.IOptions; + options: ILlmApplication.IOptions; } export namespace ILlmApplication { export type Model = "3.0" | "3.1" | "chatgpt" | "gemini"; + export type ModelParameters = { + "3.0": ILlmSchemaV3.IParameters; + "3.1": ILlmSchemaV3_1.IParameters; + chatgpt: IChatGptSchema.IParameters; + gemini: IGeminiSchema.IParameters; + }; export type ModelSchema = { "3.0": ILlmSchemaV3; "3.1": ILlmSchemaV3_1; - chatgpt: IChatGptSchema.ITop; + chatgpt: IChatGptSchema; gemini: IGeminiSchema; }; @@ -76,7 +79,7 @@ export namespace ILlmApplication { Schema extends | ILlmSchemaV3 | ILlmSchemaV3_1 - | IChatGptSchema.ITop + | IChatGptSchema | IGeminiSchema = ILlmApplication.ModelSchema[Model], > { /** diff --git a/src/structures/ILlmFunction.ts b/src/structures/ILlmFunction.ts index 81f73db..3fdc549 100644 --- a/src/structures/ILlmFunction.ts +++ b/src/structures/ILlmFunction.ts @@ -26,11 +26,11 @@ import { ILlmSchemaV3_1 } from "./ILlmSchemaV3_1"; * @author Jeongho Nam - https://github.com/samchon */ export interface ILlmFunction< - Schema extends - | ILlmSchemaV3 - | ILlmSchemaV3_1 - | IChatGptSchema.ITop - | IGeminiSchema, + Parameters extends + | ILlmSchemaV3.IParameters + | ILlmSchemaV3_1.IParameters + | IChatGptSchema.IParameters + | IGeminiSchema.IParameters, > { /** * Representative name of the function. @@ -40,12 +40,12 @@ export interface ILlmFunction< /** * List of parameter types. */ - parameters: Schema[]; + parameters: Parameters; /** * Collection of separated parameters. */ - separated?: ILlmFunction.ISeparated; + separated?: ILlmFunction.ISeparated; /** * Expected return type. @@ -53,7 +53,16 @@ export interface ILlmFunction< * If the function returns nothing (`void`), the `output` value would * be `undefined`. */ - output?: Schema | undefined; + output?: Parameters["properties"][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; /** * Description of the function. @@ -89,43 +98,20 @@ export namespace ILlmFunction { * Collection of separated parameters. */ export interface ISeparated< - Schema extends - | ILlmSchemaV3 - | ILlmSchemaV3_1 - | IChatGptSchema.ITop - | IGeminiSchema, + Parameters extends + | ILlmSchemaV3.IParameters + | ILlmSchemaV3_1.IParameters + | IChatGptSchema.IParameters + | IGeminiSchema.IParameters, > { /** * Parameters that would be composed by the LLM. */ - llm: ISeparatedParameter[]; + llm: Parameters | null; /** * Parameters that would be composed by the human. */ - human: ISeparatedParameter[]; - } - - /** - * Separated parameter. - */ - export interface ISeparatedParameter< - Schema extends - | ILlmSchemaV3 - | ILlmSchemaV3_1 - | IChatGptSchema.ITop - | IGeminiSchema, - > { - /** - * Index of the parameter. - * - * @type uint - */ - index: number; - - /** - * Type schema info of the parameter. - */ - schema: Schema; + human: Parameters | null; } } diff --git a/src/structures/ILlmSchemaV3.ts b/src/structures/ILlmSchemaV3.ts index f0d2d99..577b570 100644 --- a/src/structures/ILlmSchemaV3.ts +++ b/src/structures/ILlmSchemaV3.ts @@ -28,6 +28,16 @@ export type ILlmSchemaV3 = | ILlmSchemaV3.INullOnly | ILlmSchemaV3.IOneOf; export namespace ILlmSchemaV3 { + /** + * Type of the function parameters. + */ + export interface IParameters extends Omit { + /** + * Do not allow additional properties in the parameters. + */ + additionalProperties: false; + } + /** * Boolean type schema info. */ @@ -278,7 +288,7 @@ export namespace ILlmSchemaV3 { * 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. diff --git a/src/structures/ILlmSchemaV3_1.ts b/src/structures/ILlmSchemaV3_1.ts index 8cdd148..71aed6a 100644 --- a/src/structures/ILlmSchemaV3_1.ts +++ b/src/structures/ILlmSchemaV3_1.ts @@ -10,6 +10,16 @@ export type ILlmSchemaV3_1 = | ILlmSchemaV3_1.INull | ILlmSchemaV3_1.IUnknown; export namespace ILlmSchemaV3_1 { + /** + * Type of the function parameters. + */ + export interface IParameters extends Omit { + /** + * Do not allow additional properties in the parameters. + */ + additionalProperties: false; + } + /** * Constant value type. */ @@ -304,7 +314,7 @@ export namespace ILlmSchemaV3_1 { * If you need additional properties that is represented by dynamic key, * you can use the {@link additionalProperties} instead. */ - properties?: Record; + properties: Record; /** * Additional properties' info. diff --git a/src/utils/ChatGptTypeChecker.ts b/src/utils/ChatGptTypeChecker.ts index 8587103..33d1797 100644 --- a/src/utils/ChatGptTypeChecker.ts +++ b/src/utils/ChatGptTypeChecker.ts @@ -13,14 +13,9 @@ export namespace ChatGptTypeChecker { schema: IChatGptSchema, ): schema is IChatGptSchema.IUnknown => (schema as IChatGptSchema.IUnknown).type === undefined && - !isConstant(schema) && - !isOneOf(schema) && + !isAnyOf(schema) && !isReference(schema); - export const isConstant = ( - schema: IChatGptSchema, - ): schema is IChatGptSchema.IConstant => - (schema as IChatGptSchema.IConstant).const !== undefined; export const isBoolean = ( schema: IChatGptSchema, ): schema is IChatGptSchema.IBoolean => @@ -55,17 +50,17 @@ export namespace ChatGptTypeChecker { export const isReference = ( schema: IChatGptSchema, ): schema is IChatGptSchema.IReference => (schema as any).$ref !== undefined; - export const isOneOf = ( + export const isAnyOf = ( schema: IChatGptSchema, - ): schema is IChatGptSchema.IOneOf => - (schema as IChatGptSchema.IOneOf).oneOf !== undefined; + ): schema is IChatGptSchema.IAnyOf => + (schema as IChatGptSchema.IAnyOf).anyOf !== undefined; /* ----------------------------------------------------------- OPERATORS ----------------------------------------------------------- */ export const visit = (props: { closure: (schema: IChatGptSchema) => void; - top: IChatGptSchema.ITop; + $defs?: Record | undefined; schema: IChatGptSchema; }): void => { const already: Set = new Set(); @@ -75,9 +70,9 @@ export namespace ChatGptTypeChecker { const key: string = schema.$ref.split("#/$defs/").pop()!; if (already.has(key) === true) return; already.add(key); - const found: IChatGptSchema | undefined = props.top.$defs?.[key]; + const found: IChatGptSchema | undefined = props.$defs?.[key]; if (found !== undefined) next(found); - } else if (ChatGptTypeChecker.isOneOf(schema)) schema.oneOf.forEach(next); + } else if (ChatGptTypeChecker.isAnyOf(schema)) schema.anyOf.forEach(next); else if (ChatGptTypeChecker.isObject(schema)) { for (const value of Object.values(schema.properties ?? {})) next(value); if ( @@ -99,19 +94,19 @@ export namespace ChatGptTypeChecker { }; export const covers = (props: { - top: IChatGptSchema.ITop; + $defs?: Record | undefined; x: IChatGptSchema; y: IChatGptSchema; }): boolean => coverStation({ - top: props.top, + $defs: props.$defs, x: props.x, y: props.y, visited: new Map(), }); const coverStation = (p: { - top: IChatGptSchema.ITop; + $defs?: Record | undefined; visited: Map>; x: IChatGptSchema; y: IChatGptSchema; @@ -132,7 +127,7 @@ export namespace ChatGptTypeChecker { }; const coverSchema = (p: { - top: IChatGptSchema.ITop; + $defs?: Record | undefined; visited: Map>; x: IChatGptSchema; y: IChatGptSchema; @@ -143,14 +138,14 @@ export namespace ChatGptTypeChecker { return true; // COMPARE WITH FLATTENING - const alpha: IChatGptSchema[] = flatSchema(p.top, p.x); - const beta: IChatGptSchema[] = flatSchema(p.top, p.y); + const alpha: IChatGptSchema[] = flatSchema(p.$defs, p.x); + const beta: IChatGptSchema[] = flatSchema(p.$defs, p.y); if (alpha.some((x) => isUnknown(x))) return true; else if (beta.some((x) => isUnknown(x))) return false; return beta.every((b) => alpha.some((a) => coverEscapedSchema({ - top: p.top, + $defs: p.$defs, visited: p.visited, x: a, y: b, @@ -160,7 +155,7 @@ export namespace ChatGptTypeChecker { }; const coverEscapedSchema = (p: { - top: IChatGptSchema.ITop; + $defs?: Record | undefined; visited: Map>; x: IChatGptSchema; y: IChatGptSchema; @@ -171,26 +166,16 @@ export namespace ChatGptTypeChecker { else if (isUnknown(p.y)) return false; else if (isNull(p.x)) return isNull(p.y); // ATOMIC CASE - else if (isConstant(p.x)) return isConstant(p.y) && p.x.const === p.y.const; - else if (isBoolean(p.x)) - return ( - isBoolean(p.y) || (isConstant(p.y) && typeof p.y.const === "boolean") - ); - else if (isInteger(p.x)) - return (isInteger(p.y) || isConstant(p.y)) && coverInteger(p.x, p.y); - else if (isNumber(p.x)) - return ( - (isConstant(p.y) || isInteger(p.y) || isNumber(p.y)) && - coverNumber(p.x, p.y) - ); - else if (isString(p.x)) - return (isConstant(p.y) || isString(p.y)) && coverString(p.x, p.y); + else if (isBoolean(p.x)) return isBoolean(p.y) && coverBoolean(p.x, p.y); + else if (isInteger(p.x)) return isInteger(p.y) && coverInteger(p.x, p.y); + else if (isNumber(p.x)) return isNumber(p.y) && coverNumber(p.x, p.y); + else if (isString(p.x)) return isString(p.y) && coverString(p.x, p.y); // INSTANCE CASE else if (isArray(p.x)) return ( (isArray(p.y) || isTuple(p.y)) && coverArray({ - top: p.top, + $defs: p.$defs, visited: p.visited, x: p.x, y: p.y, @@ -200,7 +185,7 @@ export namespace ChatGptTypeChecker { return ( isObject(p.y) && coverObject({ - top: p.top, + $defs: p.$defs, visited: p.visited, x: p.x, y: p.y, @@ -211,7 +196,7 @@ export namespace ChatGptTypeChecker { }; const coverArray = (p: { - top: IChatGptSchema.ITop; + $defs?: Record | undefined; visited: Map>; x: IChatGptSchema.IArray; y: IChatGptSchema.IArray | IChatGptSchema.ITuple; @@ -220,7 +205,7 @@ export namespace ChatGptTypeChecker { return ( p.y.prefixItems.every((v) => coverStation({ - top: p.top, + $defs: p.$defs, visited: p.visited, x: p.x.items, y: v, @@ -229,7 +214,7 @@ export namespace ChatGptTypeChecker { (p.y.additionalItems === undefined || (typeof p.y.additionalItems === "object" && coverStation({ - top: p.top, + $defs: p.$defs, visited: p.visited, x: p.x.items, y: p.y.additionalItems, @@ -250,7 +235,7 @@ export namespace ChatGptTypeChecker { ) return false; return coverStation({ - top: p.top, + $defs: p.$defs, visited: p.visited, x: p.x.items, y: p.y.items, @@ -258,7 +243,7 @@ export namespace ChatGptTypeChecker { }; const coverObject = (p: { - top: IChatGptSchema.ITop; + $defs?: Record | undefined; visited: Map>; x: IChatGptSchema.IObject; y: IChatGptSchema.IObject; @@ -272,7 +257,7 @@ export namespace ChatGptTypeChecker { (typeof p.x.additionalProperties === "object" && typeof p.y.additionalProperties === "object" && !coverStation({ - top: p.top, + $defs: p.$defs, visited: p.visited, x: p.x.additionalProperties, y: p.y.additionalProperties, @@ -288,7 +273,7 @@ export namespace ChatGptTypeChecker { ) return false; return coverStation({ - top: p.top, + $defs: p.$defs, visited: p.visited, x: a, y: b, @@ -296,12 +281,21 @@ export namespace ChatGptTypeChecker { }); }; + const coverBoolean = ( + x: IChatGptSchema.IBoolean, + y: IChatGptSchema.IBoolean, + ): boolean => { + if (!!x.enum?.length) + return !!y.enum?.length && y.enum.every((v) => x.enum!.includes(v)); + return true; + }; + const coverInteger = ( x: IChatGptSchema.IInteger, - y: IChatGptSchema.IConstant | IChatGptSchema.IInteger, + y: IChatGptSchema.IInteger, ): boolean => { - if (isConstant(y)) - return typeof y.const === "number" && Number.isInteger(y.const); + if (!!x.enum?.length) + return !!y.enum?.length && y.enum.every((v) => x.enum!.includes(v)); return [ x.type === y.type, x.minimum === undefined || @@ -325,12 +319,10 @@ export namespace ChatGptTypeChecker { const coverNumber = ( x: IChatGptSchema.INumber, - y: - | IChatGptSchema.IConstant - | IChatGptSchema.IInteger - | IChatGptSchema.INumber, + y: IChatGptSchema.IInteger | IChatGptSchema.INumber, ): boolean => { - if (isConstant(y)) return typeof y.const === "number"; + if (!!x.enum?.length) + return !!y.enum?.length && y.enum.every((v) => x.enum!.includes(v)); return [ x.type === y.type || (x.type === "number" && y.type === "integer"), x.minimum === undefined || @@ -354,10 +346,12 @@ export namespace ChatGptTypeChecker { const coverString = ( x: IChatGptSchema.IString, - y: IChatGptSchema.IConstant | IChatGptSchema.IString, + y: IChatGptSchema.IString, ): boolean => { - if (isConstant(y)) return typeof y.const === "string"; + if (!!x.enum?.length) + return !!y.enum?.length && y.enum.every((v) => x.enum!.includes(v)); return [ + x.type === y.type, x.format === undefined || (y.format !== undefined && coverFormat(x.format, y.format)), x.pattern === undefined || x.pattern === y.pattern, @@ -380,20 +374,20 @@ export namespace ChatGptTypeChecker { (x === "iri-reference" && y === "uri-reference"); const flatSchema = ( - top: IChatGptSchema.ITop, + $defs: Record | undefined, schema: IChatGptSchema, ): IChatGptSchema[] => { - schema = escapeReference(top, schema); - if (isOneOf(schema)) - return schema.oneOf.map((v) => flatSchema(top, v)).flat(); + schema = escapeReference($defs, schema); + if (isAnyOf(schema)) + return schema.anyOf.map((v) => flatSchema($defs, v)).flat(); return [schema]; }; const escapeReference = ( - top: IChatGptSchema.ITop, + $defs: Record | undefined, schema: IChatGptSchema, ): Exclude => isReference(schema) - ? escapeReference(top, top.$defs![schema.$ref.replace("#/$defs/", "")]!) + ? escapeReference($defs, $defs![schema.$ref.replace("#/$defs/", "")]!) : schema; } diff --git a/src/utils/LlmDataMerger.ts b/src/utils/LlmDataMerger.ts index 4613bac..0ba14b6 100644 --- a/src/utils/LlmDataMerger.ts +++ b/src/utils/LlmDataMerger.ts @@ -1,6 +1,5 @@ import { IChatGptSchema } from "../structures/IChatGptSchema"; import { IGeminiSchema } from "../structures/IGeminiSchema"; -import { IHttpLlmFunction } from "../structures/IHttpLlmFunction"; import { ILlmFunction } from "../structures/ILlmFunction"; import { ILlmSchemaV3 } from "../structures/ILlmSchemaV3"; import { ILlmSchemaV3_1 } from "../structures/ILlmSchemaV3_1"; @@ -15,26 +14,26 @@ export namespace LlmDataMerger { * Properties of {@link parameters} function. */ export interface IProps< - Schema extends - | ILlmSchemaV3 - | ILlmSchemaV3_1 - | IChatGptSchema - | IGeminiSchema, + Parameters extends + | ILlmSchemaV3.IParameters + | ILlmSchemaV3_1.IParameters + | IChatGptSchema.IParameters + | IGeminiSchema.IParameters, > { /** * Target function to call. */ - function: ILlmFunction; + function: ILlmFunction; /** * Arguments composed by LLM (Large Language Model). */ - llm: any[]; + llm: object | null; /** * Arguments composed by human. */ - human: any[]; + human: object | null; } /** @@ -53,29 +52,20 @@ export namespace LlmDataMerger { * @returns Combined arguments */ export const parameters = < - Schema extends - | ILlmSchemaV3 - | ILlmSchemaV3_1 - | IChatGptSchema - | IGeminiSchema, + Parameters extends + | ILlmSchemaV3.IParameters + | ILlmSchemaV3_1.IParameters + | IChatGptSchema.IParameters + | IGeminiSchema.IParameters, >( - props: IProps, - ): unknown[] => { - const separated: IHttpLlmFunction.ISeparated | undefined = - props.function.separated; + props: IProps, + ): object => { + const separated = 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]); - }); + return value(props.llm, props.human) as object; }; /** diff --git a/src/utils/OpenApiTypeChecker.ts b/src/utils/OpenApiTypeChecker.ts index 8194ad4..a2902a8 100644 --- a/src/utils/OpenApiTypeChecker.ts +++ b/src/utils/OpenApiTypeChecker.ts @@ -75,18 +75,19 @@ export namespace OpenApiTypeChecker { schema: OpenApi.IJsonSchema; }): boolean => { if (isReference(props.schema) === false) return false; - const counter: Map = new Map(); + const current: string = props.schema.$ref.split("#/components/schemas/")[1]; + let counter: number = 0; visit({ components: props.components, schema: props.schema, closure: (schema) => { if (OpenApiTypeChecker.isReference(schema)) { - const key: string = schema.$ref.split("#/components/schemas/")[1]; - counter.set(key, (counter.get(key) ?? 0) + 1); + const next: string = schema.$ref.split("#/components/schemas/")[1]; + if (current === next) ++counter; } }, }); - return Array.from(counter.values()).some((v) => v > 1); + return counter > 1; }; /* ----------------------------------------------------------- diff --git a/test/TestGlobal.ts b/test/TestGlobal.ts index 2537d80..3ffe147 100644 --- a/test/TestGlobal.ts +++ b/test/TestGlobal.ts @@ -1,6 +1,25 @@ -export namespace TestGlobal { - export const ROOT: string = +import dotenv from "dotenv"; +import dotenvExpand from "dotenv-expand"; +import { Singleton } from "tstl"; +import typia from "typia"; + +export class TestGlobal { + public static readonly ROOT: string = __filename.substring(__filename.length - 2) === "js" ? `${__dirname}/../..` : `${__dirname}/..`; + + public static get env(): IEnvironments { + return environments.get(); + } } + +interface IEnvironments { + OPENAI_API_KEY?: string; +} + +const environments = new Singleton(() => { + const env = dotenv.config(); + dotenvExpand.expand(env); + return typia.assert(process.env); +}); diff --git a/test/examples/execute.ts b/test/examples/execute.ts index 469f9af..bbbee12 100644 --- a/test/examples/execute.ts +++ b/test/examples/execute.ts @@ -30,11 +30,11 @@ const main = async (): Promise => { }); // Let's imagine that LLM has selected a function to call - const func: IHttpLlmFunction | undefined = + const func: IHttpLlmFunction | undefined = application.functions.find( (f) => f.path === "/bbs/articles" && f.method === "post", ); - typia.assertGuard>(func); + typia.assertGuard>(func); // actual execution is by yourself const article = await HttpLlm.execute({ @@ -43,13 +43,13 @@ const main = async (): Promise => { }, application, function: func, - arguments: [ - "general", - { + input: { + section: "general", + body: { title: "Hello, world!", body: "Let's imagine that this argument is composed by LLM.", }, - ], + }, }); console.log("article", article); }; diff --git a/test/examples/keyword.ts b/test/examples/keyword.ts index f68439a..758f15e 100644 --- a/test/examples/keyword.ts +++ b/test/examples/keyword.ts @@ -28,13 +28,11 @@ const main = async (): Promise => { const application: IHttpLlmApplication<"3.0"> = HttpLlm.application({ model: "3.0", document, - options: { - keyword: true, - }, + options: {}, }); // Let's imagine that LLM has selected a function to call - const func: IHttpLlmFunction | undefined = + const func: IHttpLlmFunction | undefined = application.functions.find( // (f) => f.name === "llm_selected_fuction_name" (f) => f.path === "/bbs/articles/{id}" && f.method === "put", @@ -48,7 +46,7 @@ const main = async (): Promise => { }, application, function: func, - arguments: [ + input: [ { section: "general", id: v4(), diff --git a/test/examples/separate.ts b/test/examples/separate.ts deleted file mode 100644 index eaeb824..0000000 --- a/test/examples/separate.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { - HttpLlm, - IHttpLlmApplication, - IHttpLlmFunction, - ILlmSchemaV3, - LlmTypeCheckerV3, - 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<"3.0"> = HttpLlm.application({ - model: "3.0", - document, - options: { - keyword: false, - separate: (schema) => - LlmTypeCheckerV3.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); diff --git a/test/features/llm/chatgpt/test_chatgpt_schema_oneof_discriminator.ts b/test/features/llm/chatgpt/test_chatgpt_schema_anyof.ts similarity index 53% rename from test/features/llm/chatgpt/test_chatgpt_schema_oneof_discriminator.ts rename to test/features/llm/chatgpt/test_chatgpt_schema_anyof.ts index 2d2059d..737aad1 100644 --- a/test/features/llm/chatgpt/test_chatgpt_schema_oneof_discriminator.ts +++ b/test/features/llm/chatgpt/test_chatgpt_schema_anyof.ts @@ -3,38 +3,30 @@ import { IChatGptSchema } from "@samchon/openapi"; import { ChatGptConverter } from "@samchon/openapi/lib/converters/ChatGptConverter"; import typia, { IJsonSchemaCollection } from "typia"; -export const test_chatgpt_schema_oneof_discriminator = (): void => { +export const test_chatgpt_schema_anyof = (): void => { const collection: IJsonSchemaCollection = typia.json.schemas<[IPoint | ILine | ITriangle | IRectangle]>(); - const schema = ChatGptConverter.schema({ + + const $defs: Record = {}; + const schema: IChatGptSchema | null = ChatGptConverter.schema({ + $defs, components: collection.components, schema: collection.schemas[0], + escape: true, }); - TestValidator.equals("discriminator")({ - oneOf: [ - { - $ref: "#/$defs/IPoint", - }, - { - $ref: "#/$defs/ILine", - }, - { - $ref: "#/$defs/ITriangle", - }, - { - $ref: "#/$defs/IRectangle", - }, - ], - discriminator: { - propertyName: "type", - mapping: { - point: "#/$defs/IPoint", - line: "#/$defs/ILine", - triangle: "#/$defs/ITriangle", - rectangle: "#/$defs/IRectangle", + const type = (str: string) => ({ + type: "object", + properties: { + type: { + type: "string", + enum: [str], }, }, - } satisfies IChatGptSchema as IChatGptSchema)(schema as IChatGptSchema); + }); + + TestValidator.equals("anyOf")({ + anyOf: [type("point"), type("line"), type("triangle"), type("rectangle")], + })(schema as any); }; interface IPoint { diff --git a/test/features/llm/chatgpt/test_chatgpt_schema_recursive_array.ts b/test/features/llm/chatgpt/test_chatgpt_schema_recursive_array.ts index 1024b5e..a3b2d48 100644 --- a/test/features/llm/chatgpt/test_chatgpt_schema_recursive_array.ts +++ b/test/features/llm/chatgpt/test_chatgpt_schema_recursive_array.ts @@ -1,8 +1,11 @@ import { TestValidator } from "@nestia/e2e"; +import { IChatGptSchema } from "@samchon/openapi"; import { ChatGptConverter } from "@samchon/openapi/lib/converters/ChatGptConverter"; export const test_chatgpt_schema_recursive_array = (): void => { + const $defs: Record = {}; const schema = ChatGptConverter.schema({ + $defs, components: { schemas: { Department: { @@ -25,26 +28,27 @@ export const test_chatgpt_schema_recursive_array = (): void => { schema: { $ref: "#/components/schemas/Department", }, + escape: true, }); - TestValidator.equals("recursive")(schema)({ - $ref: "#/$defs/Department", - $defs: { - Department: { - type: "object", - properties: { - name: { - type: "string", - }, - children: { - type: "array", - items: { - $ref: "#/$defs/Department", - }, + TestValidator.equals("$defs")($defs)({ + Department: { + type: "object", + properties: { + name: { + type: "string", + }, + children: { + type: "array", + items: { + $ref: "#/$defs/Department", }, }, - required: ["name", "children"], - additionalProperties: false, }, + required: ["name", "children"], + additionalProperties: false, }, }); + TestValidator.equals("schema")(schema)({ + $ref: "#/$defs/Department", + }); }; diff --git a/test/features/llm/chatgpt/test_chatgpt_schema_ref.ts b/test/features/llm/chatgpt/test_chatgpt_schema_ref.ts index 9dfc9a4..e3a0ee1 100644 --- a/test/features/llm/chatgpt/test_chatgpt_schema_ref.ts +++ b/test/features/llm/chatgpt/test_chatgpt_schema_ref.ts @@ -1,18 +1,19 @@ import { TestValidator } from "@nestia/e2e"; import { IChatGptSchema } from "@samchon/openapi"; import { ChatGptConverter } from "@samchon/openapi/lib/converters/ChatGptConverter"; -import typia, { IJsonSchemaCollection, tags } from "typia"; +import typia, { IJsonSchemaCollection } from "typia"; export const test_chatgpt_schema_ref = (): void => { test(typia.json.schemas<[IShoppingCategory]>(), { - $ref: "#/$defs/IShoppingCategory", + schema: { + $ref: "#/$defs/IShoppingCategory", + }, $defs: { IShoppingCategory: { type: "object", properties: { id: { type: "string", - format: "uuid", }, name: { type: "string", @@ -30,20 +31,21 @@ export const test_chatgpt_schema_ref = (): void => { }, }); test(typia.json.schemas<[IShoppingCategory.IInvert]>(), { - $ref: "#/$defs/IShoppingCategory.IInvert", + schema: { + $ref: "#/$defs/IShoppingCategory.IInvert", + }, $defs: { "IShoppingCategory.IInvert": { type: "object", properties: { id: { type: "string", - format: "uuid", }, name: { type: "string", }, parent: { - oneOf: [ + anyOf: [ { type: "null", }, @@ -62,23 +64,32 @@ export const test_chatgpt_schema_ref = (): void => { const test = ( collection: IJsonSchemaCollection, - expected: IChatGptSchema.ITop, + expected: { + schema: IChatGptSchema; + $defs: Record; + }, ): void => { - const schema: IChatGptSchema.ITop | null = ChatGptConverter.schema({ + const $defs: Record = {}; + const schema: IChatGptSchema | null = ChatGptConverter.schema({ + $defs, components: collection.components, schema: collection.schemas[0], + escape: true, + }); + TestValidator.equals("ref")(expected)({ + $defs, + schema: schema!, }); - TestValidator.equals("ref")(schema)(expected); }; interface IShoppingCategory { - id: string & tags.Format<"uuid">; + id: string; name: string; children: IShoppingCategory[]; } namespace IShoppingCategory { export interface IInvert { - id: string & tags.Format<"uuid">; + id: string; name: string; parent: IShoppingCategory.IInvert | null; } diff --git a/test/features/llm/function-calling/test_llm_function_calling_chatgpt_recursive.ts b/test/features/llm/function-calling/test_llm_function_calling_chatgpt_recursive.ts new file mode 100644 index 0000000..f9756ca --- /dev/null +++ b/test/features/llm/function-calling/test_llm_function_calling_chatgpt_recursive.ts @@ -0,0 +1,112 @@ +import { ArrayUtil, TestValidator } from "@nestia/e2e"; +import { IChatGptSchema, OpenApi } from "@samchon/openapi"; +import { ChatGptConverter } from "@samchon/openapi/lib/converters/ChatGptConverter"; +import fs from "fs"; +import OpenAI from "openai"; +import { ChatCompletion } from "openai/resources"; +import typia, { IJsonSchemaCollection } from "typia"; + +import { TestGlobal } from "../../../TestGlobal"; + +export const test_provider_chatgpt_function_calling_recursive = + async (): Promise => { + if (TestGlobal.env.OPENAI_API_KEY === undefined) return; + + const collection: IJsonSchemaCollection = + typia.json.schemas<[{ input: IShoppingCategory[] }]>(); + const parameters: IChatGptSchema.IParameters | null = + ChatGptConverter.parameters({ + components: collection.components, + schema: typia.assert( + collection.schemas[0], + ), + escape: false, + }); + if (parameters === null) + throw new Error( + "Failed to convert the JSON schema to the ChatGPT schema.", + ); + await fs.promises.writeFile( + `${TestGlobal.ROOT}/examples/function-calling/recursive.schema.json`, + JSON.stringify(parameters, null, 2), + "utf8", + ); + + const client: OpenAI = new OpenAI({ + apiKey: TestGlobal.env.OPENAI_API_KEY, + }); + const completion: ChatCompletion = await client.chat.completions.create({ + model: "gpt-4o", + messages: [ + { + role: "system", + content: SYSTEM_MESSAGE, + }, + { + role: "user", + content: USER_MESSAGE, + }, + ], + tools: [ + { + type: "function", + function: { + name: "composeCategories", + description: "Compose categories from the input.", + parameters: parameters as any, + strict: true, + }, + }, + ], + }); + await ArrayUtil.asyncMap(completion.choices[0].message.tool_calls ?? [])( + async (call) => { + TestValidator.equals("name")(call.function.name)("composeCategories"); + const { input } = typia.assert<{ + input: IShoppingCategory[]; + }>(JSON.parse(call.function.arguments)); + await fs.promises.writeFile( + `${TestGlobal.ROOT}/examples/function-calling/recursive.input.json`, + JSON.stringify(input, null, 2), + "utf8", + ); + }, + ); + }; + +interface IShoppingCategory { + id: string; + name: string; + children: IShoppingCategory[]; +} + +const SYSTEM_MESSAGE = + "You are a helpful customer support assistant. Use the supplied tools to assist the user."; +const USER_MESSAGE = ` + I'll insert a shopping category tree here. + + - electronics + - desktops + - laptops + - ultrabooks + - macbooks + - desknotes + - 2 in 1 laptops + - tablets + - ipads + - android tablets + - windows tablets + - smartphones + - mini smartphones + - phablets + - gaming smartphones + - rugged smartphones + - foldable smartphones + - cameras + - televisions + - furnitures + - accessories + - jewelry + - clothing + - shoes +`; diff --git a/test/features/llm/function-calling/test_llm_function_calling_chatgpt_sale.ts b/test/features/llm/function-calling/test_llm_function_calling_chatgpt_sale.ts new file mode 100644 index 0000000..fcbdae6 --- /dev/null +++ b/test/features/llm/function-calling/test_llm_function_calling_chatgpt_sale.ts @@ -0,0 +1,119 @@ +import { ArrayUtil, TestValidator } from "@nestia/e2e"; +import { IChatGptSchema, OpenApi } from "@samchon/openapi"; +import { ChatGptConverter } from "@samchon/openapi/lib/converters/ChatGptConverter"; +import fs from "fs"; +import OpenAI from "openai"; +import { ChatCompletion } from "openai/resources"; +import typia, { IJsonSchemaCollection } from "typia"; + +import { TestGlobal } from "../../../TestGlobal"; +import { IShoppingSale } from "../../../structures/IShoppingSale"; + +export const test_llm_function_calling_chatgpt_sale = + async (): Promise => { + if (TestGlobal.env.OPENAI_API_KEY === undefined) return; + + const collection: IJsonSchemaCollection = + typia.json.schemas<[{ input: IShoppingSale.ICreate }]>(); + const parameters: IChatGptSchema.IParameters | null = + ChatGptConverter.parameters({ + components: collection.components, + schema: typia.assert( + collection.schemas[0], + ), + escape: process.argv.includes("--escape"), + tag: process.argv.includes("--tag"), + }); + if (parameters === null) + throw new Error( + "Failed to convert the JSON schema to the ChatGPT schema.", + ); + await fs.promises.writeFile( + `${TestGlobal.ROOT}/examples/function-calling/sale.schema.json`, + JSON.stringify(parameters, null, 2), + "utf8", + ); + + const client: OpenAI = new OpenAI({ + apiKey: TestGlobal.env.OPENAI_API_KEY, + }); + const completion: ChatCompletion = await client.chat.completions.create({ + model: "gpt-4o", + messages: [ + { + role: "assistant", + content: SYSTEM_MESSAGE, + }, + { + role: "user", + content: USER_MESSAGE, + }, + ], + tools: [ + { + type: "function", + function: { + name: "createSale", + description: "Create a sale and returns the detailed information.", + parameters: parameters as any, + strict: true, + }, + }, + ], + }); + await ArrayUtil.asyncForEach( + completion.choices[0].message.tool_calls ?? [], + )(async (call) => { + TestValidator.equals("name")(call.function.name)("createSale"); + const { input } = typia.assert<{ + input: IShoppingSale.ICreate; + }>(JSON.parse(call.function.arguments)); + await fs.promises.writeFile( + `${TestGlobal.ROOT}/examples/function-calling/sale.input.json`, + JSON.stringify(input, null, 2), + "utf8", + ); + }); + }; + +const SYSTEM_MESSAGE = + "You are a helpful customer support assistant. Use the supplied tools to assist the user."; +const USER_MESSAGE = ` + I will create a sale with the following details. + At first, title of the sale is "Surface Pro 8". + Then, the sale has a description "The best laptop for your daily needs.". + + Also, it has only two unit, the "Surface Pro 8 Entity" and "Warranty Program". + + About the "Warranty Program" unit, it is not essential to the sale, + and there is no option to select. Its nominal price is $99, and + the real price is $89. + + About the "Surface Pro 8 Entity", it is essential to the sale, and + there are two options to select like below. + + - CPU + - Intel Core i3 + - Intel Core i5 + - Intel Core i7 + - RAM + - 8 GB + - 16 GB + - 32 GB + - Storage + - 128 GB + - 256 GB + - 512 GB + + The final stocks combinated by the options are like below. + The sequence of selected options are {(CPU, RAM, Storage): (nominal price / real price)}. + Also, quantity of them are fixed to 1,000 value. + + - (i3, 8 GB, 128 GB): ($999 / $899) + - (i3, 16 GB, 256 GB): ($1,199 / $1,099) + - (i3, 16 GB, 512 GB): ($1,399 / $1,299) + - (i5, 16 GB, 256 GB): ($1,499 / $1,399) + - (i5, 32 GB, 512 GB): ($1,799 / $1,699) + - (i7, 16 GB, 512 GB): ($1,799 / $1,699) + - (i7, 32 GB, 512 GB): ($1,999 / $1,899) +`; diff --git a/test/features/llm/function-calling/test_llm_function_calling_chatgpt_tags.ts b/test/features/llm/function-calling/test_llm_function_calling_chatgpt_tags.ts new file mode 100644 index 0000000..3b491d0 --- /dev/null +++ b/test/features/llm/function-calling/test_llm_function_calling_chatgpt_tags.ts @@ -0,0 +1,86 @@ +import { ArrayUtil, TestValidator } from "@nestia/e2e"; +import { IChatGptSchema, OpenApi } from "@samchon/openapi"; +import { ChatGptConverter } from "@samchon/openapi/lib/converters/ChatGptConverter"; +import fs from "fs"; +import OpenAI from "openai"; +import { ChatCompletion } from "openai/resources"; +import typia, { IJsonSchemaCollection, tags } from "typia"; + +import { TestGlobal } from "../../../TestGlobal"; + +export const test_llm_function_calling_chatgpt_tags = + async (): Promise => { + if (TestGlobal.env.OPENAI_API_KEY === undefined) return; + + const collection: IJsonSchemaCollection = + typia.json.schemas<[{ input: OpeningTime }]>(); + const parameters: IChatGptSchema.IParameters | null = + ChatGptConverter.parameters({ + components: collection.components, + schema: typia.assert( + collection.schemas[0], + ), + escape: false, + }); + if (parameters === null) + throw new Error( + "Failed to convert the JSON schema to the ChatGPT schema.", + ); + await fs.promises.writeFile( + `${TestGlobal.ROOT}/examples/function-calling/tags.schema.json`, + JSON.stringify(parameters, null, 2), + "utf8", + ); + + const client: OpenAI = new OpenAI({ + apiKey: TestGlobal.env.OPENAI_API_KEY, + }); + const completion: ChatCompletion = await client.chat.completions.create({ + model: "gpt-4o", + messages: [ + { + role: "system", + content: SYSTEM_MESSAGE, + }, + { + role: "user", + content: USER_MESSAGE, + }, + ], + tools: [ + { + type: "function", + function: { + name: "reserve", + description: "Reserve some opening time.", + parameters: parameters as any, + }, + }, + ], + }); + await ArrayUtil.asyncMap(completion.choices[0].message.tool_calls ?? [])( + async (call) => { + TestValidator.equals("name")(call.function.name)("reserve"); + const { input } = typia.assert<{ + input: OpeningTime; + }>(JSON.parse(call.function.arguments)); + await fs.promises.writeFile( + `${TestGlobal.ROOT}/examples/function-calling/tags.input.json`, + JSON.stringify(input, null, 2), + "utf8", + ); + }, + ); + }; + +const SYSTEM_MESSAGE = + "You are a helpful customer support assistant. Use the supplied tools to assist the user."; +const USER_MESSAGE = ` + Reserve current date-time permenantely as the reason of marketing sales. +`; + +interface OpeningTime { + reasons: Array & tags.MinItems<1>; + temporary: boolean; + time: (string & tags.Format<"date-time">) | null; +} diff --git a/test/features/llm/test_http_llm_application_keyword.ts b/test/features/llm/test_http_llm_application.ts similarity index 60% rename from test/features/llm/test_http_llm_application_keyword.ts rename to test/features/llm/test_http_llm_application.ts index dbe58a0..9e4d6b6 100644 --- a/test/features/llm/test_http_llm_application_keyword.ts +++ b/test/features/llm/test_http_llm_application.ts @@ -3,35 +3,25 @@ import { HttpLlm, IHttpLlmApplication, IHttpMigrateRoute, - ILlmSchemaV3, - LlmTypeCheckerV3, OpenApi, } from "@samchon/openapi"; import swagger from "../../swagger.json"; -export const test_http_llm_application_keyword = (): void => { +export const test_http_llm_application = (): void => { const document: OpenApi.IDocument = OpenApi.convert(swagger as any); const application: IHttpLlmApplication<"3.0"> = HttpLlm.application({ model: "3.0", document, - options: { - keyword: true, - }, + options: {}, }); for (const func of application.functions) { const route: IHttpMigrateRoute = func.route(); - TestValidator.equals("length")(1)(func.parameters.length); + TestValidator.equals("type")({ type: "object" })(func.parameters); TestValidator.equals("properties")([ ...route.parameters.map((p) => p.key), ...(route.query ? ["query"] : []), ...(route.body ? ["body"] : []), - ])( - (() => { - const schema: ILlmSchemaV3 = func.parameters[0]; - if (!LlmTypeCheckerV3.isObject(schema)) return []; - return Object.keys(schema.properties ?? {}); - })(), - ); + ])(Object.keys(func.parameters.properties ?? {})); } }; diff --git a/test/features/llm/test_http_llm_application_positional.ts b/test/features/llm/test_http_llm_application_positional.ts deleted file mode 100644 index ee2c11e..0000000 --- a/test/features/llm/test_http_llm_application_positional.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { TestValidator } from "@nestia/e2e"; -import { - HttpLlm, - 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<"3.0"> = HttpLlm.application({ - model: "3.0", - document, - options: { - 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_http_llm_fetcher_positional_body.ts b/test/features/llm/test_http_llm_fetcher_body.ts similarity index 78% rename from test/features/llm/test_http_llm_fetcher_positional_body.ts rename to test/features/llm/test_http_llm_fetcher_body.ts index 74f1cff..fc23109 100644 --- a/test/features/llm/test_http_llm_fetcher_positional_body.ts +++ b/test/features/llm/test_http_llm_fetcher_body.ts @@ -12,7 +12,7 @@ import { import swagger from "../../swagger.json"; -export const test_http_llm_fetcher_positional_body = async ( +export const test_http_llm_fetcher_body = async ( connection: IHttpConnection, ): Promise => { const document: OpenApi.IDocument = OpenApi.convert(swagger as any); @@ -20,12 +20,11 @@ export const test_http_llm_fetcher_positional_body = async ( model: "3.0", document, options: { - keyword: false, separate: (schema) => LlmTypeCheckerV3.isString(schema) && !!schema.contentMediaType, }, }); - const func: IHttpLlmFunction | undefined = + const func: IHttpLlmFunction | undefined = application.functions.find( (f) => f.path === "/{index}/{level}/{optimal}/body" && f.method === "post", @@ -36,18 +35,20 @@ export const test_http_llm_fetcher_positional_body = async ( connection, application, function: func, - arguments: HttpLlm.mergeParameters({ + input: HttpLlm.mergeParameters({ function: func, - llm: [ - 123, - true, - { + llm: { + level: 123, + optimal: true, + body: { title: "some title", body: "some body", draft: false, }, - ], - human: ["https://some.url/index.html"], + }, + 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_body.ts b/test/features/llm/test_http_llm_fetcher_keyword_body.ts deleted file mode 100644 index d7cad1d..0000000 --- a/test/features/llm/test_http_llm_fetcher_keyword_body.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { TestValidator } from "@nestia/e2e"; -import { - HttpLlm, - IHttpConnection, - IHttpLlmApplication, - IHttpLlmFunction, - IHttpResponse, - ILlmSchemaV3, - LlmTypeCheckerV3, - 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<"3.0"> = HttpLlm.application({ - model: "3.0", - document, - options: { - keyword: true, - separate: (schema) => - LlmTypeCheckerV3.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 deleted file mode 100644 index eb20b51..0000000 --- a/test/features/llm/test_http_llm_fetcher_keyword_parameters.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { TestValidator } from "@nestia/e2e"; -import { - HttpLlm, - IHttpConnection, - IHttpLlmApplication, - IHttpLlmFunction, - IHttpResponse, - ILlmSchemaV3, - LlmTypeCheckerV3, - 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<"3.0"> = HttpLlm.application({ - model: "3.0", - document, - options: { - keyword: true, - separate: (schema) => - LlmTypeCheckerV3.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 deleted file mode 100644 index 4b99f00..0000000 --- a/test/features/llm/test_http_llm_fetcher_keyword_query.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { TestValidator } from "@nestia/e2e"; -import { - HttpLlm, - IHttpConnection, - IHttpLlmApplication, - IHttpLlmFunction, - IHttpResponse, - ILlmSchemaV3, - LlmTypeCheckerV3, - 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<"3.0"> = HttpLlm.application({ - model: "3.0", - document, - options: { - keyword: true, - separate: (schema) => - LlmTypeCheckerV3.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 deleted file mode 100644 index 84e7eb2..0000000 --- a/test/features/llm/test_http_llm_fetcher_keyword_query_and_body.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { TestValidator } from "@nestia/e2e"; -import { - HttpLlm, - IHttpConnection, - IHttpLlmApplication, - IHttpLlmFunction, - IHttpResponse, - ILlmSchemaV3, - LlmTypeCheckerV3, - 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<"3.0"> = HttpLlm.application({ - model: "3.0", - document, - options: { - keyword: true, - separate: (schema) => - LlmTypeCheckerV3.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_parameters.ts b/test/features/llm/test_http_llm_fetcher_parameters.ts similarity index 77% rename from test/features/llm/test_http_llm_fetcher_positional_parameters.ts rename to test/features/llm/test_http_llm_fetcher_parameters.ts index 5f86adb..6bb6848 100644 --- a/test/features/llm/test_http_llm_fetcher_positional_parameters.ts +++ b/test/features/llm/test_http_llm_fetcher_parameters.ts @@ -12,7 +12,7 @@ import { import swagger from "../../swagger.json"; -export const test_http_llm_fetcher_keyword_parameters = async ( +export const test_http_llm_fetcher_parameters = async ( connection: IHttpConnection, ): Promise => { const document: OpenApi.IDocument = OpenApi.convert(swagger as any); @@ -20,12 +20,11 @@ export const test_http_llm_fetcher_keyword_parameters = async ( model: "3.0", document, options: { - keyword: false, separate: (schema) => LlmTypeCheckerV3.isString(schema) && !!schema.contentMediaType, }, }); - const func: IHttpLlmFunction | undefined = + const func: IHttpLlmFunction | undefined = application.functions.find( (f) => f.path === "/{index}/{level}/{optimal}/parameters" && @@ -37,10 +36,15 @@ export const test_http_llm_fetcher_keyword_parameters = async ( connection, application, function: func, - arguments: HttpLlm.mergeParameters({ + input: HttpLlm.mergeParameters({ function: func, - llm: [123, true], - human: ["https://some.url/index.html"], + 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_positional_query.ts b/test/features/llm/test_http_llm_fetcher_query.ts similarity index 77% rename from test/features/llm/test_http_llm_fetcher_positional_query.ts rename to test/features/llm/test_http_llm_fetcher_query.ts index c6548fb..532739a 100644 --- a/test/features/llm/test_http_llm_fetcher_positional_query.ts +++ b/test/features/llm/test_http_llm_fetcher_query.ts @@ -12,7 +12,7 @@ import { import swagger from "../../swagger.json"; -export const test_http_llm_fetcher_keyword_query = async ( +export const test_http_llm_fetcher_query = async ( connection: IHttpConnection, ): Promise => { const document: OpenApi.IDocument = OpenApi.convert(swagger as any); @@ -20,12 +20,11 @@ export const test_http_llm_fetcher_keyword_query = async ( model: "3.0", document, options: { - keyword: false, separate: (schema) => LlmTypeCheckerV3.isString(schema) && !!schema.contentMediaType, }, }); - const func: IHttpLlmFunction | undefined = + const func: IHttpLlmFunction | undefined = application.functions.find( (f) => f.path === "/{index}/{level}/{optimal}/query" && f.method === "get", @@ -36,21 +35,21 @@ export const test_http_llm_fetcher_keyword_query = async ( connection, application, function: func, - arguments: HttpLlm.mergeParameters({ + input: HttpLlm.mergeParameters({ function: func, - llm: [ - 123, - true, - { + llm: { + level: 123, + optimal: true, + query: { summary: "some summary", }, - ], - human: [ - "https://some.url/index.html", - { + }, + 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_positional_query_and_body.ts b/test/features/llm/test_http_llm_fetcher_query_and_body.ts similarity index 77% rename from test/features/llm/test_http_llm_fetcher_positional_query_and_body.ts rename to test/features/llm/test_http_llm_fetcher_query_and_body.ts index 3676883..2d7a5f0 100644 --- a/test/features/llm/test_http_llm_fetcher_positional_query_and_body.ts +++ b/test/features/llm/test_http_llm_fetcher_query_and_body.ts @@ -12,7 +12,7 @@ import { import swagger from "../../swagger.json"; -export const test_http_llm_fetcher_keyword_query_and_body = async ( +export const test_http_llm_fetcher_query_and_body = async ( connection: IHttpConnection, ): Promise => { const document: OpenApi.IDocument = OpenApi.convert(swagger as any); @@ -20,12 +20,11 @@ export const test_http_llm_fetcher_keyword_query_and_body = async ( model: "3.0", document, options: { - keyword: false, separate: (schema) => LlmTypeCheckerV3.isString(schema) && !!schema.contentMediaType, }, }); - const func: IHttpLlmFunction | undefined = + const func: IHttpLlmFunction | undefined = application.functions.find( (f) => f.path === "/{index}/{level}/{optimal}/query/body" && @@ -37,26 +36,26 @@ export const test_http_llm_fetcher_keyword_query_and_body = async ( connection, application, function: func, - arguments: HttpLlm.mergeParameters({ + input: HttpLlm.mergeParameters({ function: func, - llm: [ - 123, - true, - { + llm: { + level: 123, + optimal: true, + query: { summary: "some summary", }, - { + body: { title: "some title", body: "some body", draft: false, }, - ], - human: [ - "https://some.url/index.html", - { + }, + 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_function_deprecated.ts b/test/features/llm/test_http_llm_function_deprecated.ts index 87d1ab8..a0a49f8 100644 --- a/test/features/llm/test_http_llm_function_deprecated.ts +++ b/test/features/llm/test_http_llm_function_deprecated.ts @@ -14,11 +14,8 @@ export const test_http_llm_function_deprecated = (): void => { const application: IHttpLlmApplication<"3.0"> = HttpLlm.application({ model: "3.0", document, - options: { - keyword: true, - }, }); - const func: IHttpLlmFunction | undefined = + const func: IHttpLlmFunction | undefined = application.functions.find( (f) => f.method === "get" && f.path === "/nothing", ); diff --git a/test/features/llm/test_http_llm_function_multipart.ts b/test/features/llm/test_http_llm_function_multipart.ts index 77336cf..9042527 100644 --- a/test/features/llm/test_http_llm_function_multipart.ts +++ b/test/features/llm/test_http_llm_function_multipart.ts @@ -8,9 +8,6 @@ export const test_http_llm_function_multipart = (): void => { const application: IHttpLlmApplication<"3.0"> = HttpLlm.application({ model: "3.0", document, - options: { - keyword: true, - }, }); TestValidator.equals("multipart not suppported")( !!application.errors.find( diff --git a/test/features/llm/test_http_llm_function_tags.ts b/test/features/llm/test_http_llm_function_tags.ts index c7fe031..05cbbbc 100644 --- a/test/features/llm/test_http_llm_function_tags.ts +++ b/test/features/llm/test_http_llm_function_tags.ts @@ -14,11 +14,8 @@ export const test_http_llm_function_deprecated = (): void => { const application: IHttpLlmApplication<"3.0"> = HttpLlm.application({ model: "3.0", document, - options: { - keyword: true, - }, }); - const func: IHttpLlmFunction | undefined = + const func: IHttpLlmFunction | undefined = application.functions.find( (f) => f.method === "post" && f.path === "/{index}/{level}/{optimal}/body", diff --git a/test/features/llm/test_llm_merge_parameters.ts b/test/features/llm/test_llm_merge_parameters.ts index 759a6a6..2e5aa6e 100644 --- a/test/features/llm/test_llm_merge_parameters.ts +++ b/test/features/llm/test_llm_merge_parameters.ts @@ -5,38 +5,53 @@ export const test_llm_merge_parameters = (): void => { TestValidator.equals("atomics")( HttpLlm.mergeParameters({ function: { + strict: true, name: "test", - parameters: [ - { type: "boolean" }, - { type: "number" }, - { type: "string" }, - { type: "string" }, - ], + parameters: { + type: "object", + properties: { + a: { type: "boolean" }, + b: { type: "number" }, + c: { type: "string" }, + d: { type: "string" }, + }, + additionalProperties: false, + required: ["a", "b", "c", "d"], + }, separated: { - human: [ - { - schema: { type: "boolean" }, - index: 0, - }, - { - schema: { type: "number" }, - index: 1, - }, - ], - llm: [ - { - schema: { type: "string" }, - index: 2, + human: { + type: "object", + properties: { + a: { type: "boolean" }, + b: { type: "number" }, }, - { - schema: { type: "string" }, - index: 3, + additionalProperties: false, + required: ["a", "b"], + }, + llm: { + type: "object", + properties: { + c: { type: "string" }, + d: { type: "string" }, }, - ], + additionalProperties: false, + required: ["c", "d"], + }, }, }, - human: [false, 1], - llm: ["two", "three"], + human: { + a: false, + b: 1, + }, + llm: { + c: "two", + d: "three", + }, }), - )([false, 1, "two", "three"]); + )({ + a: false, + b: 1, + c: "two", + d: "three", + }); }; diff --git a/test/features/llm/test_llm_schema_nullable.ts b/test/features/llm/test_llm_schema_nullable.ts index 12e0093..8fc7ed5 100644 --- a/test/features/llm/test_llm_schema_nullable.ts +++ b/test/features/llm/test_llm_schema_nullable.ts @@ -1,5 +1,6 @@ import { TestValidator } from "@nestia/e2e"; -import { HttpLlm, ILlmSchemaV3, OpenApi } from "@samchon/openapi"; +import { ILlmSchemaV3, OpenApi } from "@samchon/openapi"; +import { LlmConverterV3 } from "@samchon/openapi/lib/converters/LlmConverterV3"; export const test_llm_schema_union = (): void => { const components: OpenApi.IComponents = { @@ -39,8 +40,7 @@ export const test_llm_schema_union = (): void => { }, ], }; - const llm: ILlmSchemaV3 | null = HttpLlm.schema({ - model: "3.0", + const llm: ILlmSchemaV3 | null = LlmConverterV3.schema({ components, schema, recursive: false, diff --git a/test/features/llm/test_llm_schema_object.ts b/test/features/llm/test_llm_schema_object.ts index f81ccb8..206aa83 100644 --- a/test/features/llm/test_llm_schema_object.ts +++ b/test/features/llm/test_llm_schema_object.ts @@ -1,11 +1,11 @@ import { TestValidator } from "@nestia/e2e"; -import { HttpLlm, ILlmSchemaV3 } from "@samchon/openapi"; +import { ILlmSchemaV3 } from "@samchon/openapi"; +import { LlmConverterV3 } from "@samchon/openapi/lib/converters/LlmConverterV3"; import typia, { IJsonSchemaCollection, tags } from "typia"; export const test_llm_schema_object = (): void => { const app: IJsonSchemaCollection = typia.json.schemas<[First]>(); - const schema: ILlmSchemaV3 | null = HttpLlm.schema({ - model: "3.0", + const schema: ILlmSchemaV3 | null = LlmConverterV3.schema({ components: app.components, schema: app.schemas[0], recursive: false, diff --git a/test/features/llm/test_llm_schema_oneof.ts b/test/features/llm/test_llm_schema_oneof.ts index d262f0f..51e30ab 100644 --- a/test/features/llm/test_llm_schema_oneof.ts +++ b/test/features/llm/test_llm_schema_oneof.ts @@ -1,12 +1,12 @@ import { TestValidator } from "@nestia/e2e"; -import { HttpLlm, ILlmSchemaV3 } from "@samchon/openapi"; +import { ILlmSchemaV3 } from "@samchon/openapi"; +import { LlmConverterV3 } from "@samchon/openapi/lib/converters/LlmConverterV3"; import typia, { IJsonSchemaCollection } from "typia"; export const test_llm_schema_oneof = (): void => { const app: IJsonSchemaCollection = typia.json.schemas<[Circle | Triangle | Rectangle]>(); - const casted: ILlmSchemaV3 | null = HttpLlm.schema({ - model: "3.0", + const casted: ILlmSchemaV3 | null = LlmConverterV3.schema({ components: app.components, schema: app.schemas[0], recursive: false, diff --git a/test/features/llm/test_llm_schema_separate_array.ts b/test/features/llm/test_llm_schema_separate_array.ts index a3e6857..39f7fed 100644 --- a/test/features/llm/test_llm_schema_separate_array.ts +++ b/test/features/llm/test_llm_schema_separate_array.ts @@ -1,10 +1,5 @@ import { TestValidator } from "@nestia/e2e"; -import { - HttpLlm, - ILlmSchemaV3, - LlmTypeCheckerV3, - OpenApi, -} from "@samchon/openapi"; +import { ILlmSchemaV3, LlmTypeCheckerV3, OpenApi } from "@samchon/openapi"; import { LlmConverterV3 } from "@samchon/openapi/lib/converters/LlmConverterV3"; import typia, { tags } from "typia"; @@ -37,8 +32,7 @@ const schema = (props: { components: OpenApi.IComponents; schemas: OpenApi.IJsonSchema[]; }): ILlmSchemaV3 => { - const schema: ILlmSchemaV3 | null = HttpLlm.schema({ - model: "3.0", + const schema: ILlmSchemaV3 | null = LlmConverterV3.schema({ components: props.components, schema: props.schemas[0], recursive: false, diff --git a/test/features/llm/test_llm_schema_separate_nested.ts b/test/features/llm/test_llm_schema_separate_nested.ts index e0db835..a20a631 100644 --- a/test/features/llm/test_llm_schema_separate_nested.ts +++ b/test/features/llm/test_llm_schema_separate_nested.ts @@ -1,10 +1,5 @@ import { TestValidator } from "@nestia/e2e"; -import { - HttpLlm, - ILlmSchemaV3, - LlmTypeCheckerV3, - OpenApi, -} from "@samchon/openapi"; +import { ILlmSchemaV3, LlmTypeCheckerV3, OpenApi } from "@samchon/openapi"; import { LlmConverterV3 } from "@samchon/openapi/lib/converters/LlmConverterV3"; import typia, { tags } from "typia"; @@ -60,8 +55,7 @@ const schema = (props: { components: OpenApi.IComponents; schemas: OpenApi.IJsonSchema[]; }): ILlmSchemaV3 => { - const schema: ILlmSchemaV3 | null = HttpLlm.schema({ - model: "3.0", + const schema: ILlmSchemaV3 | null = LlmConverterV3.schema({ components: props.components, schema: props.schemas[0], recursive: false, diff --git a/test/features/llm/test_llm_schema_separate_object.ts b/test/features/llm/test_llm_schema_separate_object.ts index 0d465a3..ddc4c4e 100644 --- a/test/features/llm/test_llm_schema_separate_object.ts +++ b/test/features/llm/test_llm_schema_separate_object.ts @@ -1,10 +1,5 @@ import { TestValidator } from "@nestia/e2e"; -import { - HttpLlm, - ILlmSchemaV3, - LlmTypeCheckerV3, - OpenApi, -} from "@samchon/openapi"; +import { ILlmSchemaV3, LlmTypeCheckerV3, OpenApi } from "@samchon/openapi"; import { LlmConverterV3 } from "@samchon/openapi/lib/converters/LlmConverterV3"; import typia, { tags } from "typia"; @@ -46,8 +41,7 @@ const schema = (props: { components: OpenApi.IComponents; schemas: OpenApi.IJsonSchema[]; }): ILlmSchemaV3 => { - const schema: ILlmSchemaV3 | null = HttpLlm.schema({ - model: "3.0", + const schema: ILlmSchemaV3 | null = LlmConverterV3.schema({ components: props.components, schema: props.schemas[0], recursive: false, diff --git a/test/features/llm/test_llm_schema_union.ts b/test/features/llm/test_llm_schema_union.ts index 1cacfcf..e9422b4 100644 --- a/test/features/llm/test_llm_schema_union.ts +++ b/test/features/llm/test_llm_schema_union.ts @@ -1,5 +1,6 @@ import { TestValidator } from "@nestia/e2e"; -import { HttpLlm, ILlmSchemaV3, OpenApi } from "@samchon/openapi"; +import { ILlmSchemaV3, OpenApi } from "@samchon/openapi"; +import { LlmConverterV3 } from "@samchon/openapi/lib/converters/LlmConverterV3"; export const test_llm_schema_union = (): void => { const components: OpenApi.IComponents = { @@ -27,8 +28,7 @@ export const test_llm_schema_union = (): void => { ], }; - const llm: ILlmSchemaV3 | null = HttpLlm.schema({ - model: "3.0", + const llm: ILlmSchemaV3 | null = LlmConverterV3.schema({ components, schema, recursive: false, diff --git a/test/features/llm/test_llm_schema_union_const.ts b/test/features/llm/test_llm_schema_union_const.ts new file mode 100644 index 0000000..b59e012 --- /dev/null +++ b/test/features/llm/test_llm_schema_union_const.ts @@ -0,0 +1,29 @@ +import { TestValidator } from "@nestia/e2e"; +import { IChatGptSchema } from "@samchon/openapi"; +import { ChatGptConverter } from "@samchon/openapi/lib/converters/ChatGptConverter"; +import typia, { IJsonSchemaCollection } from "typia"; + +export const test_llm_schema_union_const = (): void => { + const collection: IJsonSchemaCollection = typia.json.schemas<[IBbsArticle]>(); + const $defs: Record = {}; + const schema: IChatGptSchema | null = ChatGptConverter.schema({ + $defs, + components: collection.components, + schema: collection.schemas[0], + escape: true, + }); + TestValidator.equals("enum")( + typia.assert( + typia.assert(schema).properties.format, + ).enum, + )(["html", "md", "txt"]); +}; + +interface IBbsArticle { + format: IBbsArticle.Format; + // title: string; + // body: string; +} +namespace IBbsArticle { + export type Format = "html" | "md" | "txt"; +} diff --git a/test/features/llm/v3.1/test_llm_schema_v31_ultimate_union.ts b/test/features/llm/v3.1/test_llm_schema_v31_ultimate_union.ts index d2f4442..4f36b15 100644 --- a/test/features/llm/v3.1/test_llm_schema_v31_ultimate_union.ts +++ b/test/features/llm/v3.1/test_llm_schema_v31_ultimate_union.ts @@ -1,11 +1,10 @@ import { OpenApi } from "@samchon/openapi"; -import { HttpLlmConverter } from "@samchon/openapi/lib/converters/HttpLlmConverter"; +import { LlmConverterV3_1 } from "@samchon/openapi/lib/converters/LlmConverterV3_1"; import typia from "typia"; export const test_llm_schema_v31_ultimate_union = (): void => { const collection = typia.json.schemas<[IJsonSchemaCollection[]]>(); - HttpLlmConverter.schema({ - model: "3.1", + LlmConverterV3_1.schema({ components: collection.components, schema: collection.schemas[0], recursive: 3, diff --git a/test/features/openapi/test_json_schema_type_checker_cover_object.ts b/test/features/openapi/test_json_schema_type_checker_cover_object.ts index 375bd22..b55763d 100644 --- a/test/features/openapi/test_json_schema_type_checker_cover_object.ts +++ b/test/features/openapi/test_json_schema_type_checker_cover_object.ts @@ -78,10 +78,12 @@ export const test_json_schema_type_checker_cover_object = (): void => { components, x: { type: "object", + properties: {}, additionalProperties: true, }, y: { type: "object", + properties: {}, }, }), ); @@ -90,12 +92,15 @@ export const test_json_schema_type_checker_cover_object = (): void => { components, x: { type: "object", + properties: {}, additionalProperties: { type: "object", + properties: {}, }, }, y: { type: "object", + properties: {}, }, }), ); @@ -104,10 +109,12 @@ export const test_json_schema_type_checker_cover_object = (): void => { components, x: { type: "object", + properties: {}, additionalProperties: true, }, y: { type: "object", + properties: {}, additionalProperties: { type: "object", properties: { @@ -122,10 +129,12 @@ export const test_json_schema_type_checker_cover_object = (): void => { components, x: { type: "object", + properties: {}, additionalProperties: box3D, }, y: { type: "object", + properties: {}, additionalProperties: box2D, }, }), @@ -184,9 +193,11 @@ export const test_json_schema_type_checker_cover_object = (): void => { components, x: { type: "object", + properties: {}, }, y: { type: "object", + properties: {}, additionalProperties: true, }, }), @@ -198,11 +209,14 @@ export const test_json_schema_type_checker_cover_object = (): void => { components, x: { type: "object", + properties: {}, }, y: { type: "object", + properties: {}, additionalProperties: { type: "object", + properties: {}, }, }, }), @@ -214,6 +228,7 @@ export const test_json_schema_type_checker_cover_object = (): void => { components, x: { type: "object", + properties: {}, additionalProperties: { type: "object", properties: { @@ -223,6 +238,7 @@ export const test_json_schema_type_checker_cover_object = (): void => { }, y: { type: "object", + properties: {}, additionalProperties: true, }, }), @@ -232,10 +248,12 @@ export const test_json_schema_type_checker_cover_object = (): void => { components, x: { type: "object", + properties: {}, additionalProperties: box2D, }, y: { type: "object", + properties: {}, additionalProperties: box3D, }, }), diff --git a/test/structures/IAttachmentFile.ts b/test/structures/IAttachmentFile.ts new file mode 100644 index 0000000..e3ac551 --- /dev/null +++ b/test/structures/IAttachmentFile.ts @@ -0,0 +1,45 @@ +import { tags } from "typia"; + +/** + * Attachment File. + * + * Every attachment files that are managed in current system. + * + * For reference, it is possible to omit one of file {@link name} + * or {@link extension} like `.gitignore` or `README` case, but not + * possible to omit both of them. + */ +export interface IAttachmentFile extends IAttachmentFile.ICreate { + /** + * Primary Key. + */ + id: string & tags.Format<"uuid">; + + /** + * Creation time of attachment file. + */ + created_at: string & tags.Format<"date-time">; +} + +export namespace IAttachmentFile { + export interface ICreate { + /** + * File name, except extension. + * + * If there's file `.gitignore`, then its name is an empty string. + */ + name: string & tags.MaxLength<255>; + + /** + * Extension. + * + * Possible to omit like `README` case. + */ + extension: null | (string & tags.MinLength<1> & tags.MaxLength<8>); + + /** + * URL path of the real file. + */ + url: string & tags.Format<"uri">; + } +} diff --git a/test/structures/IShoppingPrice.ts b/test/structures/IShoppingPrice.ts new file mode 100644 index 0000000..ce312d0 --- /dev/null +++ b/test/structures/IShoppingPrice.ts @@ -0,0 +1,22 @@ +import { tags } from "typia"; + +/** + * Shopping price interface. + * + * @author Samchon + */ +export interface IShoppingPrice { + /** + * Nominal price. + * + * This is not {@link real real price} to pay, but just a nominal price to show. + * If this value is greater than the {@link real real price}, it would be shown + * like {@link IShoppingSeller seller} is giving a discount. + */ + nominal: number & tags.Minimum<0>; + + /** + * Real price to pay. + */ + real: number & tags.Minimum<0>; +} diff --git a/test/structures/IShoppingSale.ts b/test/structures/IShoppingSale.ts new file mode 100644 index 0000000..f8dc611 --- /dev/null +++ b/test/structures/IShoppingSale.ts @@ -0,0 +1,159 @@ +import { tags } from "typia"; + +import { IShoppingSaleSnapshot } from "./IShoppingSaleSnapshot"; +import { IShoppingSection } from "./IShoppingSection"; + +/** + * Seller sales products. + * + * `IShoppingSale` is an entity that embodies "product sales" (sales) + * information registered by the {@link ISoppingSeller seller}. And the main + * information of the sale is recorded in the sub {@link IShoppingSaleSnapshot}, + * not in the main `IShoppingSale`. When a seller changes a previously registered + * item, the existing `IShoppingSale` record is not changed, but a new + * {@link IShoppingSaleSnapshot snapshot} record be created. + * + * This is to preserve the {@link IShoppingCustomer customer}'s + * {@link IShoppingOrder purchase history} flawlessly after the customer + * purchases a specific item, even if the seller changes the components or + * price of the item. It is also intended to support sellers in so-called A/B + * testing, which involves changing components or prices and measuring the + * performance in each case. + * + * @author Samchon + */ +export interface IShoppingSale + extends IShoppingSaleSnapshot, + IShoppingSale.ITimestamps { + /** + * Belonged section. + */ + section: IShoppingSection; +} +export namespace IShoppingSale { + /** + * Definitions of timepoints of sale. + */ + export interface ITimestamps { + /** + * Creation time of the record. + * + * Note that, this property is different with {@link opened_at}, + * which means the timepoint of the sale is opened. + */ + created_at: string & tags.Format<"date-time">; + + /** + * Last updated time of the record. + * + * In another words, creation time of the last snapshot. + */ + updated_at: string & tags.Format<"date-time">; + + /** + * Paused time of the sale. + * + * The sale is paused by the seller, for some reason. + * + * {@link IShoppingCustomer Customers} can still see the sale on the + * both list and detail pages, but the sale has a warning label + * "The sale is paused by the seller". + */ + paused_at: null | (string & tags.Format<"date-time">); + + /** + * Suspended time of the sale. + * + * The sale is suspended by the seller, for some reason. + * + * {@link IShoppingCustomer Customers} cannot see the sale on the + * both list and detail pages. It is almost same with soft delettion, + * but there's a little bit difference that the owner + * {@link IShoppingSeller seller} can still see the sale and resume it. + * + * Of course, the {@link IShoppingCustomer customers} who have + * already purchased the sale can still see the sale on the + * {@link IShoppingOrder order} page. + */ + suspended_at: null | (string & tags.Format<"date-time">); + + /** + * Opening time of the sale. + */ + opened_at: null | (string & tags.Format<"date-time">); + + /** + * Closing time of the sale. + * + * If this value is `null`, the sale be continued forever. + */ + closed_at: null | (string & tags.Format<"date-time">); + } + + /** + * Summarized information of sale. + * + * This summarized information being used for pagination. + */ + export interface ISummary + extends IShoppingSaleSnapshot.ISummary, + ITimestamps { + /** + * Belonged section. + */ + section: IShoppingSection; + } + + /** + * Creation information of sale. + */ + export interface ICreate extends IShoppingSaleSnapshot.ICreate { + /** + * Belonged section's {@link IShoppingSection.code}. + */ + section_code: string; + + /** + * Initial status of the sale. + * + * `null` or `undefined`: No restriction + * `paused`: Starts with {@link ITimestamps.paused_at paused} status + * `suspended`: Starts with {@link ITimestamps.suspended_at suspended} status + */ + status?: null | "paused" | "suspended"; + + /** + * Opening time of the sale. + */ + opened_at: null | string; // (string & tags.Format<"date-time">); + + /** + * Closing time of the sale. + * + * If this value is `null`, the sale be continued forever. + */ + closed_at: null | string; // (string & tags.Format<"date-time">); + } + + /** + * Update information of sale. + */ + export type IUpdate = IShoppingSaleSnapshot.ICreate; + + /** + * Update opening time information of sale. + */ + export interface IUpdateOpeningTime { + /** + * Opening time of the sale. + */ + opened_at: null | (string & tags.Format<"date-time">); + + /** + * Closing time of the sale. + * + * If this value is `null`, the sale be continued forever. + */ + closed_at: null | (string & tags.Format<"date-time">); + } +} diff --git a/test/structures/IShoppingSaleContent.ts b/test/structures/IShoppingSaleContent.ts new file mode 100644 index 0000000..ca71fae --- /dev/null +++ b/test/structures/IShoppingSaleContent.ts @@ -0,0 +1,63 @@ +import { tags } from "typia"; + +import { IAttachmentFile } from "./IAttachmentFile"; + +/** + * Content information of sale snapshot. + * + * `IShoppingSaleContent` is an entity embodies the description contents + * of {@link IShoppingSale}. + * + * @author Samchon + */ +export interface IShoppingSaleContent { + /** + * Primary Key. + */ + id: string & tags.Format<"uuid">; + + /** + * Title of the content. + */ + title: string; + + /** + * Format of the body content. + * + * Same meaning with file extension like `html`, `md`, and `txt`. + */ + format: IShoppingSaleContent.Type; + + /** + * The main body content. + */ + body: string; + + /** + * List of attached files. + */ + files: IAttachmentFile[]; + + /** + * List of thumbnails. + */ + thumbnails: IAttachmentFile[]; +} +export namespace IShoppingSaleContent { + export type Type = "html" | "md" | "txt"; + + export interface IInvert { + id: string & tags.Format<"uuid">; + title: string; + thumbnails: IAttachmentFile[]; + } + export type ISummary = IInvert; + + export interface ICreate { + title: string; + format: IShoppingSaleContent.Type; + body: string; + files: IAttachmentFile.ICreate[]; + thumbnails: IAttachmentFile.ICreate[]; + } +} diff --git a/test/structures/IShoppingSalePriceRange.ts b/test/structures/IShoppingSalePriceRange.ts new file mode 100644 index 0000000..e1a3615 --- /dev/null +++ b/test/structures/IShoppingSalePriceRange.ts @@ -0,0 +1,6 @@ +import { IShoppingPrice } from "./IShoppingPrice"; + +export interface IShoppingSalePriceRange { + lowest: IShoppingPrice; + highest: IShoppingPrice; +} diff --git a/test/structures/IShoppingSaleSnapshot.ts b/test/structures/IShoppingSaleSnapshot.ts new file mode 100644 index 0000000..3e41150 --- /dev/null +++ b/test/structures/IShoppingSaleSnapshot.ts @@ -0,0 +1,133 @@ +import { tags } from "typia"; + +import { IShoppingSale } from "./IShoppingSale"; +import { IShoppingSaleContent } from "./IShoppingSaleContent"; +import { IShoppingSalePriceRange } from "./IShoppingSalePriceRange"; +import { IShoppingSaleUnit } from "./IShoppingSaleUnit"; +import { IShoppingSection } from "./IShoppingSection"; + +// import { IShoppingBusinessAggregate } from "./aggregates/IShoppingBusinessAggregate"; + +/** + * Snapshot record of sale. + * + * `IShoppingSaleSnapshot` is an entity that embodies a snapshot of a sale, + * and the ERD (Entity Relationship Diagram) describes the role of the + * `shopping_sale_snapshots` table as follows: + * + * > {@link IShoppingSale shopping_sales} is an entity that embodies + * > "product sales" (sales) information registered by the + * > {@link IShoppingSeller seller}. And the main information of the sale is + * > recorded in the sub `shopping_sale_snapshots`, not in the main + * > {@link IShoppingSale shopping_sales}. When a seller changes a previously + * > registered item, the existing {@link IShoppingSale shopping_sales} record + * > is not changed, but a new snapshot record is created. + * > + * > This is to preserve the {@link IShoppingCustomer customer}'s + * > {@link IShoppingOrder purchase history} flawlessly after the customer + * > purchases a specific item, even if the seller changes the components or price + * > of the item. It is also intended to support sellers in so-called A/B testing, + * > which involves changing components or prices and measuring the performance + * > in each case. + * + * By the way, DTO (Data Transfer Object) level used by the front-end developer, + * it does not distinguish {@link IShoppingSale} and `IShoppingSaleSnapshot` + * strictly, and generally handles {@link IShoppingSale} and snapshot together. + * + * But even though the DTO level does not strictly distinguish them, the word and + * concept of "snapshot" is still important, so it is recommended to understand + * the concept of "snapshot" properly. + * + * @author Samchon + */ +export interface IShoppingSaleSnapshot + extends IShoppingSaleSnapshot.IBase< + IShoppingSaleContent, + IShoppingSaleUnit + > {} +export namespace IShoppingSaleSnapshot { + /** + * Invert information of the sale snapshot, in the perspective of commodity. + * + * `IShoppingSaleSnapshot.IInvert` is a structure used to represent a + * snapshot in the perspective of a {@link IShoppingCommodity}, corresponding + * to an {@link IShoppingCartCommodityStock} entity. + * + * Therefore, `IShoppingSaleSnapshot.IInvert` does not contain every + * {@link IShoppingSaleUnit units} and {@link IShoppingSaleUnitStock stocks} + * of the snapshot records, but only some of the records which are put + * into the {@link IShoppingCartCommodity shopping cart}. + */ + export interface IInvert + extends IBase, + IShoppingSale.ITimestamps { + /** + * Belonged section's information. + */ + section: IShoppingSection; + } + + /** + * Summarized information of the sale snapshot. + */ + export interface ISummary + extends IBase { + /** + * Price range of the unit. + */ + price_range: IShoppingSalePriceRange; + } + + export interface IBase { + /** + * Primary Key of Sale. + */ + id: string & tags.Format<"uuid">; + + /** + * Primary Key of Snapshot. + */ + snapshot_id: string & tags.Format<"uuid">; + + /** + * Whether the snapshot is the latest one or not. + */ + latest: boolean; + + /** + * Description and image content describing the sale. + */ + content: Content; + + /** + * List of search tags. + */ + tags: string[]; + + // /** + // * Aggregation of business performance. + // */ + // aggregate: Omit; + + /** + * List of units. + * + * Records about individual product composition informations that are sold + * in the sale. Each {@link IShoppingSaleUnit unit} record has configurable + * {@link IShoppingSaleUnitOption options}, + * {@link IShoppingSaleUnitOptionCandidate candidate} values for each + * option, and {@link IShoppingSaleUnitStock final stocks} determined by + * selecting every candidate values of each option. + */ + units: Unit[] & tags.MinItems<1>; + } + + /** + * Creation information of the snapshot. + */ + export interface ICreate { + content: IShoppingSaleContent.ICreate; + units: IShoppingSaleUnit.ICreate[] & tags.MinItems<1>; + tags: string[]; + } +} diff --git a/test/structures/IShoppingSaleUnit.ts b/test/structures/IShoppingSaleUnit.ts new file mode 100644 index 0000000..94b09f0 --- /dev/null +++ b/test/structures/IShoppingSaleUnit.ts @@ -0,0 +1,104 @@ +import { tags } from "typia"; + +import { IShoppingSalePriceRange } from "./IShoppingSalePriceRange"; +import { IShoppingSaleUnitOption } from "./IShoppingSaleUnitOption"; +import { IShoppingSaleUnitStock } from "./IShoppingSaleUnitStock"; + +/** + * Product composition information handled in the sale. + * + * `IShoppingSaleUnit` is an entity that embodies the "individual product" + * information handled in the {@link IShoppingSale sale}. + * + * For reference, the reason why `IShoppingSaleUnit` is separated from + * {@link IShoppingSaleSnapshot} by an algebraic relationship of 1: N is because + * there are some cases where multiple products are sold in one listing. This is + * the case with so-called "bundled products". + * + * - Bundle from regular product (Mackbook Set) + * - Main Body + * - Keyboard + * - Mouse + * - Apple Care (Free A/S Voucher) + * + * And again, `IShoppingSaleUnit` does not in itself refer to the + * {@link IShoppingSaleUnitStock final stock} that the + * {@link IShoppingCustomer customer} will {@link IShoppingOrder purchase}. + * The final stock can be found only after selecting all given + * {@link IShoppingSaleUnitOption options} and their + * {@link IShoppingSaleUnitOptionCandidate candidate values}. + * + * For example, even if you buy a Macbook, the final stocks are determined only + * after selecting all the options (CPU / RAM / SSD), etc. + * + * @author Samchon + */ +export interface IShoppingSaleUnit extends IShoppingSaleUnit.IBase { + /** + * List of options. + */ + options: IShoppingSaleUnitOption[]; + + /** + * List of final stocks. + */ + stocks: IShoppingSaleUnitStock[] & tags.MinItems<1>; +} +export namespace IShoppingSaleUnit { + export interface IInvert extends IBase { + /** + * List of final stocks. + */ + stocks: IShoppingSaleUnitStock.IInvert[] & tags.MinItems<1>; + } + + export interface ISummary extends IBase { + price_range: IShoppingSalePriceRange; + } + + export interface IBase { + /** + * Primary Key. + */ + id: string & tags.Format<"uuid">; + + /** + * Representative name of the unit. + */ + name: string; + + /** + * Whether the unit is primary or not. + * + * Just a labeling value. + */ + primary: boolean; + + /** + * Whether the unit is required or not. + * + * When the unit is required, the customer must select the unit. If do not + * select, customer can't buy it. + * + * For example, if there's a sale "Macbook Set" and one of the unit is the + * "Main Body", is it possible to buy the "Macbook Set" without the + * "Main Body" unit? This property is for that case. + */ + required: boolean; + } + + /** + * Creation information of sale unit. + */ + export interface ICreate extends Omit { + /** + * List of options. + */ + options: IShoppingSaleUnitOption.ICreate[]; + + /** + * List of final stocks. + */ + stocks: IShoppingSaleUnitStock.ICreate[] & tags.MinItems<1>; + } +} diff --git a/test/structures/IShoppingSaleUnitDescriptiveOption.ts b/test/structures/IShoppingSaleUnitDescriptiveOption.ts new file mode 100644 index 0000000..1c014ec --- /dev/null +++ b/test/structures/IShoppingSaleUnitDescriptiveOption.ts @@ -0,0 +1,39 @@ +import { tags } from "typia"; + +/** + * Descriptive option. + * + * When type of the option not `"select"`, it means the option is descriptive + * that requiring {@link IShoppingCustomer customers} to write some value to + * {@link IShoppingOrder purchase}. Also, whatever customer writes about the + * option, it does not affect the {@link IShoppingSaleUnitStock final stock}. + * + * Another words, the descriptive option is just for information transfer. + * + * @author Samchon + */ +export interface IShoppingSaleUnitDescriptiveOption + extends IShoppingSaleUnitDescriptiveOption.ICreate { + /** + * Primary Key. + */ + id: string & tags.Format<"uuid">; +} +export namespace IShoppingSaleUnitDescriptiveOption { + /** + * Creation information of the descriptive option. + */ + export interface ICreate { + /** + * Type of descriptive option. + * + * Which typed value should be written when purchasing. + */ + type: "boolean" | "number" | "string"; + + /** + * Readable name of the option. + */ + name: string; + } +} diff --git a/test/structures/IShoppingSaleUnitOption.ts b/test/structures/IShoppingSaleUnitOption.ts new file mode 100644 index 0000000..ae96ab2 --- /dev/null +++ b/test/structures/IShoppingSaleUnitOption.ts @@ -0,0 +1,59 @@ +import { IShoppingSaleUnitDescriptiveOption } from "./IShoppingSaleUnitDescriptiveOption"; +import { IShoppingSaleUnitSelectableOption } from "./IShoppingSaleUnitSelectableOption"; + +/** + * Individual option information on units for sale. + * + * `IShoppingSaleUnitOption` is a subsidiary entity of + * {@link IShoppingSaleUnit} that represents individual products in the + * {@link IShoppingSale sale}, and is an entity designed to represent individual + * option information for the unit. + * + * Also, `IShoppingSaleUnitOption` is an union type of two entities, + * {@link IShoppingSaleUnitSelectableOption} and + * {@link IShoppingSaleUnitDescriptiveOption}. To specify the detailed type of + * them, just use the `if` statement to the {@link type} property like below: + * + * ```typescript + * if (option.type === "select") + * option.candidates; // IShoppingSaleUnitSelectableOption + * ``` + * + * - Examples of Options + * - selectable options + * - Computer: CPU, RAM, SSD, etc. + * - Clothes: size, color, style, etc. + * - descriptive options + * - Engrave + * - Simple question + * + * If the type of option is a variable value in "select", the final stock that the + * {@link IShoppingCustomer customer} will purchase changes depending on the + * selection of the {@link IShoppingSaleUnitOptionCandidate candidate value}. + * + * Conversely, if it is a type other than "select", or if the type is "select" but + * variable is false, this is an option that has no meaning beyond simple information + * transfer. Therefore, no matter what value the customer enters and chooses when + * purchasing it, the option in this case does not affect the + * {@link IShoppingSaleUnitStock final stock}. + * + * @author Samchon + */ +export type IShoppingSaleUnitOption = + | IShoppingSaleUnitSelectableOption + | IShoppingSaleUnitDescriptiveOption; +export namespace IShoppingSaleUnitOption { + /** + * Inversely referenced information of the option. + */ + export type IInvert = + | IShoppingSaleUnitSelectableOption.IInvert + | IShoppingSaleUnitDescriptiveOption; + + /** + * Creation information of the option. + */ + export type ICreate = + | IShoppingSaleUnitSelectableOption.ICreate + | IShoppingSaleUnitDescriptiveOption.ICreate; +} diff --git a/test/structures/IShoppingSaleUnitOptionCandidate.ts b/test/structures/IShoppingSaleUnitOptionCandidate.ts new file mode 100644 index 0000000..be552ea --- /dev/null +++ b/test/structures/IShoppingSaleUnitOptionCandidate.ts @@ -0,0 +1,38 @@ +import { tags } from "typia"; + +/** + * Selectable candidate values within an option. + * + * `IShoppingSaleUnitOptionCandidate` is an entity that represents individual + * candidate values that can be selected from + * {@link IShoppingSaleUnitSelectableOption options of the "select" type}. + * + * - Example + * - RAM: 8GB, 16GB, 32GB + * - GPU: RTX 3060, RTX 4080, TESLA + * - License: Private, Commercial, Educatiion + * + * By the way, if belonged option is not "select" type, this entity never + * being used. + * + * @author Samchon + */ +export interface IShoppingSaleUnitOptionCandidate + extends IShoppingSaleUnitOptionCandidate.ICreate { + /** + * Primary Key. + */ + id: string & tags.Format<"uuid">; +} + +export namespace IShoppingSaleUnitOptionCandidate { + /** + * Creation information of the candidate value. + */ + export interface ICreate { + /** + * Represents the name of the candidate value. + */ + name: string; + } +} diff --git a/test/structures/IShoppingSaleUnitSelectableOption.ts b/test/structures/IShoppingSaleUnitSelectableOption.ts new file mode 100644 index 0000000..c126a85 --- /dev/null +++ b/test/structures/IShoppingSaleUnitSelectableOption.ts @@ -0,0 +1,93 @@ +import { tags } from "typia"; + +import { IShoppingSaleUnitOptionCandidate } from "./IShoppingSaleUnitOptionCandidate"; + +/** + * Individual option information on units for sale. + * + * `IShoppingSaleUnitSelectableOption` is a subsidiary entity of + * {@link IShoppingSaleUnit} that represents individual products in the + * {@link IShoppingSale sale}, and is an entity designed to represent individual + * selectable option information for the unit. + * + * - Examples of Options + * - selectable options + * - Computer: CPU, RAM, SSD, etc. + * - Clothes: size, color, style, etc. + * - descriptive options + * - Engrave + * - Simple question + * + * If the {@link variable} property value is `true`, the final stock that the + * {@link IShoppingCustomer customer} will purchase changes depending on the + * selection of the {@link IShoppingSaleUnitOptionCandidate candidate value}. + * + * Conversely, if it is a type other than "select", or if the {@link variable} + * property value is "false", , this is an option that has no meaning beyond + * simple information transfer. Therefore, no matter what value the customer + * chooses when purchasing it, the option in this case does not affect the + * {@link IShoppingSaleUnitStock final stock}. + * + * @author Samchon + */ +export interface IShoppingSaleUnitSelectableOption + extends IShoppingSaleUnitSelectableOption.IInvert { + /** + * List of candidate values. + */ + candidates: IShoppingSaleUnitOptionCandidate[] & tags.MinItems<1>; +} +export namespace IShoppingSaleUnitSelectableOption { + export interface IInvert { + /** + * Primary Key. + */ + id: string & tags.Format<"uuid">; + + /** + * Discriminant for the type of selectable option. + */ + type: "select"; + + /** + * Represents the name of the option. + */ + name: string; + + /** + * Whether the option is variable or not. + * + * When type of current option is "select", this attribute means whether + * selecting different candidate value affects the final stock or not. + */ + variable: boolean; + } + + /** + * Creation information of the selectable option. + */ + export interface ICreate { + /** + * Discriminant for the type of selectable option. + */ + type: "select"; + + /** + * Represents the name of the option. + */ + name: string; + + /** + * Whether the option is variable or not. + * + * When type of current option is "select", this attribute means whether + * selecting different candidate value affects the final stock or not. + */ + variable: boolean; + + /** + * List of candidate values. + */ + candidates: IShoppingSaleUnitOptionCandidate.ICreate[] & tags.MinItems<1>; + } +} diff --git a/test/structures/IShoppingSaleUnitStock.ts b/test/structures/IShoppingSaleUnitStock.ts new file mode 100644 index 0000000..289361f --- /dev/null +++ b/test/structures/IShoppingSaleUnitStock.ts @@ -0,0 +1,127 @@ +import { tags } from "typia"; + +import { IShoppingPrice } from "./IShoppingPrice"; +import { IShoppingSaleUnitStockChoice } from "./IShoppingSaleUnitStockChoice"; +import { IShoppingSaleUnitStockInventory } from "./IShoppingSaleUnitStockInventory"; + +/** + * Final component information on units for sale. + * + * `IShoppingSaleUnitStock` is a subsidiary entity of {@link IShoppingSaleUnit} + * that represents a product catalog for sale, and is a kind of final stock that is + * constructed by selecting all {@link IShoppingSaleUnitSelectableOption options} + * (variable "select" type) and their + * {@link IShoppingSaleUnitOptionCandidate candidate} values in the belonging unit. + * It is the "good" itself that customers actually purchase. + * + * - Product Name) MacBook + * - Options + * - CPU: { i3, i5, i7, i9 } + * - RAM: { 8GB, 16GB, 32GB, 64GB, 96GB } + * - SSD: { 256GB, 512GB, 1TB } + * - Number of final stocks: 4 * 5 * 3 = 60 + * + * For reference, the total number of `IShoppingSaleUnitStock` records in an + * attribution unit can be obtained using Cartesian Product. In other words, the + * value obtained by multiplying all the candidate values that each + * (variable "select" type) option can have by the number of cases is the total + * number of final stocks in the unit. + * + * Of course, without a single variable "select" type option, the final stocks + * count in the unit is only 1. + * + * @author Samchon + */ +export interface IShoppingSaleUnitStock { + /** + * Primary Key. + */ + id: string & tags.Format<"uuid">; + + /** + * Representative name of the stock. + */ + name: string; + + /** + * Price of the stock. + */ + price: IShoppingPrice; + + /** + * Current inventory status of the stock. + */ + inventory: IShoppingSaleUnitStockInventory; + + /** + * List of choices. + * + * Which candidate values being chosen for each option. + */ + choices: IShoppingSaleUnitStockChoice[]; +} +export namespace IShoppingSaleUnitStock { + /** + * Invert information from the cart. + */ + export interface IInvert { + /** + * Primary Key. + */ + id: string & tags.Format<"uuid">; + + /** + * Representative name of the stock. + */ + name: string; + + /** + * Price of the stock. + */ + price: IShoppingPrice; + + /** + * Quantity of the stock in the cart. + */ + quantity: number & tags.Type<"uint32"> & tags.Minimum<1>; + + /** + * Current inventory status of the stock. + */ + inventory: IShoppingSaleUnitStockInventory; + + /** + * List of choices. + * + * Which values being written for each option. + */ + choices: IShoppingSaleUnitStockChoice.IInvert[]; + } + + /** + * Creation information of the stock. + */ + export interface ICreate { + /** + * Representative name of the stock. + */ + name: string; + + /** + * Price of the stock. + */ + price: IShoppingPrice; + + /** + * Initial inventory quantity. + */ + quantity: number & tags.Type<"uint32"> & tags.Minimum<1>; + + /** + * List of choices. + * + * Which candidate values being chosen for each option. + */ + choices: IShoppingSaleUnitStockChoice.ICreate[]; + } +} diff --git a/test/structures/IShoppingSaleUnitStockChoice.ts b/test/structures/IShoppingSaleUnitStockChoice.ts new file mode 100644 index 0000000..d81f9fb --- /dev/null +++ b/test/structures/IShoppingSaleUnitStockChoice.ts @@ -0,0 +1,79 @@ +import { tags } from "typia"; + +import { IShoppingSaleUnitOption } from "./IShoppingSaleUnitOption"; +import { IShoppingSaleUnitOptionCandidate } from "./IShoppingSaleUnitOptionCandidate"; + +/** + * Selection information of final stock. + * + * `IShoppingSaleUnitStockChoice` is an entity that represents which + * {@link IShoppingSaleUnitSelectableOption option} of each variable "select" + * type was selected for each {@link IShoppingSaleUnitStock stock} and which + * {@link IShoppingSaleUnitOptionCandidate candidate value} was selected within + * it. + * + * Of course, if the bound {@link IShoppingSaleUnit unit} does not have any + * options, this entity can also be ignored. + * + * @author Samchon + */ +export interface IShoppingSaleUnitStockChoice { + /** + * Primary Key. + */ + id: string & tags.Format<"uuid">; + + /** + * Target option's {@link IShoppingSaleUnitOption.id} + */ + option_id: string & tags.Format<"uuid">; + + /** + * Target candidate's {@link IShoppingSaleUnitOptionCandidate.id} + */ + candidate_id: string & tags.Format<"uuid">; +} + +export namespace IShoppingSaleUnitStockChoice { + /** + * Invert information from the cart. + */ + export interface IInvert { + /** + * Primary Key. + */ + id: string & tags.Format<"uuid">; + + /** + * Target option. + */ + option: IShoppingSaleUnitOption.IInvert; + + /** + * Selected candidate value. + */ + candidate: IShoppingSaleUnitOptionCandidate | null; + + /** + * Written value. + */ + value: boolean | number | string | null; + } + + /** + * Creation information of stock choice. + */ + export interface ICreate { + /** + * Target option's index number in + * {@link IShoppingSaleUnit.ICreate.options}. + */ + option_index: number & tags.Type<"uint32">; + + /** + * Target candidate's index number in + * {@link IShoppingSaleUnitSelectableOption.ICreate.candidates}. + */ + candidate_index: number & tags.Type<"uint32">; + } +} diff --git a/test/structures/IShoppingSaleUnitStockInventory.ts b/test/structures/IShoppingSaleUnitStockInventory.ts new file mode 100644 index 0000000..411a254 --- /dev/null +++ b/test/structures/IShoppingSaleUnitStockInventory.ts @@ -0,0 +1,18 @@ +import { tags } from "typia"; + +/** + * Inventory information of a final stock. + * + * @author Samchon + */ +export interface IShoppingSaleUnitStockInventory { + /** + * Total income quantity. + */ + income: number & tags.Type<"uint32">; + + /** + * Total outcome quantity. + */ + outcome: number & tags.Type<"uint32">; +} diff --git a/test/structures/IShoppingSaleUnitStockSupplement.ts b/test/structures/IShoppingSaleUnitStockSupplement.ts new file mode 100644 index 0000000..89506c0 --- /dev/null +++ b/test/structures/IShoppingSaleUnitStockSupplement.ts @@ -0,0 +1,51 @@ +import { tags } from "typia"; + +/** + * Supplementation of inventory quantity of stock. + * + * You know what? If a {@link IShoppingSaleUnitStock stock} has been sold over + * its {@link IShoppingSaleUnitStock.ICreate.quantity initial inventory quantity}, + * the stock can't be sold anymore, because of out of stock. In that case, how the + * {@link IShoppingSeller} should do? + * + * When the sotck is sold out, seller can supplement the inventory record by + * registering this `IShoppingSaleUnitStockSupplement` record. Right, this + * `IShoppingSaleUnitStockSupplement` is an entity that embodies the + * supplementation of the inventory quantity of the belonged stock. + * + * @author Samchon + */ +export interface IShoppingSaleUnitStockSupplement { + /** + * Primary Key. + */ + id: string & tags.Format<"uuid">; + + /** + * Supplemented quantity. + */ + value: number & tags.Type<"uint32">; + + /** + * Creation time of the record. + * + * Another words, the time when inventory of the stock being supplemented. + */ + created_at: string & tags.Format<"date-time">; +} +export namespace IShoppingSaleUnitStockSupplement { + /** + * Creation information of the supplement. + */ + export interface ICreate { + /** + * Supplemented quantity. + */ + value: number & tags.Type<"uint32">; + } + + /** + * Update information of the supplement. + */ + export type IUpdate = ICreate; +} diff --git a/test/structures/IShoppingSection.ts b/test/structures/IShoppingSection.ts new file mode 100644 index 0000000..dbe6da0 --- /dev/null +++ b/test/structures/IShoppingSection.ts @@ -0,0 +1,40 @@ +import { tags } from "typia"; + +/** + * Section information. + * + * `IShoppingSection` is a concept that refers to the spatial information of + * the market. + * + * If we compare the section mentioned here to the offline market, it means a + * spatially separated area within the store, such as the "fruit corner" or + * "butcher corner". Therefore, in the {@link IShoppingSale sale} entity, it is + * not possible to classify multiple sections simultaneously, but only one section + * can be classified. + * + * By the way, if your shopping mall system requires only one section, then just + * use only one. This concept is designed to be expandable in the future. + * + * @author Samchon + */ +export interface IShoppingSection { + /** + * Primary Key. + */ + id: string & tags.Format<"uuid">; + + /** + * Identifier code. + */ + code: string; + + /** + * Representative name of the section. + */ + name: string; + + /** + * Creation time of record. + */ + created_at: string & tags.Format<"date-time">; +}