From 11c97cfb222e7d04c8733dd82b0d67278aa89051 Mon Sep 17 00:00:00 2001 From: Jeongho Nam Date: Tue, 3 Dec 2024 22:36:33 +0900 Subject: [PATCH 1/2] Revive legacy `IOpenAiSchema` type. --- package.json | 2 +- src/IHttpOpenAiApplication.ts | 5 +- src/IHttpOpenAiFunction.ts | 51 +- src/IOpenAiSchema.ts | 434 +++++++++++++++++- src/OpenAiTypeChecker.ts | 154 ++++++- .../test_http_llm_application_keyword.ts | 5 +- 6 files changed, 630 insertions(+), 21 deletions(-) diff --git a/package.json b/package.json index 6b93ae5..2df73e7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@wrtnio/schema", - "version": "2.0.2", + "version": "3.0.0", "description": "JSON and LLM function calling schemas extended for Wrtn Studio Pro", "main": "lib/index.js", "module": "./lib/index.mjs", diff --git a/src/IHttpOpenAiApplication.ts b/src/IHttpOpenAiApplication.ts index a2f670c..a7a1f92 100644 --- a/src/IHttpOpenAiApplication.ts +++ b/src/IHttpOpenAiApplication.ts @@ -19,11 +19,12 @@ import { ISwaggerOperation } from "./ISwaggerOperation"; * {@link ISwaggerSchema.IReference} type, the operation would be failed and * pushed into the {@link IHttpOpenAiApplication.errors}. Otherwise not, the operation * would be successfully converted to {@link IHttpOpenAiFunction} and its type schemas - * are downgraded to {@link OpenApiV3.IJsonSchema} and converted to {@link ILlmSchema}. + * are downgraded to {@link OpenApiV3.IJsonSchema} and converted to + * {@link IOpenAiSchema}. * * About the options, if you've configured {@link IHttpOpenAiApplication.options.keyword} * (as `true`), number of {@link IHttpOpenAiFunction.parameters} are always 1 and the first - * parameter type is always {@link ILlmSchema.IObject}. Otherwise, the parameters would + * parameter type is always {@link IOpenAiSchema.IObject}. Otherwise, the parameters would * be multiple, and the sequence of the parameters are following below rules. * * - `pathParameters`: Path parameters of {@link IHttpMigrateRoute.parameters} diff --git a/src/IHttpOpenAiFunction.ts b/src/IHttpOpenAiFunction.ts index a1b3c1c..2c862a2 100644 --- a/src/IHttpOpenAiFunction.ts +++ b/src/IHttpOpenAiFunction.ts @@ -57,7 +57,7 @@ import { ISwaggerOperation } from "./ISwaggerOperation"; * @author Samchon */ export interface IHttpOpenAiFunction - extends Omit, "parameters" | "separated"> { + extends Omit, "parameters" | "separated" | "output"> { /** * List of parameter types. * @@ -91,6 +91,13 @@ export interface IHttpOpenAiFunction */ parameters: IOpenAiSchema[]; + /** + * Expected return type. + * + * If the function returns nothing (`void`), then the output is `undefined`. + */ + output?: IOpenAiSchema | undefined; + /** * Collection of separated parameters. * @@ -99,8 +106,48 @@ export interface IHttpOpenAiFunction separated?: IHttpOpenAiFunction.ISeparated; } export namespace IHttpOpenAiFunction { - export interface IOptions extends IOpenAiSchema.IConfig { + export interface IOptions { + /** + * Separator function for the parameters. + * + * When composing parameter arguments through LLM function call, + * there can be a case that some parameters must be composed by human, + * or LLM cannot understand the parameter. + * + * For example, if the parameter type has configured + * {@link IOpenAiSchema.IString.contentMediaType} which indicates file + * uploading, it must be composed by human, not by LLM + * (Large Language Model). + * + * In that case, if you configure this property with a function that + * predicating whether the schema value must be composed by human or + * not, the parameters would be separated into two parts. + * + * - {@link IHttpOpenAiFunction.separated.llm} + * - {@link IHttpOpenAiFunction.separated.human} + * + * When writing the function, note that returning value `true` means + * to be a human composing the value, and `false` means to LLM + * composing the value. Also, when predicating the schema, it would + * better to utilize the {@link GeminiTypeChecker} like features. + * + * @param schema Schema to be separated. + * @returns Whether the schema value must be composed by human or not. + * @default null + */ separate: null | ((schema: IOpenAiSchema) => boolean); + + /** + * Whether to allow recursive types or not. + * + * If allow, then how many times to repeat the recursive types. + * + * By the way, if the model is "chatgpt", the recursive types are always + * allowed without any limitation, due to it supports the reference type. + * + * @default 3 + */ + recursive: false | number; } /** diff --git a/src/IOpenAiSchema.ts b/src/IOpenAiSchema.ts index 91379e2..81764cc 100644 --- a/src/IOpenAiSchema.ts +++ b/src/IOpenAiSchema.ts @@ -1,18 +1,430 @@ -import { ILlmSchemaV3 } from "@samchon/openapi"; - import { ISwaggerSchemaCommonPlugin } from "./ISwaggerSchemaCommonPlugin"; import { ISwaggerSchemaPaymentPlugin } from "./ISwaggerSchemaPaymentPlugin"; import { ISwaggerSchemaSecurityPlugin } from "./ISwaggerSchemaSecurityPlugin"; -export import IOpenAiSchema = ILlmSchemaV3; +/** + * Type schema info of OpenAI function call. + * + * `IOpenAiSchema` is a type schema info of OpenAI function call. + * + * `IOpenAiSchema` is basically follows the JSON schema definition of + * OpenAI v3.0: {@link OpenApiV3.IJsonSchema}. However, `IOpenAiSchema` does not + * have the reference type {@link OpenApiV3.IJsonSchema.IReference}. It's because + * the OpenAI cannot compose + * {@link OpenAiFetcher.IProps.arguments function call arguments} of + * the reference type. + * + * For reference, the OpenAPI v3.0 based JSON schema definition can't express + * the tuple array type. It has been supported since OpenAPI v3.1. Therefore, + * it would better to avoid using the tuple array type. + * + * @author Samchon + */ +export type IOpenAiSchema = + | IOpenAiSchema.IBoolean + | IOpenAiSchema.IInteger + | IOpenAiSchema.INumber + | IOpenAiSchema.IString + | IOpenAiSchema.IArray + | IOpenAiSchema.IObject + | IOpenAiSchema.IUnknown + | IOpenAiSchema.INullOnly + | IOpenAiSchema.IOneOf; +export namespace IOpenAiSchema { + /** + * Boolean type schema info. + */ + export interface IBoolean extends __ISignificant<"boolean"> { + /** + * Default value. + */ + default?: boolean | null; + + /** + * Enumeration values. + */ + enum?: Array; + } + + /** + * Integer type schema info. + */ + export interface IInteger + extends __ISignificant<"integer">, + ISwaggerSchemaPaymentPlugin.INumeric { + /** + * Default value. + * + * @type int64 + */ + default?: number | null; + + /** + * Enumeration values. + * + * @type int64 + */ + enum?: Array; + + /** + * Minimum value restriction. + * + * @type int64 + */ + minimum?: number; + + /** + * Maximum value restriction. + * + * @type int64 + */ + maximum?: number; + + /** + * Exclusive minimum value restriction. + * + * For reference, even though your Swagger document has defined the + * `exclusiveMinimum` value as `number`, it has been forcibly converted + * to `boolean` type, and assigns the numeric value to the + * {@link minimum} property in the {@link OpenApi} conversion. + */ + exclusiveMinimum?: boolean; + + /** + * Exclusive maximum value restriction. + * + * For reference, even though your Swagger document has defined the + * `exclusiveMaximum` value as `number`, it has been forcibly converted + * to `boolean` type, and assigns the numeric value to the + * {@link maximum} property in the {@link OpenApi} conversion. + */ + exclusiveMaximum?: boolean; + + /** + * Multiple of value restriction. + * + * @type uint64 + * @exclusiveMinimum 0 + */ + multipleOf?: number; + } + + /** + * Number type schema info. + */ + export interface INumber + extends __ISignificant<"number">, + ISwaggerSchemaPaymentPlugin.INumeric { + /** + * Default value. + */ + default?: number | null; + + /** + * Enumeration values. + */ + enum?: Array; + + /** + * Minimum value restriction. + */ + minimum?: number; + + /** + * Maximum value restriction. + */ + maximum?: number; + + /** + * Exclusive minimum value restriction. + * + * For reference, even though your Swagger (or OpenAPI) document has + * defined the `exclusiveMinimum` value as `number`, {@link OpenAiComposer} + * forcibly converts it to `boolean` type, and assign the numeric value to + * the {@link minimum} property. + */ + exclusiveMinimum?: boolean; + + /** + * Exclusive maximum value restriction. + * + * For reference, even though your Swagger (or OpenAPI) document has + * defined the `exclusiveMaximum` value as `number`, {@link OpenAiComposer} + * forcibly converts it to `boolean` type, and assign the numeric value to + * the {@link maximum} property. + */ + exclusiveMaximum?: boolean; + + /** + * Multiple of value restriction. + * + * @exclusiveMinimum 0 + */ + multipleOf?: number; + } + + /** + * String type schema info. + */ + export interface IString + extends __ISignificant<"string">, + ISwaggerSchemaPaymentPlugin.IString, + ISwaggerSchemaSecurityPlugin { + /** + * Default value. + */ + default?: string | null; + + /** + * Enumeration values. + */ + enum?: Array; + + /** + * Format restriction. + */ + format?: + | "binary" + | "byte" + | "password" + | "regex" + | "uuid" + | "email" + | "hostname" + | "idn-email" + | "idn-hostname" + | "iri" + | "iri-reference" + | "ipv4" + | "ipv6" + | "uri" + | "uri-reference" + | "uri-template" + | "url" + | "date-time" + | "date" + | "time" + | "duration" + | "json-pointer" + | "relative-json-pointer" + | (string & {}); + + /** + * Pattern restriction. + */ + pattern?: string; + + /** + * Minimum length restriction. + * + * @type uint64 + */ + minLength?: number; + + /** + * Maximum length restriction. + * + * @type uint64 + */ + maxLength?: number; + + /** + * Content media type restriction. + */ + contentMediaType?: string; + } + + /** + * Array type schema info. + */ + export interface IArray extends __ISignificant<"array"> { + /** + * Items type schema info. + * + * The `items` means the type of the array elements. In other words, it is + * the type schema info of the `T` in the TypeScript array type `Array`. + */ + items: IOpenAiSchema; + + /** + * Unique items restriction. + * + * If this property value is `true`, target array must have unique items. + */ + uniqueItems?: boolean; + + /** + * Minimum items restriction. + * + * Restriction of minumum number of items in the array. + * + * @type uint64 + */ + minItems?: number; + + /** + * Maximum items restriction. + * + * Restriction of maximum number of items in the array. + * + * @type uint64 + */ + maxItems?: number; + } + + /** + * Object type schema info. + */ + export interface IObject extends __ISignificant<"object"> { + /** + * Properties of the object. + * + * The `properties` means a list of key-value pairs of the object's + * regular properties. The key is the name of the regular property, + * and the value is the type schema info. + * + * If you need additional properties that is represented by dynamic key, + * you can use the {@link additionalProperties} instead. + */ + properties?: Record; + + /** + * List of key values of the required properties. + * + * The `required` means a list of the key values of the required + * {@link properties}. If some property key is not listed in the `required` + * list, it means that property is optional. Otherwise some property key + * exists in the `required` list, it means that the property must be filled. + * + * Below is an example of the {@link properties} and `required`. + * + * ```typescript + * interface SomeObject { + * id: string; + * email: string; + * name?: string; + * } + * ``` + * + * As you can see, `id` and `email` {@link properties} are {@link required}, + * so that they are listed in the `required` list. + * + * ```json + * { + * "type": "object", + * "properties": { + * "id": { "type": "string" }, + * "email": { "type": "string" }, + * "name": { "type": "string" } + * }, + * "required": ["id", "email"] + * } + * ``` + */ + required?: string[]; + + /** + * Additional properties' info. + * + * The `additionalProperties` means the type schema info of the additional + * properties that are not listed in the {@link properties}. + * + * If the value is `true`, it means that the additional properties are not + * restricted. They can be any type. Otherwise, if the value is + * {@link IOpenAiSchema} type, it means that the additional properties must + * follow the type schema info. + * + * - `true`: `Record` + * - `IOpenAiSchema`: `Record` + */ + additionalProperties?: boolean | IOpenAiSchema; + } + + /** + * Unknown type schema info. + * + * It means the type of the value is `any`. + */ + export interface IUnknown extends __IAttribute { + /** + * Type is never be defined. + */ + type?: undefined; + } + + /** + * Null only type schema info. + */ + export interface INullOnly extends __IAttribute { + /** + * Type is always `null`. + */ + type: "null"; + + /** + * Default value. + */ + default?: null; + } + + /** + * One of type schema info. + * + * `IOneOf` represents an union type of the TypeScript (`A | B | C`). + * + * For reference, even though your Swagger (or OpenAPI) document has + * defined `anyOf` instead of the `oneOf`, it has been forcibly converted + * to `oneOf` type by {@link OpenApi.convert OpenAPI conversion}. + */ + export interface IOneOf extends __IAttribute { + /** + * List of the union types. + */ + oneOf: Exclude[]; + } + + /** + * Significant attributes that can be applied to the most types. + */ + export interface __ISignificant extends __IAttribute { + /** + * Discriminator value of the type. + */ + type: Type; + + /** + * Whether to allow `null` value or not. + */ + nullable?: boolean; + } + + /** + * Common attributes that can be applied to all types. + */ + export interface __IAttribute extends ISwaggerSchemaCommonPlugin { + /** + * Title of the schema. + */ + title?: string; + + /** + * Detailed description of the schema. + */ + description?: string; + + /** + * Whether the type is deprecated or not. + */ + deprecated?: boolean; + + /** + * Example value. + */ + example?: any; -declare module "@samchon/openapi" { - export namespace ILlmSchemaV3 { - export interface IInteger extends ISwaggerSchemaPaymentPlugin.INumeric {} - export interface INumber extends ISwaggerSchemaPaymentPlugin.INumeric {} - export interface IString - extends ISwaggerSchemaPaymentPlugin.IString, - ISwaggerSchemaSecurityPlugin {} - export interface __IAttribute extends ISwaggerSchemaCommonPlugin {} + /** + * List of example values as key-value pairs. + */ + examples?: Record; } } diff --git a/src/OpenAiTypeChecker.ts b/src/OpenAiTypeChecker.ts index 3b07543..a159be2 100644 --- a/src/OpenAiTypeChecker.ts +++ b/src/OpenAiTypeChecker.ts @@ -1,5 +1,153 @@ -import { LlmTypeCheckerV3 } from "@samchon/openapi"; +import { IOpenAiSchema } from "./IOpenAiSchema"; -import "./IOpenAiSchema"; +/** + * Type checker for OpenAI function call schema. + * + * `OpenAiTypeChecker` is a type checker of {@link IOpenAiSchema}. + * + * @author Samchon + */ +export namespace OpenAiTypeChecker { + /** + * Visit every nested schemas. + * + * Visit every nested schemas of the target, and apply the callback function + * to them. + * + * If the visitor meets an union type, it will visit every individual schemas + * in the union type. Otherwise meets an object type, it will visit every + * properties and additional properties. If the visitor meets an array type, + * it will visit the item type. + * + * @param schema Target schema to visit + * @param callback Callback function to apply + */ + export const visit = ( + schema: IOpenAiSchema, + callback: (schema: IOpenAiSchema) => void, + ): void => { + callback(schema); + if (isOneOf(schema)) schema.oneOf.forEach((s) => visit(s, callback)); + else if (isObject(schema)) { + for (const [_, s] of Object.entries(schema.properties ?? {})) + visit(s, callback); + if ( + typeof schema.additionalProperties === "object" && + schema.additionalProperties !== null + ) + visit(schema.additionalProperties, callback); + } else if (isArray(schema)) visit(schema.items, callback); + }; -export import OpenAiTypeChecker = LlmTypeCheckerV3; + /** + * Test whether the schema is an union type. + * + * @param schema Target schema + * @returns Whether union type or not + */ + export const isOneOf = ( + schema: IOpenAiSchema, + ): schema is IOpenAiSchema.IOneOf => + (schema as IOpenAiSchema.IOneOf).oneOf !== undefined; + + /** + * Test whether the schema is an object type. + * + * @param schema Target schema + * @returns Whether object type or not + */ + export const isObject = ( + schema: IOpenAiSchema, + ): schema is IOpenAiSchema.IObject => + (schema as IOpenAiSchema.IObject).type === "object"; + + /** + * Test whether the schema is an array type. + * + * @param schema Target schema + * @returns Whether array type or not + */ + export const isArray = ( + schema: IOpenAiSchema, + ): schema is IOpenAiSchema.IArray => + (schema as IOpenAiSchema.IArray).type === "array"; + + /** + * Test whether the schema is a boolean type. + * + * @param schema Target schema + * @returns Whether boolean type or not + */ + export const isBoolean = ( + schema: IOpenAiSchema, + ): schema is IOpenAiSchema.IBoolean => + (schema as IOpenAiSchema.IBoolean).type === "boolean"; + + /** + * Test whether the schema is an integer type. + * + * @param schema Target schema + * @returns Whether integer type or not + */ + export const isInteger = ( + schema: IOpenAiSchema, + ): schema is IOpenAiSchema.IInteger => + (schema as IOpenAiSchema.IInteger).type === "integer"; + + /** + * Test whether the schema is a number type. + * + * @param schema Target schema + * @returns Whether number type or not + */ + export const isNumber = ( + schema: IOpenAiSchema, + ): schema is IOpenAiSchema.INumber => + (schema as IOpenAiSchema.INumber).type === "number"; + + /** + * Test whether the schema is a string type. + * + * @param schema Target schema + * @returns Whether string type or not + */ + export const isString = ( + schema: IOpenAiSchema, + ): schema is IOpenAiSchema.IString => + (schema as IOpenAiSchema.IString).type === "string"; + + /** + * Test whether the schema is a null type. + * + * @param schema Target schema + * @returns Whether null type or not + */ + export const isNullOnly = ( + schema: IOpenAiSchema, + ): schema is IOpenAiSchema.INullOnly => + (schema as IOpenAiSchema.INullOnly).type === "null"; + + /** + * Test whether the schema is a nullable type. + * + * @param schema Target schema + * @returns Whether nullable type or not + */ + export const isNullable = (schema: IOpenAiSchema): boolean => + !isUnknown(schema) && + (isNullOnly(schema) || + (isOneOf(schema) + ? schema.oneOf.some(isNullable) + : schema.nullable === true)); + + /** + * Test whether the schema is an unknown type. + * + * @param schema Target schema + * @returns Whether unknown type or not + */ + export const isUnknown = ( + schema: IOpenAiSchema, + ): schema is IOpenAiSchema.IUnknown => + !isOneOf(schema) && (schema as IOpenAiSchema.IUnknown).type === undefined; +} diff --git a/test/features/test_http_llm_application_keyword.ts b/test/features/test_http_llm_application_keyword.ts index 5185aa7..d203b69 100644 --- a/test/features/test_http_llm_application_keyword.ts +++ b/test/features/test_http_llm_application_keyword.ts @@ -1,8 +1,9 @@ import { TestValidator } from "@nestia/e2e"; -import { IHttpMigrateRoute, ILlmSchema, OpenApi } from "@samchon/openapi"; +import { IHttpMigrateRoute, OpenApi } from "@samchon/openapi"; import { HttpOpenAi, IHttpOpenAiApplication, + IOpenAiSchema, OpenAiTypeChecker, } from "@wrtnio/schema"; @@ -25,7 +26,7 @@ export const test_http_llm_application_keyword = (): void => { ...(route.body ? ["body"] : []), ])( (() => { - const schema: ILlmSchema = func.parameters[0]; + const schema: IOpenAiSchema = func.parameters[0]; if (!OpenAiTypeChecker.isObject(schema)) return []; return Object.keys(schema.properties ?? {}); })(), From 56bad25f3ff488acc97f20d084f91e8ea441eb18 Mon Sep 17 00:00:00 2001 From: Jeongho Nam Date: Wed, 4 Dec 2024 13:26:08 +0900 Subject: [PATCH 2/2] Fix test script bug --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 2df73e7..b2c221c 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "typings": "./lib/index.d.ts", "scripts": { "prepare": "ts-patch install", - "build": "npm run build:main", + "build": "npm run build:main && npm run build:test", "build:main": "rimraf lib && tsc && rollup -c", "build:test": "rimraf bin && tsc -p test/tsconfig.json", "dev": "npm run build:test -- --watch",