Skip to content

Commit 6f0f412

Browse files
authored
Merge pull request #112 from samchon/feat/optional
Make ChatGPT strict mode configurable.
2 parents ab3fc2c + f73f747 commit 6f0f412

19 files changed

+542
-58
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.1.2",
3+
"version": "2.2.0",
44
"description": "OpenAPI definitions and converters for 'typia' and 'nestia'.",
55
"main": "./lib/index.js",
66
"module": "./lib/index.mjs",

src/composers/LlmSchemaComposer.ts

+1
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ const SEPARATE_PARAMETERS = {
6666
const DEFAULT_CONFIGS = {
6767
chatgpt: {
6868
reference: false,
69+
strict: false,
6970
} satisfies IChatGptSchema.IConfig,
7071
claude: {
7172
reference: false,

src/composers/llm/ChatGptSchemaComposer.ts

+57-13
Original file line numberDiff line numberDiff line change
@@ -17,16 +17,22 @@ export namespace ChatGptSchemaComposer {
1717
accessor?: string;
1818
refAccessor?: string;
1919
}): IResult<IChatGptSchema.IParameters, IOpenApiSchemaError> => {
20+
// polyfill
21+
props.config.strict ??= false;
22+
23+
// validate
2024
const result: IResult<ILlmSchemaV3_1.IParameters, IOpenApiSchemaError> =
2125
LlmSchemaV3_1Composer.parameters({
2226
...props,
2327
config: {
2428
reference: props.config.reference,
2529
constraint: false,
2630
},
27-
validate,
31+
validate: props.config.strict === true ? validateStrict : undefined,
2832
});
2933
if (result.success === false) return result;
34+
35+
// returns with transformation
3036
for (const key of Object.keys(result.value.$defs))
3137
result.value.$defs[key] = transform(result.value.$defs[key]);
3238
return {
@@ -43,6 +49,10 @@ export namespace ChatGptSchemaComposer {
4349
accessor?: string;
4450
refAccessor?: string;
4551
}): IResult<IChatGptSchema, IOpenApiSchemaError> => {
52+
// polyfill
53+
props.config.strict ??= false;
54+
55+
// validate
4656
const oldbie: Set<string> = new Set(Object.keys(props.$defs));
4757
const result: IResult<ILlmSchemaV3_1, IOpenApiSchemaError> =
4858
LlmSchemaV3_1Composer.schema({
@@ -51,9 +61,11 @@ export namespace ChatGptSchemaComposer {
5161
reference: props.config.reference,
5262
constraint: false,
5363
},
54-
validate,
64+
validate: props.config.strict === true ? validateStrict : undefined,
5565
});
5666
if (result.success === false) return result;
67+
68+
// returns with transformation
5769
for (const key of Object.keys(props.$defs))
5870
if (oldbie.has(key) === false)
5971
props.$defs[key] = transform(props.$defs[key]);
@@ -63,20 +75,29 @@ export namespace ChatGptSchemaComposer {
6375
};
6476
};
6577

66-
const validate = (
78+
const validateStrict = (
6779
schema: OpenApi.IJsonSchema,
6880
accessor: string,
6981
): IOpenApiSchemaError.IReason[] => {
70-
if (OpenApiTypeChecker.isObject(schema) && !!schema.additionalProperties)
71-
return [
72-
{
82+
const reasons: IOpenApiSchemaError.IReason[] = [];
83+
if (OpenApiTypeChecker.isObject(schema)) {
84+
if (!!schema.additionalProperties)
85+
reasons.push({
7386
schema: schema,
7487
accessor: `${accessor}.additionalProperties`,
7588
message:
76-
"ChatGPT does not allow additionalProperties, the dynamic key typed object.",
77-
},
78-
];
79-
return [];
89+
"ChatGPT does not allow additionalProperties in strict mode, the dynamic key typed object.",
90+
});
91+
for (const key of Object.keys(schema.properties ?? {}))
92+
if (schema.required?.includes(key) === false)
93+
reasons.push({
94+
schema: schema,
95+
accessor: `${accessor}.properties.${key}`,
96+
message:
97+
"ChatGPT does not allow optional properties in strict mode.",
98+
});
99+
}
100+
return reasons;
80101
};
81102

82103
const transform = (schema: ILlmSchemaV3_1): IChatGptSchema => {
@@ -108,7 +129,11 @@ export namespace ChatGptSchemaComposer {
108129
transform(value),
109130
]),
110131
),
111-
additionalProperties: false,
132+
additionalProperties:
133+
typeof input.additionalProperties === "object" &&
134+
input.additionalProperties !== null
135+
? transform(input.additionalProperties)
136+
: input.additionalProperties,
112137
});
113138
else if (LlmTypeCheckerV3_1.isConstant(input) === false)
114139
union.push(input);
@@ -181,6 +206,7 @@ export namespace ChatGptSchemaComposer {
181206
key.endsWith(".Llm"),
182207
),
183208
),
209+
additionalProperties: false,
184210
},
185211
human: {
186212
...human,
@@ -189,6 +215,7 @@ export namespace ChatGptSchemaComposer {
189215
key.endsWith(".Human"),
190216
),
191217
),
218+
additionalProperties: false,
192219
},
193220
};
194221
for (const key of Object.keys(props.parameters.$defs))
@@ -270,6 +297,7 @@ export namespace ChatGptSchemaComposer {
270297
const llm = {
271298
...props.schema,
272299
properties: {} as Record<string, IChatGptSchema>,
300+
additionalProperties: props.schema.additionalProperties,
273301
} satisfies IChatGptSchema.IObject;
274302
const human = {
275303
...props.schema,
@@ -285,9 +313,25 @@ export namespace ChatGptSchemaComposer {
285313
if (x !== null) llm.properties[key] = x;
286314
if (y !== null) human.properties[key] = y;
287315
}
316+
if (
317+
typeof props.schema.additionalProperties === "object" &&
318+
props.schema.additionalProperties !== null
319+
) {
320+
const [dx, dy] = separateStation({
321+
$defs: props.$defs,
322+
predicate: props.predicate,
323+
schema: props.schema.additionalProperties,
324+
});
325+
llm.additionalProperties = dx ?? false;
326+
human.additionalProperties = dy ?? false;
327+
}
288328
return [
289-
Object.keys(llm.properties).length === 0 ? null : shrinkRequired(llm),
290-
Object.keys(human.properties).length === 0 ? null : shrinkRequired(human),
329+
!!Object.keys(llm.properties).length || !!llm.additionalProperties
330+
? shrinkRequired(llm)
331+
: null,
332+
!!Object.keys(human.properties).length || human.additionalProperties
333+
? shrinkRequired(human)
334+
: null,
291335
];
292336
};
293337

src/composers/llm/LlmSchemaV3Composer.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,7 @@ export namespace LlmSchemaV3Composer {
165165
predicate: props.predicate,
166166
schema: props.parameters,
167167
});
168-
return { llm, human };
168+
return { llm, human } as ILlmFunction.ISeparated<"3.0">;
169169
};
170170

