Skip to content

Commit 955f41a

Browse files
authored
Merge pull request #113 from samchon/feat/chatgpt
Enhance `ChatGptTypeChecker` and `LlmTypeCheckerV3`.
2 parents 6f0f412 + 4c37a42 commit 955f41a

5 files changed

+469
-5
lines changed

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@samchon/openapi",
3-
"version": "2.2.0",
3+
"version": "2.2.1",
44
"description": "OpenAPI definitions and converters for 'typia' and 'nestia'.",
55
"main": "./lib/index.js",
66
"module": "./lib/index.mjs",

src/utils/ChatGptTypeChecker.ts

+26-4
Original file line numberDiff line numberDiff line change
@@ -153,10 +153,15 @@ export namespace ChatGptTypeChecker {
153153
if (found !== undefined) next(found, `${refAccessor}[${key}]`);
154154
} else if (ChatGptTypeChecker.isAnyOf(schema))
155155
schema.anyOf.forEach((s, i) => next(s, `${accessor}.anyOf[${i}]`));
156-
else if (ChatGptTypeChecker.isObject(schema))
156+
else if (ChatGptTypeChecker.isObject(schema)) {
157157
for (const [key, value] of Object.entries(schema.properties))
158158
next(value, `${accessor}.properties[${JSON.stringify(key)}]`);
159-
else if (ChatGptTypeChecker.isArray(schema))
159+
if (
160+
typeof schema.additionalProperties === "object" &&
161+
schema.additionalProperties !== null
162+
)
163+
next(schema.additionalProperties, `${accessor}.additionalProperties`);
164+
} else if (ChatGptTypeChecker.isArray(schema))
160165
next(schema.items, `${accessor}.items`);
161166
};
162167
next(props.schema, props.accessor ?? "$input.schemas");
@@ -288,8 +293,24 @@ export namespace ChatGptTypeChecker {
288293
visited: Map<IChatGptSchema, Map<IChatGptSchema, boolean>>;
289294
x: IChatGptSchema.IObject;
290295
y: IChatGptSchema.IObject;
291-
}): boolean =>
292-
Object.entries(p.y.properties ?? {}).every(([key, b]) => {
296+
}): boolean => {
297+
if (!p.x.additionalProperties && !!p.y.additionalProperties) return false;
298+
else if (
299+
!!p.x.additionalProperties &&
300+
!!p.y.additionalProperties &&
301+
((typeof p.x.additionalProperties === "object" &&
302+
p.y.additionalProperties === true) ||
303+
(typeof p.x.additionalProperties === "object" &&
304+
typeof p.y.additionalProperties === "object" &&
305+
!coverStation({
306+
$defs: p.$defs,
307+
visited: p.visited,
308+
x: p.x.additionalProperties,
309+
y: p.y.additionalProperties,
310+
})))
311+
)
312+
return false;
313+
return Object.entries(p.y.properties ?? {}).every(([key, b]) => {
293314
const a: IChatGptSchema | undefined = p.x.properties?.[key];
294315
if (a === undefined) return false;
295316
else if (
@@ -304,6 +325,7 @@ export namespace ChatGptTypeChecker {
304325
y: b,
305326
});
306327
});
328+
};
307329

308330
const coverBoolean = (
309331
x: IChatGptSchema.IBoolean,

src/utils/LlmTypeCheckerV3.ts

+168
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,174 @@ export namespace LlmTypeCheckerV3 {
6363
});
6464
};
6565

