From 049b522aa709bb3d6d5aac0dd54a132d1136022c Mon Sep 17 00:00:00 2001 From: Jeongho Nam Date: Tue, 29 Oct 2024 23:19:20 +0900 Subject: [PATCH] Wiriting Gemini schema and its type checker --- README.md | 10 +- src/index.ts | 43 +-- src/structures/IGeminiSchema.ts | 464 ++++++++++++++++++++++++++++++++ src/utils/GeminiTypeChecker.ts | 251 +++++++++++++++++ src/utils/LlmTypeChecker.ts | 6 + 5 files changed, 755 insertions(+), 19 deletions(-) create mode 100644 src/structures/IGeminiSchema.ts create mode 100644 src/utils/GeminiTypeChecker.ts diff --git a/README.md b/README.md index c2111ed..662d771 100644 --- a/README.md +++ b/README.md @@ -29,10 +29,12 @@ OpenAPI definitions, converters and LLM function calling application composer. `@samchon/openapi` also provides LLM (Large Language Model) function calling application composer from the OpenAPI document with many strategies. With the [`HttpLlm`](https://github.com/samchon/openapi/blob/master/src/HttpLlm.ts) module, you can perform the LLM funtion calling extremely easily just by delivering the OpenAPI (Swagger) document. - [`HttpLlm.application()`](https://github.com/samchon/openapi/blob/master/src/HttpLlm.ts) - - [`IHttpLlmApplication`](https://github.com/samchon/openapi/blob/master/src/structures/ILlmApplication.ts) - - [`IHttpLlmFunction`](https://github.com/samchon/openapi/blob/master/src/structures/ILlmFunction.ts) - - [`ILlmSchema`](https://github.com/samchon/openapi/blob/master/src/structures/ILlmSchema.ts) - - [`LlmTypeChecker`](https://github.com/samchon/openapi/blob/master/src/utils/LlmTypeChecker.ts) + - [`IHttpLlmApplication`](https://github.com/samchon/openapi/blob/master/src/structures/ILlmApplication.ts) + - [`IHttpLlmFunction`](https://github.com/samchon/openapi/blob/master/src/structures/ILlmFunction.ts) + - Supported schemes + - [`ILlmSchema`](https://github.com/samchon/openapi/blob/master/src/structures/ILlmSchema.ts) + - [`IGeminiSchema`](https://github.com/samchon/openapi/blob/master/src/structures/IGeminiSchema.ts) + - [`IOpenAiSchema`](https://github.com/samchon/openapi/blob/master/src/structures/IOpenAiSchema.ts) > [!TIP] > diff --git a/src/index.ts b/src/index.ts index a2d2ec3..541da70 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,22 +1,35 @@ -// STRUCTURES +//---- +// OPENAPI +//---- +export * from "./OpenApi"; +export * from "./SwaggerV2"; +export * from "./OpenApiV3"; +export * from "./OpenApiV3_1"; + +export * from "./utils/OpenApiTypeChecker"; + +//---- +// MIGRATION +//---- +export * from "./structures/IHttpMigrateApplication"; +export * from "./structures/IHttpMigrateRoute"; + export * from "./structures/IHttpConnection"; +export * from "./structures/IHttpResponse"; +export * from "./http/HttpError"; + +export * from "./HttpMigration"; + +//---- +// LLM +//---- export * from "./structures/IHttpLlmApplication"; export * from "./structures/IHttpLlmFunction"; -export * from "./structures/IHttpMigrateRoute"; -export * from "./structures/IHttpMigrateApplication"; -export * from "./structures/IHttpResponse"; export * from "./structures/ILlmApplication"; -export * from "./structures/ILlmSchema"; -// UTILS -export * from "./http/HttpError"; -export * from "./utils/OpenApiTypeChecker"; -export * from "./utils/LlmTypeChecker"; +export * from "./structures/ILlmSchema"; +export * from "./structures/IGeminiSchema"; -// OPENAPI MODULES -export * from "./OpenApi"; -export * from "./SwaggerV2"; -export * from "./OpenApiV3"; -export * from "./OpenApiV3_1"; export * from "./HttpLlm"; -export * from "./HttpMigration"; +export * from "./utils/LlmTypeChecker"; +export * from "./utils/GeminiTypeChecker"; diff --git a/src/structures/IGeminiSchema.ts b/src/structures/IGeminiSchema.ts new file mode 100644 index 0000000..c7036ca --- /dev/null +++ b/src/structures/IGeminiSchema.ts @@ -0,0 +1,464 @@ +/** + * Type schema info of the Gemini. + * + * `IGeminiSchema` iis a type metadata of LLM (Large Language Model) + * function calling in the Geminimi. + * + * `IGeminiSchema` basically follows the JSON schema definition of the + * OpenAPI v3.0 specification; {@link OpenApiV3.IJsonSchema}. However, + * `IGeminiSchema` cannot understand union and reference types, referenced + * by the `oneOf` and `$ref` properties. Also, as OpenAPI v3.0 specification + * does not support the tuple type, `IGeminiSchema` does not support the + * tuple type either. + * + * - Does not support + * - {@link OpenApi.IJsonSchema.IReference} + * - {@link OpenApi.IJsonSchema.IOneOf} + * - {@link OpenApi.IJsonSchema.ITuple} + * + * Also, by the documents of Gemini, these additional properties are not + * supported, either. However, I can't sure that these additional properties + * are really not supported in the Geimni, because the Gemini seems like + * understanding them. Therefore, I've decided to keep them alive. + * + * - ex) additional properties + * - {@link IGeminiSchema.IString.default} + * - {@link IGeminiSchema.__IAttribute.example} + * - {@link IGeminiSchema.__IAttribute.examples} + * - {@link IGeminiSchema.INumber.maximum} + * - {@link IGeminiSchema.IObject.additionalProperties} + * + * @reference https://cloud.google.com/vertex-ai/generative-ai/docs/model-reference/function-calling + * @reference https://cloud.google.com/vertex-ai/generative-ai/docs/multimodal/function-calling + * @author Jeongho Nam - https://github.com/samchon + */ +export type IGeminiSchema = + | IGeminiSchema.IBoolean + | IGeminiSchema.IInteger + | IGeminiSchema.INumber + | IGeminiSchema.IString + | IGeminiSchema.IArray + | IGeminiSchema.IObject + | IGeminiSchema.IUnknown + | IGeminiSchema.INullOnly; +export namespace IGeminiSchema { + /** + * Boolean type schema info. + */ + export interface IBoolean extends __ISignificant<"boolean"> { + /** + * Enumeration values. + */ + enum?: Array; + + /** + * Default value. + * + * @warning document of Gemini says not supported, but cannot sure + */ + default?: boolean | null; + } + + /** + * Integer type schema info. + */ + export interface IInteger extends __ISignificant<"integer"> { + /** + * Enumeration values. + * + * @type int64 + */ + enum?: Array; + + /** + * Default value. + * + * @type int64 + * @warning document of Gemini says not supported, but cannot sure + */ + default?: number | null; + + /** + * Minimum value restriction. + * + * @type int64 + * @warning document of Gemini says not supported, but cannot sure + */ + minimum?: number; + + /** + * Maximum value restriction. + * + * @type int64 + * @warning document of Gemini says not supported, but cannot sure + */ + 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. + * + * @warning document of Gemini says not supported, but cannot sure + */ + 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. + * + * @warning document of Gemini says not supported, but cannot sure + */ + exclusiveMaximum?: boolean; + + /** + * Multiple of value restriction. + * + * @type uint64 + * @exclusiveMinimum 0 + * @warning document of Gemini says not supported, but cannot sure + */ + multipleOf?: number; + } + + /** + * Number type schema info. + */ + export interface INumber extends __ISignificant<"number"> { + /** + * Enumeration values. + */ + enum?: Array; + + /** + * Default value. + * + * @warning document of Gemini says not supported, but cannot sure + */ + default?: number | null; + + /** + * Minimum value restriction. + * + * @warning document of Gemini says not supported, but cannot sure + */ + minimum?: number; + + /** + * Maximum value restriction. + * + * @warning document of Gemini says not supported, but cannot sure + */ + 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. + * + * @warning document of Gemini says not supported, but cannot sure + */ + 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. + * + * @warning document of Gemini says not supported, but cannot sure + */ + exclusiveMaximum?: boolean; + + /** + * Multiple of value restriction. + * + * @exclusiveMinimum 0 + * @warning document of Gemini says not supported, but cannot sure + */ + multipleOf?: number; + } + + /** + * String type schema info. + */ + export interface IString extends __ISignificant<"string"> { + /** + * Enumeration values. + */ + enum?: Array; + + /** + * Default value. + * + * @warning document of Gemini says not supported, but cannot sure + */ + default?: string | null; + + /** + * Format restriction. + * + * @warning document of Gemini says not supported, but cannot sure + */ + 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. + * + * @warning document of Gemini says not supported, but cannot sure + */ + pattern?: string; + + /** + * Minimum length restriction. + * + * @type uint64 + * @warning document of Gemini says not supported, but cannot sure + */ + minLength?: number; + + /** + * Maximum length restriction. + * + * @type uint64 + * @warning document of Gemini says not supported, but cannot sure + */ + maxLength?: number; + + /** + * Content media type restriction. + * + * @warning document of Gemini says not supported, but cannot sure + */ + 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: IGeminiSchema; + + /** + * Unique items restriction. + * + * If this property value is `true`, target array must have unique items. + * + * @warning document of Gemini says not supported, but cannot sure + */ + uniqueItems?: boolean; + + /** + * Minimum items restriction. + * + * Restriction of minumum number of items in the array. + * + * @type uint64 + * @warning document of Gemini says not supported, but cannot sure + */ + minItems?: number; + + /** + * Maximum items restriction. + * + * Restriction of maximum number of items in the array. + * + * @type uint64 + * @warning document of Gemini says not supported, but cannot sure + */ + 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 IGeminiSchema} type, it means that the additional properties + * must follow the type schema info. + * + * - `false`: only regular properties + * - `true`: `Record` + * - `IGeminiSchema`: `Record` + * + * @warning document of Gemini says not supported, but cannot sure + */ + additionalProperties?: boolean | IGeminiSchema; + } + + /** + * 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. + * + * @warning document of Gemini says not supported, but cannot sure + */ + default?: null; + } + + /** + * Significant attributes that can be applied to the most types. + */ + export interface __ISignificant extends __IAttribute { + /** + * Discriminator value of the type. + */ + type: Type; + + /** + * Whether to allow `null` value or not. + */ + nullable?: boolean; + } + + /** + * Common attributes that can be applied to all types. + */ + export interface __IAttribute { + /** + * Title of the schema. + */ + title?: string; + + /** + * Detailed description of the schema. + */ + description?: string; + + /** + * Whether the type is deprecated or not. + * + * @warning document of Gemini says not supported, but cannot sure + */ + deprecated?: boolean; + + /** + * Example value. + * + * @warning document of Gemini says not supported, but cannot sure + */ + example?: any; + + /** + * List of example values as key-value pairs. + * + * @warning document of Gemini says not supported, but cannot sure + */ + examples?: Record; + } +} diff --git a/src/utils/GeminiTypeChecker.ts b/src/utils/GeminiTypeChecker.ts new file mode 100644 index 0000000..ece2b45 --- /dev/null +++ b/src/utils/GeminiTypeChecker.ts @@ -0,0 +1,251 @@ +import { IGeminiSchema } from "../structures/IGeminiSchema"; + +/** + * Type checker for Gemini type schema. + * + * `GeminiTypeChecker` is a type checker of {@link IGeminiSchema}. + * + * @author Samchon + */ +export namespace GeminiTypeChecker { + /* ----------------------------------------------------------- + OPERATORS + ----------------------------------------------------------- */ + export const visit = ( + schema: IGeminiSchema, + closure: (schema: IGeminiSchema) => void, + ): void => { + closure(schema); + if (isObject(schema)) { + Object.values(schema.properties ?? {}).forEach((child) => + visit(child, closure), + ); + if ( + typeof schema.additionalProperties === "object" && + schema.additionalProperties !== null + ) + visit(schema.additionalProperties, closure); + } else if (isArray(schema)) visit(schema.items, closure); + }; + + export const covers = (x: IGeminiSchema, y: IGeminiSchema): boolean => { + // CHECK EQUALITY + if (x === y) return true; + else if (isUnknown(x)) return true; + else if (isUnknown(y)) return false; + else if (isNullOnly(x)) return isNullOnly(y); + else if (isNullOnly(y)) return x.nullable === true; + else if (x.nullable === true && !!y.nullable === false) return false; + // ATOMIC CASE + else if (isBoolean(x)) return isBoolean(y) && coverBoolean(x, y); + else if (isInteger(x)) return isInteger(y) && coverInteger(x, y); + else if (isNumber(x)) + return (isNumber(y) || isInteger(y)) && coverNumber(x, y); + else if (isString(x)) return isString(y) && covertString(x, y); + // INSTANCE CASE + else if (isArray(x)) return isArray(y) && coverArray(x, y); + else if (isObject(x)) return isObject(y) && coverObject(x, y); + return false; + }; + + /** + * @internal + */ + const coverBoolean = ( + x: IGeminiSchema.IBoolean, + y: IGeminiSchema.IBoolean, + ): boolean => + x.enum === undefined || + (y.enum !== undefined && x.enum.every((v) => y.enum!.includes(v))); + + /** + * @internal + */ + const coverInteger = ( + x: IGeminiSchema.IInteger, + y: IGeminiSchema.IInteger, + ): boolean => { + if (x.enum !== undefined) + return y.enum !== undefined && x.enum.every((v) => y.enum!.includes(v)); + return [ + x.type === y.type, + x.minimum === undefined || + (y.minimum !== undefined && x.minimum <= y.minimum), + x.maximum === undefined || + (y.maximum !== undefined && x.maximum >= y.maximum), + x.exclusiveMinimum !== true || + x.minimum === undefined || + (y.minimum !== undefined && + (y.exclusiveMinimum === true || x.minimum < y.minimum)), + x.exclusiveMaximum !== true || + x.maximum === undefined || + (y.maximum !== undefined && + (y.exclusiveMaximum === true || x.maximum > y.maximum)), + x.multipleOf === undefined || + (y.multipleOf !== undefined && + y.multipleOf / x.multipleOf === + Math.floor(y.multipleOf / x.multipleOf)), + ].every((v) => v); + }; + + /** + * @internal + */ + const coverNumber = ( + x: IGeminiSchema.INumber, + y: IGeminiSchema.INumber | IGeminiSchema.IInteger, + ): boolean => { + if (x.enum !== undefined) + return y.enum !== undefined && x.enum.every((v) => y.enum!.includes(v)); + return [ + x.type === y.type, + x.minimum === undefined || + (y.minimum !== undefined && x.minimum <= y.minimum), + x.maximum === undefined || + (y.maximum !== undefined && x.maximum >= y.maximum), + x.exclusiveMinimum !== true || + x.minimum === undefined || + (y.minimum !== undefined && + (y.exclusiveMinimum === true || x.minimum < y.minimum)), + x.exclusiveMaximum !== true || + x.maximum === undefined || + (y.maximum !== undefined && + (y.exclusiveMaximum === true || x.maximum > y.maximum)), + x.multipleOf === undefined || + (y.multipleOf !== undefined && + y.multipleOf / x.multipleOf === + Math.floor(y.multipleOf / x.multipleOf)), + ].every((v) => v); + }; + + /** + * @internal + */ + const covertString = ( + x: IGeminiSchema.IString, + y: IGeminiSchema.IString, + ): boolean => { + if (x.enum !== undefined) + return y.enum !== undefined && x.enum.every((v) => y.enum!.includes(v)); + return [ + x.format === undefined || + (y.format !== undefined && coverFormat(x.format, y.format)), + x.pattern === undefined || x.pattern === y.pattern, + x.minLength === undefined || + (y.minLength !== undefined && x.minLength <= y.minLength), + x.maxLength === undefined || + (y.maxLength !== undefined && x.maxLength >= y.maxLength), + ].every((v) => v); + }; + + /** + * @internal + */ + const coverFormat = ( + x: Required["format"], + y: Required["format"], + ): boolean => + x === y || + (x === "idn-email" && y === "email") || + (x === "idn-hostname" && y === "hostname") || + (["uri", "iri"].includes(x) && y === "url") || + (x === "iri" && y === "uri") || + (x === "iri-reference" && y === "uri-reference"); + + /** + * @internal + */ + const coverArray = ( + x: IGeminiSchema.IArray, + y: IGeminiSchema.IArray, + ): boolean => { + if ( + !( + x.minItems === undefined || + (y.minItems !== undefined && x.minItems <= y.minItems) + ) + ) + return false; + else if ( + !( + x.maxItems === undefined || + (y.maxItems !== undefined && x.maxItems >= y.maxItems) + ) + ) + return false; + return covers(x.items, y.items); + }; + + /** + * @internal + */ + const coverObject = ( + x: IGeminiSchema.IObject, + y: IGeminiSchema.IObject, + ): boolean => { + if (!x.additionalProperties && !!y.additionalProperties) return false; + else if ( + !!x.additionalProperties && + !!y.additionalProperties && + ((typeof x.additionalProperties === "object" && + y.additionalProperties === true) || + (typeof x.additionalProperties === "object" && + typeof y.additionalProperties === "object" && + !covers(x.additionalProperties, y.additionalProperties))) + ) + return false; + return Object.entries(y.properties ?? {}).every(([key, b]) => { + const a: IGeminiSchema | undefined = x.properties?.[key]; + if (a === undefined) return false; + else if ( + (x.required?.includes(key) ?? false) === true && + (y.required?.includes(key) ?? false) === false + ) + return false; + return covers(a, b); + }); + }; + + /* ----------------------------------------------------------- + TYPE CHECKERS + ----------------------------------------------------------- */ + export const isBoolean = ( + schema: IGeminiSchema, + ): schema is IGeminiSchema.IBoolean => + (schema as IGeminiSchema.IBoolean).type === "boolean"; + + export const isInteger = ( + schema: IGeminiSchema, + ): schema is IGeminiSchema.IInteger => + (schema as IGeminiSchema.IInteger).type === "integer"; + + export const isNumber = ( + schema: IGeminiSchema, + ): schema is IGeminiSchema.INumber => + (schema as IGeminiSchema.INumber).type === "number"; + + export const isString = ( + schema: IGeminiSchema, + ): schema is IGeminiSchema.IString => + (schema as IGeminiSchema.IString).type === "string"; + + export const isArray = ( + schema: IGeminiSchema, + ): schema is IGeminiSchema.IArray => + (schema as IGeminiSchema.IArray).type === "array"; + + export const isObject = ( + schema: IGeminiSchema, + ): schema is IGeminiSchema.IObject => + (schema as IGeminiSchema.IObject).type === "object"; + + export const isNullOnly = ( + schema: IGeminiSchema, + ): schema is IGeminiSchema.INullOnly => + (schema as IGeminiSchema.INullOnly).type === "null"; + + export const isUnknown = ( + schema: IGeminiSchema, + ): schema is IGeminiSchema.IUnknown => + (schema as IGeminiSchema.IUnknown).type === undefined; +} diff --git a/src/utils/LlmTypeChecker.ts b/src/utils/LlmTypeChecker.ts index cc35366..1e51a5b 100644 --- a/src/utils/LlmTypeChecker.ts +++ b/src/utils/LlmTypeChecker.ts @@ -8,6 +8,9 @@ import { ILlmSchema } from "../structures/ILlmSchema"; * @author Samchon */ export namespace LlmTypeChecker { + /* ----------------------------------------------------------- + OPERATORS + ----------------------------------------------------------- */ /** * Visit every nested schemas. * @@ -39,6 +42,9 @@ export namespace LlmTypeChecker { } else if (isArray(schema)) visit(schema.items, callback); }; + /* ----------------------------------------------------------- + TYPE CHECKERS + ----------------------------------------------------------- */ /** * Test whether the schema is an union type. *