171171
const separateStation = (props: {

src/composers/llm/LlmSchemaV3_1Composer.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -230,7 +230,7 @@ export namespace LlmSchemaV3_1Composer {
230230
...input,
231231
properties: properties as Record<string, ILlmSchemaV3_1>,
232232
additionalProperties,
233-
required: Object.keys(properties),
233+
required: input.required ?? [],
234234
});
235235
} else if (OpenApiTypeChecker.isArray(input)) {
236236
const items: IResult<ILlmSchemaV3_1, IOpenApiSchemaError> = schema({
@@ -345,6 +345,7 @@ export namespace LlmSchemaV3_1Composer {
345345
key.endsWith(".Llm"),
346346
),
347347
),
348+
additionalProperties: false,
348349
},
349350
human: {
350351
...human,
@@ -353,6 +354,7 @@ export namespace LlmSchemaV3_1Composer {
353354
key.endsWith(".Human"),
354355
),
355356
),
357+
additionalProperties: false,
356358
},
357359
};
358360
for (const key of Object.keys(props.parameters.$defs))

src/structures/IChatGptSchema.ts

+39-7
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,9 @@
1717
* - Merge {@link OpenApiV3_1.IJsonSchema.IOneOf} to {@link IChatGptSchema.IAnOf}
1818
* - Merge {@link OpenApiV3_1.IJsonSchema.IAllOf} to {@link IChatGptSchema.IObject}
1919
* - Merge {@link OpenApiV3_1.IJsonSchema.IRecursiveReference} to {@link IChatGptSchema.IReference}
20-
* - Forcibly transform every object properties to be required
20+
* - When {@link IChatGptSchema.IConfig.strict} mode
21+
* - Every object properties must be required
22+
* - Do not allow {@link IChatGptSchema.IObject.additionalProperties}
2123
*
2224
* If compare with the {@link OpenApi.IJsonSchema}, the emended JSON schema specification,
2325
*
@@ -26,7 +28,9 @@
2628
* - {@link IChatGptSchema.IString.enum} instead of the {@link OpenApi.IJsonSchema.IConstant}
2729
* - {@link IChatGptSchema.additionalProperties} is fixed to `false`
2830
* - No tuple type {@link OpenApi.IJsonSchema.ITuple} support
29-
* - Forcibly transform every object properties to be required
31+
* - When {@link IChatGptSchema.IConfig.strict} mode
32+
* - Every object properties must be required
33+
* - Do not allow {@link IChatGptSchema.IObject.additionalProperties}
3034
*
3135
* For reference, if you've composed the `IChatGptSchema` type with the
3236
* {@link IChatGptSchema.IConfig.reference} `false` option (default is `false`),
@@ -82,11 +86,21 @@ export namespace IChatGptSchema {
8286
*
8387
* @reference https://platform.openai.com/docs/guides/structured-outputs
8488
*/
85-
export interface IParameters extends IObject {
89+
export interface IParameters extends Omit<IObject, "additionalProperties"> {
8690
/**
8791
* Collection of the named types.
8892
*/
8993
$defs: Record<string, IChatGptSchema>;
94+
95+
/**
96+
* Additional properties' info.
97+
*
98+
* The `additionalProperties` means the type schema info of the additional
99+
* properties that are not listed in the {@link properties}.
100+
*
101+
* By the way, it is not allowed in the parameters level.
102+
*/
103+
additionalProperties: false;
90104
}
91105

92106
/**
@@ -161,11 +175,11 @@ export namespace IChatGptSchema {
161175
* The `additionalProperties` means the type schema info of the additional
162176
* properties that are not listed in the {@link properties}.
163177
*
164-
* By the way, as ChatGPT function calling does not support such
165-
* dynamic key typed properties, the `additionalProperties` becomes
166-
* always `false`.
178+
* By the way, if you've configured {@link IChatGptSchema.IConfig.strict} as `true`,
179+
* ChatGPT function calling does not support such dynamic key typed properties, so
180+
* the `additionalProperties` becomes always `false`.
167181
*/
168-
additionalProperties: false;
182+
additionalProperties?: boolean | IChatGptSchema;
169183

170184
/**
171185
* List of key values of the required properties.
@@ -315,5 +329,23 @@ export namespace IChatGptSchema {
315329
* @default false
316330
*/
317331
reference: boolean;
332+
333+
/**
334+
* Whether to apply the strict mode.
335+
*
336+
* If you configure this property to `true`, the ChatGPT function calling
337+
* does not allow optional properties and dynamic key typed properties in the
338+
* {@link IChatGptSchema.IObject} type. Instead, it increases the success
339+
* rate of the function calling.
340+
*
341+
* By the way, if you utilize the {@link typia.validate} function and give
342+
* its validation feedback to the ChatGPT, its performance is much better
343+
* than the strict mode. Therefore, I recommend you to just turn off the
344+
* strict mode and utilize the {@link typia.validate} function instead.
345+
*
346+
* @todo Would be required in the future
347+
* @default false
348+
*/
349+
strict?: boolean;
318350
}
319351
}

