Skip to content

Commit

Permalink
Merge pull request #112 from samchon/feat/optional
Browse files Browse the repository at this point in the history
Make ChatGPT strict mode configurable.
  • Loading branch information
samchon authored Dec 14, 2024
2 parents ab3fc2c + f73f747 commit 6f0f412
Show file tree
Hide file tree
Showing 19 changed files with 542 additions and 58 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@samchon/openapi",
"version": "2.1.2",
"version": "2.2.0",
"description": "OpenAPI definitions and converters for 'typia' and 'nestia'.",
"main": "./lib/index.js",
"module": "./lib/index.mjs",
Expand Down
1 change: 1 addition & 0 deletions src/composers/LlmSchemaComposer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ const SEPARATE_PARAMETERS = {
const DEFAULT_CONFIGS = {
chatgpt: {
reference: false,
strict: false,
} satisfies IChatGptSchema.IConfig,
claude: {
reference: false,
Expand Down
70 changes: 57 additions & 13 deletions src/composers/llm/ChatGptSchemaComposer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,22 @@ export namespace ChatGptSchemaComposer {
accessor?: string;
refAccessor?: string;
}): IResult<IChatGptSchema.IParameters, IOpenApiSchemaError> => {
// polyfill
props.config.strict ??= false;

// validate
const result: IResult<ILlmSchemaV3_1.IParameters, IOpenApiSchemaError> =
LlmSchemaV3_1Composer.parameters({
...props,
config: {
reference: props.config.reference,
constraint: false,
},
validate,
validate: props.config.strict === true ? validateStrict : undefined,
});
if (result.success === false) return result;

// returns with transformation
for (const key of Object.keys(result.value.$defs))
result.value.$defs[key] = transform(result.value.$defs[key]);
return {
Expand All @@ -43,6 +49,10 @@ export namespace ChatGptSchemaComposer {
accessor?: string;
refAccessor?: string;
}): IResult<IChatGptSchema, IOpenApiSchemaError> => {
// polyfill
props.config.strict ??= false;

// validate
const oldbie: Set<string> = new Set(Object.keys(props.$defs));
const result: IResult<ILlmSchemaV3_1, IOpenApiSchemaError> =
LlmSchemaV3_1Composer.schema({
Expand All @@ -51,9 +61,11 @@ export namespace ChatGptSchemaComposer {
reference: props.config.reference,
constraint: false,
},
validate,
validate: props.config.strict === true ? validateStrict : undefined,
});
if (result.success === false) return result;

// returns with transformation
for (const key of Object.keys(props.$defs))
if (oldbie.has(key) === false)
props.$defs[key] = transform(props.$defs[key]);
Expand All @@ -63,20 +75,29 @@ export namespace ChatGptSchemaComposer {
};
};

const validate = (
const validateStrict = (
schema: OpenApi.IJsonSchema,
accessor: string,
): IOpenApiSchemaError.IReason[] => {
if (OpenApiTypeChecker.isObject(schema) && !!schema.additionalProperties)
return [
{
const reasons: IOpenApiSchemaError.IReason[] = [];
if (OpenApiTypeChecker.isObject(schema)) {
if (!!schema.additionalProperties)
reasons.push({
schema: schema,
accessor: `${accessor}.additionalProperties`,
message:
"ChatGPT does not allow additionalProperties, the dynamic key typed object.",
},
];
return [];
"ChatGPT does not allow additionalProperties in strict mode, the dynamic key typed object.",
});
for (const key of Object.keys(schema.properties ?? {}))
if (schema.required?.includes(key) === false)
reasons.push({
schema: schema,
accessor: `${accessor}.properties.${key}`,
message:
"ChatGPT does not allow optional properties in strict mode.",
});
}
return reasons;
};

const transform = (schema: ILlmSchemaV3_1): IChatGptSchema => {
Expand Down Expand Up @@ -108,7 +129,11 @@ export namespace ChatGptSchemaComposer {
transform(value),
]),
),
additionalProperties: false,
additionalProperties:
typeof input.additionalProperties === "object" &&
input.additionalProperties !== null
? transform(input.additionalProperties)
: input.additionalProperties,
});
else if (LlmTypeCheckerV3_1.isConstant(input) === false)
union.push(input);
Expand Down Expand Up @@ -181,6 +206,7 @@ export namespace ChatGptSchemaComposer {
key.endsWith(".Llm"),
),
),
additionalProperties: false,
},
human: {
...human,
Expand All @@ -189,6 +215,7 @@ export namespace ChatGptSchemaComposer {
key.endsWith(".Human"),
),
),
additionalProperties: false,
},
};
for (const key of Object.keys(props.parameters.$defs))
Expand Down Expand Up @@ -270,6 +297,7 @@ export namespace ChatGptSchemaComposer {
const llm = {
...props.schema,
properties: {} as Record<string, IChatGptSchema>,
additionalProperties: props.schema.additionalProperties,
} satisfies IChatGptSchema.IObject;
const human = {
...props.schema,
Expand All @@ -285,9 +313,25 @@ export namespace ChatGptSchemaComposer {
if (x !== null) llm.properties[key] = x;
if (y !== null) human.properties[key] = y;
}
if (
typeof props.schema.additionalProperties === "object" &&
props.schema.additionalProperties !== null
) {
const [dx, dy] = separateStation({
$defs: props.$defs,
predicate: props.predicate,
schema: props.schema.additionalProperties,
});
llm.additionalProperties = dx ?? false;
human.additionalProperties = dy ?? false;
}
return [
Object.keys(llm.properties).length === 0 ? null : shrinkRequired(llm),
Object.keys(human.properties).length === 0 ? null : shrinkRequired(human),
!!Object.keys(llm.properties).length || !!llm.additionalProperties
? shrinkRequired(llm)
: null,
!!Object.keys(human.properties).length || human.additionalProperties
? shrinkRequired(human)
: null,
];
};

Expand Down
2 changes: 1 addition & 1 deletion src/composers/llm/LlmSchemaV3Composer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@ export namespace LlmSchemaV3Composer {
predicate: props.predicate,
schema: props.parameters,
});
return { llm, human };
return { llm, human } as ILlmFunction.ISeparated<"3.0">;
};