66+
export const covers = (x: ILlmSchemaV3, y: ILlmSchemaV3): boolean => {
67+
const alpha: ILlmSchemaV3[] = flatSchema(x);
68+
const beta: ILlmSchemaV3[] = flatSchema(y);
69+
if (alpha.some((x) => isUnknown(x))) return true;
70+
else if (beta.some((x) => isUnknown(x))) return false;
71+
return beta.every((b) =>
72+
alpha.some((a) => {
73+
// CHECK EQUALITY
74+
if (a === b) return true;
75+
else if (isUnknown(a)) return true;
76+
else if (isUnknown(b)) return false;
77+
else if (isNullOnly(a)) return isNullOnly(b);
78+
else if (isNullOnly(b)) return isNullable(a);
79+
else if (isNullable(a) && !isNullable(b)) return false;
80+
// ATOMIC CASE
81+
else if (isBoolean(a)) return isBoolean(b) && coverBoolean(a, b);
82+
else if (isInteger(a)) return isInteger(b) && coverInteger(a, b);
83+
else if (isNumber(a))
84+
return (isNumber(b) || isInteger(b)) && coverNumber(a, b);
85+
else if (isString(a)) return isString(b) && covertString(a, b);
86+
// INSTANCE CASE
87+
else if (isArray(a)) return isArray(b) && coverArray(a, b);
88+
else if (isObject(a)) return isObject(b) && coverObject(a, b);
89+
else if (isOneOf(a)) return false;
90+
}),
91+
);
92+
};
93+
94+
/**
95+
* @internal
96+
*/
97+
const coverBoolean = (
98+
x: ILlmSchemaV3.IBoolean,
99+
y: ILlmSchemaV3.IBoolean,
100+
): boolean =>
101+
x.enum === undefined ||
102+
(y.enum !== undefined && x.enum.every((v) => y.enum!.includes(v)));
103+
104+
/**
105+
* @internal
106+
*/
107+
const coverInteger = (
108+
x: ILlmSchemaV3.IInteger,
109+
y: ILlmSchemaV3.IInteger,
110+
): boolean => {
111+
if (x.enum !== undefined)
112+
return y.enum !== undefined && x.enum.every((v) => y.enum!.includes(v));
113+
return [
114+
x.type === y.type,
115+
x.minimum === undefined ||
116+
(y.minimum !== undefined && x.minimum <= y.minimum),
117+
x.maximum === undefined ||
118+
(y.maximum !== undefined && x.maximum >= y.maximum),
119+
x.exclusiveMinimum !== true ||
120+
x.minimum === undefined ||
121+
(y.minimum !== undefined &&
122+
(y.exclusiveMinimum === true || x.minimum < y.minimum)),
123+
x.exclusiveMaximum !== true ||
124+
x.maximum === undefined ||
125+
(y.maximum !== undefined &&
126+
(y.exclusiveMaximum === true || x.maximum > y.maximum)),
127+
x.multipleOf === undefined ||
128+
(y.multipleOf !== undefined &&
129+
y.multipleOf / x.multipleOf ===
130+
Math.floor(y.multipleOf / x.multipleOf)),
131+
].every((v) => v);
132+
};
133+
134+
/**
135+
* @internal
136+
*/
137+
const coverNumber = (
138+
x: ILlmSchemaV3.INumber,
139+
y: ILlmSchemaV3.INumber | ILlmSchemaV3.IInteger,
140+
): boolean => {
141+
if (x.enum !== undefined)
142+
return y.enum !== undefined && x.enum.every((v) => y.enum!.includes(v));
143+
return [
144+
x.type === y.type || (x.type === "number" && y.type === "integer"),
145+
x.minimum === undefined ||
146+
(y.minimum !== undefined && x.minimum <= y.minimum),
147+
x.maximum === undefined ||
148+
(y.maximum !== undefined && x.maximum >= y.maximum),
149+
x.exclusiveMinimum !== true ||
150+
x.minimum === undefined ||
151+
(y.minimum !== undefined &&
152+
(y.exclusiveMinimum === true || x.minimum < y.minimum)),
153+
x.exclusiveMaximum !== true ||
154+
x.maximum === undefined ||
155+
(y.maximum !== undefined &&
156+
(y.exclusiveMaximum === true || x.maximum > y.maximum)),
157+
x.multipleOf === undefined ||
158+
(y.multipleOf !== undefined &&
159+
y.multipleOf / x.multipleOf ===
160+
Math.floor(y.multipleOf / x.multipleOf)),
161+
].every((v) => v);
162+
};
163+
164+
/**
165+
* @internal
166+
*/
167+
const covertString = (
168+
x: ILlmSchemaV3.IString,
169+
y: ILlmSchemaV3.IString,
170+
): boolean => {
171+
if (x.enum !== undefined)
172+
return y.enum !== undefined && x.enum.every((v) => y.enum!.includes(v));
173+
return [
174+
x.type === y.type,
175+
x.format === undefined ||
176+
(y.format !== undefined && coverFormat(x.format, y.format)),
177+
x.pattern === undefined || x.pattern === y.pattern,
178+
x.minLength === undefined ||
179+
(y.minLength !== undefined && x.minLength <= y.minLength),
180+
x.maxLength === undefined ||
181+
(y.maxLength !== undefined && x.maxLength >= y.maxLength),
182+
].every((v) => v);
183+
};
184+
185+
const coverFormat = (
186+
x: Required<ILlmSchemaV3.IString>["format"],
187+
y: Required<ILlmSchemaV3.IString>["format"],
188+
): boolean =>
189+
x === y ||
190+
(x === "idn-email" && y === "email") ||
191+
(x === "idn-hostname" && y === "hostname") ||
192+
(["uri", "iri"].includes(x) && y === "url") ||
193+
(x === "iri" && y === "uri") ||
194+
(x === "iri-reference" && y === "uri-reference");
195+
196+
/**
197+
* @internal
198+
*/
199+
const coverArray = (
200+
x: ILlmSchemaV3.IArray,
201+
y: ILlmSchemaV3.IArray,
202+
): boolean => covers(x.items, y.items);
203+
204+
const coverObject = (
205+
x: ILlmSchemaV3.IObject,
206+
y: ILlmSchemaV3.IObject,
207+
): boolean => {
208+
if (!x.additionalProperties && !!y.additionalProperties) return false;
209+
else if (
210+
(!!x.additionalProperties &&
211+
!!y.additionalProperties &&
212+
typeof x.additionalProperties === "object" &&
213+
y.additionalProperties === true) ||
214+
(typeof x.additionalProperties === "object" &&
215+
typeof y.additionalProperties === "object" &&
216+
!covers(x.additionalProperties, y.additionalProperties))
217+
)
218+
return false;
219+
return Object.entries(y.properties ?? {}).every(([key, b]) => {
220+
const a: ILlmSchemaV3 | undefined = x.properties?.[key];
221+
if (a === undefined) return false;
222+
else if (
223+
(x.required?.includes(key) ?? false) === true &&
224+
(y.required?.includes(key) ?? false) === false
225+
)
226+
return false;
227+
return covers(a, b);
228+
});
229+
};
230+
231+
const flatSchema = (schema: ILlmSchemaV3): ILlmSchemaV3[] =>
232+
isOneOf(schema) ? schema.oneOf.flatMap(flatSchema) : [schema];
233+
66234
/* -----------------------------------------------------------
67235
TYPE CHECKERS
68236
----------------------------------------------------------- */
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import { TestValidator } from "@nestia/e2e";
2+
import { ILlmSchema, OpenApi } from "@samchon/openapi";
3+
import { LlmSchemaComposer } from "@samchon/openapi/lib/composers/LlmSchemaComposer";
4+
import typia, { IJsonSchemaCollection } from "typia";
5+
6+
export const test_chatgpt_type_checker_cover_any = (): void =>
7+
validate_llm_type_checker_cover_any("chatgpt");
8+
9+
export const test_claude_type_checker_cover_any = (): void =>
10+
validate_llm_type_checker_cover_any("claude");
11+
12+
export const test_gemini_type_checker_cover_any = (): void =>
13+
validate_llm_type_checker_cover_any("gemini");
14+
15+
export const test_llama_type_checker_cover_any = (): void =>
16+
validate_llm_type_checker_cover_any("llama");
17+
18+
export const test_llm_v30_type_checker_cover_any = (): void =>
19+
validate_llm_type_checker_cover_any("3.0");
20+
21+
export const test_llm_v31_type_checker_cover_any = (): void =>
22+
validate_llm_type_checker_cover_any("3.1");
23+
24+
const validate_llm_type_checker_cover_any = <Model extends ILlmSchema.Model>(
25+
model: Model,
26+
) => {
27+
const collection: IJsonSchemaCollection = typia.json.schemas<[IBasic]>();
28+
const result = LlmSchemaComposer.parameters(model)({
29+
config: LlmSchemaComposer.defaultConfig(model) as any,
30+
components: collection.components,
31+
schema: collection.schemas[0] as OpenApi.IJsonSchema.IReference,
32+
});
33+
if (result.success === false)
34+
throw new Error(`Failed to compose ${model} parameters.`);
35+
36+
const parameters = result.value;
37+
const check = (x: ILlmSchema<Model>, y: ILlmSchema<Model>): boolean =>
38+
model === "3.0" || model === "gemini"
39+
? (LlmSchemaComposer.typeChecker(model).covers as any)(x, y)
40+
: (LlmSchemaComposer.typeChecker(model).covers as any)({
41+
x,
42+
y,
43+
$defs: (parameters as any).$defs,
44+
});
45+
TestValidator.equals("any covers (string | null)")(true)(
46+
check(
47+
parameters.properties.any as ILlmSchema<Model>,
48+
parameters.properties.string_or_null as ILlmSchema<Model>,
49+
),
50+
);
51+
TestValidator.equals("any covers (string | undefined)")(true)(
52+
check(
53+
parameters.properties.any as ILlmSchema<Model>,
54+
parameters.properties.string_or_undefined as ILlmSchema<Model>,
55+
),
56+
);
57+
};
58+
59+
interface IBasic {
60+
any: any;
61+
string_or_null: null | string;
62+
string_or_undefined: string | undefined;
63+
}

0 commit comments

Comments
 (0)