src/structures/ILlmSchemaV3.ts

+11-1
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,17 @@ export namespace ILlmSchemaV3 {
6060
*
6161
* @reference https://platform.openai.com/docs/guides/structured-outputs
6262
*/
63-
export type IParameters = IObject;
63+
export interface IParameters extends Omit<IObject, "additionalProperties"> {
64+
/**
65+
* Additional properties' info.
66+
*
67+
* The `additionalProperties` means the type schema info of the additional
68+
* properties that are not listed in the {@link properties}.
69+
*
70+
* By the way, it is not allowed in the parameters level.
71+
*/
72+
additionalProperties: false;
73+
}
6474

6575
/**
6676
* Boolean type schema info.

src/structures/ILlmSchemaV3_1.ts

+11-1
Original file line numberDiff line numberDiff line change
@@ -78,11 +78,21 @@ export namespace ILlmSchemaV3_1 {
7878
*
7979
* @reference https://platform.openai.com/docs/guides/structured-outputs
8080
*/
81-
export interface IParameters extends IObject {
81+
export interface IParameters extends Omit<IObject, "additionalProperties"> {
8282
/**
8383
* Collection of the named types.
8484
*/
8585
$defs: Record<string, ILlmSchemaV3_1>;
86+
87+
/**
88+
* Additional properties' info.
89+
*
90+
* The `additionalProperties` means the type schema info of the additional
91+
* properties that are not listed in the {@link properties}.
92+
*
93+
* By the way, it is not allowed in the parameters level.
94+
*/
95+
additionalProperties: false;
8696
}
8797

8898
/**
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import fs from "fs";
2+
import typia, { tags } from "typia";
3+
4+
import { TestGlobal } from "../../../TestGlobal";
5+
import { ChatGptFunctionCaller } from "../../../utils/ChatGptFunctionCaller";
6+
7+
export const test_chatgpt_function_calling_additionalProperties =
8+
(): Promise<void> =>
9+
ChatGptFunctionCaller.test({
10+
name: "enrollPerson",
11+
description: "Enroll a person to the restaurant reservation list.",
12+
collection: typia.json.schemas<[{ input: IPerson }]>(),
13+
validate: typia.createValidate<[{ input: IPerson }]>(),
14+
texts: [
15+
{
16+
role: "assistant",
17+
content: SYSTEM_MESSAGE,
18+
},
19+
{
20+
role: "user",
21+
content: USER_MESSAGE,
22+
},
23+
],
24+
handleParameters: async (parameters) => {
25+
if (process.argv.includes("--file"))
26+
await fs.promises.writeFile(
27+
`${TestGlobal.ROOT}/examples/function-calling/schemas/chatgpt.additionalProperties.schema.json`,
28+
JSON.stringify(parameters, null, 2),
29+
"utf8",
30+
);
31+
},
32+
handleCompletion: async (input) => {
33+
typia.assert<IPerson>(input);
34+
if (process.argv.includes("--file"))
35+
await fs.promises.writeFile(
36+
`${TestGlobal.ROOT}/examples/function-calling/arguments/chatgpt.additionalProperties.input.json`,
37+
JSON.stringify(input, null, 2),
38+
"utf8",
39+
);
40+
},
41+
});
42+
43+
interface IPerson {
44+
/**
45+
* The name of the person.
46+
*/
47+
name: string;
48+
49+
/**
50+
* The age of the person.
51+
*/
52+
age: number & tags.Type<"uint32">;
53+
54+
/**
55+
* Additional informations about the person.
56+
*/
57+
etc: Record<string, string>;
58+
}
59+
60+
const SYSTEM_MESSAGE =
61+
"You are a helpful customer support assistant. Use the supplied tools to assist the user.";
62+
63+
const USER_MESSAGE = `
64+
Just enroll a person with below information:
65+
66+
- name: John Doe
67+
- age: 42
68+
- hobby: Soccer
69+
- job: Scientist
70+
`;

0 commit comments

Comments
 (0)