const separateStation = (props: {
Expand Down
4 changes: 3 additions & 1 deletion src/composers/llm/LlmSchemaV3_1Composer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -230,7 +230,7 @@ export namespace LlmSchemaV3_1Composer {
...input,
properties: properties as Record<string, ILlmSchemaV3_1>,
additionalProperties,
required: Object.keys(properties),
required: input.required ?? [],
});
} else if (OpenApiTypeChecker.isArray(input)) {
const items: IResult<ILlmSchemaV3_1, IOpenApiSchemaError> = schema({
Expand Down Expand Up @@ -345,6 +345,7 @@ export namespace LlmSchemaV3_1Composer {
key.endsWith(".Llm"),
),
),
additionalProperties: false,
},
human: {
...human,
Expand All @@ -353,6 +354,7 @@ export namespace LlmSchemaV3_1Composer {
key.endsWith(".Human"),
),
),
additionalProperties: false,
},
};
for (const key of Object.keys(props.parameters.$defs))
Expand Down
46 changes: 39 additions & 7 deletions src/structures/IChatGptSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@
* - Merge {@link OpenApiV3_1.IJsonSchema.IOneOf} to {@link IChatGptSchema.IAnOf}
* - Merge {@link OpenApiV3_1.IJsonSchema.IAllOf} to {@link IChatGptSchema.IObject}
* - Merge {@link OpenApiV3_1.IJsonSchema.IRecursiveReference} to {@link IChatGptSchema.IReference}
* - Forcibly transform every object properties to be required
* - When {@link IChatGptSchema.IConfig.strict} mode
* - Every object properties must be required
* - Do not allow {@link IChatGptSchema.IObject.additionalProperties}
*
* If compare with the {@link OpenApi.IJsonSchema}, the emended JSON schema specification,
*
Expand All @@ -26,7 +28,9 @@
* - {@link IChatGptSchema.IString.enum} instead of the {@link OpenApi.IJsonSchema.IConstant}
* - {@link IChatGptSchema.additionalProperties} is fixed to `false`
* - No tuple type {@link OpenApi.IJsonSchema.ITuple} support
* - Forcibly transform every object properties to be required
* - When {@link IChatGptSchema.IConfig.strict} mode
* - Every object properties must be required
* - Do not allow {@link IChatGptSchema.IObject.additionalProperties}
*
* For reference, if you've composed the `IChatGptSchema` type with the
* {@link IChatGptSchema.IConfig.reference} `false` option (default is `false`),
Expand Down Expand Up @@ -82,11 +86,21 @@ export namespace IChatGptSchema {
*
* @reference https://platform.openai.com/docs/guides/structured-outputs
*/
export interface IParameters extends IObject {
export interface IParameters extends Omit<IObject, "additionalProperties"> {
/**
* Collection of the named types.
*/
$defs: Record<string, IChatGptSchema>;

/**
* Additional properties' info.
*
* The `additionalProperties` means the type schema info of the additional
* properties that are not listed in the {@link properties}.
*
* By the way, it is not allowed in the parameters level.
*/
additionalProperties: false;
}

/**
Expand Down Expand Up @@ -161,11 +175,11 @@ export namespace IChatGptSchema {
* The `additionalProperties` means the type schema info of the additional
* properties that are not listed in the {@link properties}.
*
* By the way, as ChatGPT function calling does not support such
* dynamic key typed properties, the `additionalProperties` becomes
* always `false`.
* By the way, if you've configured {@link IChatGptSchema.IConfig.strict} as `true`,
* ChatGPT function calling does not support such dynamic key typed properties, so
* the `additionalProperties` becomes always `false`.
*/
additionalProperties: false;
additionalProperties?: boolean | IChatGptSchema;

/**
* List of key values of the required properties.
Expand Down Expand Up @@ -315,5 +329,23 @@ export namespace IChatGptSchema {
* @default false
*/
reference: boolean;

/**
* Whether to apply the strict mode.
*
* If you configure this property to `true`, the ChatGPT function calling
* does not allow optional properties and dynamic key typed properties in the
* {@link IChatGptSchema.IObject} type. Instead, it increases the success
* rate of the function calling.
*
* By the way, if you utilize the {@link typia.validate} function and give
* its validation feedback to the ChatGPT, its performance is much better
* than the strict mode. Therefore, I recommend you to just turn off the
* strict mode and utilize the {@link typia.validate} function instead.
*
* @todo Would be required in the future
* @default false
*/
strict?: boolean;
}
}
12 changes: 11 additions & 1 deletion src/structures/ILlmSchemaV3.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,17 @@ export namespace ILlmSchemaV3 {
*
* @reference https://platform.openai.com/docs/guides/structured-outputs
*/
export type IParameters = IObject;
export interface IParameters extends Omit<IObject, "additionalProperties"> {
/**
* Additional properties' info.
*
* The `additionalProperties` means the type schema info of the additional
* properties that are not listed in the {@link properties}.
*
* By the way, it is not allowed in the parameters level.
*/
additionalProperties: false;
}

/**
* Boolean type schema info.
Expand Down
12 changes: 11 additions & 1 deletion src/structures/ILlmSchemaV3_1.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,11 +78,21 @@ export namespace ILlmSchemaV3_1 {
*
* @reference https://platform.openai.com/docs/guides/structured-outputs
*/
export interface IParameters extends IObject {
export interface IParameters extends Omit<IObject, "additionalProperties"> {
/**
* Collection of the named types.
*/
$defs: Record<string, ILlmSchemaV3_1>;

/**
* Additional properties' info.
*
* The `additionalProperties` means the type schema info of the additional
* properties that are not listed in the {@link properties}.
*
* By the way, it is not allowed in the parameters level.
*/
additionalProperties: false;
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import fs from "fs";
import typia, { tags } from "typia";

import { TestGlobal } from "../../../TestGlobal";
import { ChatGptFunctionCaller } from "../../../utils/ChatGptFunctionCaller";

export const test_chatgpt_function_calling_additionalProperties =
(): Promise<void> =>
ChatGptFunctionCaller.test({
name: "enrollPerson",
description: "Enroll a person to the restaurant reservation list.",
collection: typia.json.schemas<[{ input: IPerson }]>(),
validate: typia.createValidate<[{ input: IPerson }]>(),
texts: [
{
role: "assistant",
content: SYSTEM_MESSAGE,
},
{
role: "user",
content: USER_MESSAGE,
},
],
handleParameters: async (parameters) => {
if (process.argv.includes("--file"))
await fs.promises.writeFile(
`${TestGlobal.ROOT}/examples/function-calling/schemas/chatgpt.additionalProperties.schema.json`,
JSON.stringify(parameters, null, 2),
"utf8",
);
},
handleCompletion: async (input) => {
typia.assert<IPerson>(input);
if (process.argv.includes("--file"))
await fs.promises.writeFile(
`${TestGlobal.ROOT}/examples/function-calling/arguments/chatgpt.additionalProperties.input.json`,
JSON.stringify(input, null, 2),
"utf8",
);
},
});

interface IPerson {
/**
* The name of the person.
*/
name: string;

/**
* The age of the person.
*/
age: number & tags.Type<"uint32">;

/**
* Additional informations about the person.
*/
etc: Record<string, string>;
}

const SYSTEM_MESSAGE =
"You are a helpful customer support assistant. Use the supplied tools to assist the user.";

const USER_MESSAGE = `
Just enroll a person with below information:
- name: John Doe
- age: 42
- hobby: Soccer
- job: Scientist
`;
Loading

0 comments on commit 6f0f412

Please sign in to comment.