Skip to content

Commit

Permalink
Fix bug of recursive ChatGPT schema conversion
Browse files Browse the repository at this point in the history
  • Loading branch information
samchon committed Nov 10, 2024
1 parent 041cc51 commit 2863849
Show file tree
Hide file tree
Showing 6 changed files with 291 additions and 151 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.0.0-dev.20241109",
"version": "2.0.0-dev.20241111",
"description": "OpenAPI definitions and converters for 'typia' and 'nestia'.",
"main": "./lib/index.js",
"module": "./lib/index.mjs",
Expand Down
10 changes: 6 additions & 4 deletions src/converters/ChatGptConverter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,11 @@ export namespace ChatGptConverter {
props.components.schemas?.[key];
if (target === undefined) return null;

const out = () => ({
...props.schema,
$ref: `#/$defs/${key}`,
});
if (props.$defs[key] !== undefined) return out();
props.$defs[key] = {};
const converted: IChatGptSchema | null = convertSchema({
components: props.components,
Expand All @@ -39,10 +44,7 @@ export namespace ChatGptConverter {
if (converted === null) return null;

props.$defs[key] = converted;
return {
...props.schema,
$ref: `#/$defs/${key}`,
};
return out();
} else if (OpenApiTypeChecker.isArray(props.schema)) {
const items: IChatGptSchema | null = convertSchema({
components: props.components,
Expand Down
294 changes: 149 additions & 145 deletions src/converters/HttpLlmConverter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,158 +89,162 @@ export namespace HttpLlmConverter {
schema: props.schema,
}) as Schema | null;
};
}

const composeFunction = <
Model extends IHttpLlmApplication.Model,
Schema extends
| ILlmSchemaV3
| ILlmSchemaV3_1
| IChatGptSchema
| IGeminiSchema = IHttpLlmApplication.ModelSchema[Model],
Operation extends OpenApi.IOperation = OpenApi.IOperation,
Route extends IHttpMigrateRoute = IHttpMigrateRoute<
OpenApi.IJsonSchema,
Operation
>,
>(props: {
model: Model;
components: OpenApi.IComponents;
route: IHttpMigrateRoute<OpenApi.IJsonSchema, Operation>;
options: IHttpLlmApplication.IOptions<Model, Schema>;
}): IHttpLlmFunction<Schema, Operation, Route> | null => {
const cast = (s: OpenApi.IJsonSchema): Schema | null =>
CASTERS[props.model]({
components: props.components,
recursive: props.options.recursive,
schema: s,
}) as Schema | null;
const output: Schema | null | undefined =
props.route.success && props.route.success
? cast(props.route.success.schema)
: undefined;
if (output === null) return null;
const properties: [string, Schema | null][] = [
...props.route.parameters.map((p) => ({
key: p.key,
schema: {
...p.schema,
title: p.parameter().title ?? p.schema.title,
description: p.parameter().description ?? p.schema.description,
},
})),
...(props.route.query
? [
{
key: props.route.query.key,
schema: {
...props.route.query.schema,
title:
props.route.query.title() ?? props.route.query.schema.title,
description:
props.route.query.description() ??
props.route.query.schema.description,
export const separateParameters = <
Model extends IHttpLlmApplication.Model,
Schema extends
| ILlmSchemaV3
| ILlmSchemaV3_1
| IChatGptSchema
| IGeminiSchema,
>(props: {
model: Model;
parameters: Schema[];
predicate: (schema: Schema) => boolean;
}): IHttpLlmFunction.ISeparated<Schema> => {
const separator: (props: {
predicate: (schema: Schema) => boolean;
schema: Schema;
}) => [Schema | null, Schema | null] = SEPARATORS[props.model] as any;
const indexes: Array<[Schema | null, Schema | null]> = props.parameters.map(
(schema) =>
separator({
predicate: props.predicate,
schema,
}),
);
return {
llm: indexes
.map(([llm], index) => ({
index,
schema: llm!,
}))
.filter(({ schema }) => schema !== null),
human: indexes
.map(([, human], index) => ({
index,
schema: human!,
}))
.filter(({ schema }) => schema !== null),
};
};

const composeFunction = <
Model extends IHttpLlmApplication.Model,
Schema extends
| ILlmSchemaV3
| ILlmSchemaV3_1
| IChatGptSchema
| IGeminiSchema = IHttpLlmApplication.ModelSchema[Model],
Operation extends OpenApi.IOperation = OpenApi.IOperation,
Route extends IHttpMigrateRoute = IHttpMigrateRoute<
OpenApi.IJsonSchema,
Operation
>,
>(props: {
model: Model;
components: OpenApi.IComponents;
route: IHttpMigrateRoute<OpenApi.IJsonSchema, Operation>;
options: IHttpLlmApplication.IOptions<Model, Schema>;
}): IHttpLlmFunction<Schema, Operation, Route> | null => {
const cast = (s: OpenApi.IJsonSchema): Schema | null =>
CASTERS[props.model]({
components: props.components,
recursive: props.options.recursive,
schema: s,
}) as Schema | null;
const output: Schema | null | undefined =
props.route.success && props.route.success
? cast(props.route.success.schema)
: undefined;
if (output === null) return null;
const properties: [string, Schema | null][] = [
...props.route.parameters.map((p) => ({
key: p.key,
schema: {
...p.schema,
title: p.parameter().title ?? p.schema.title,
description: p.parameter().description ?? p.schema.description,
},
})),
...(props.route.query
? [
{
key: props.route.query.key,
schema: {
...props.route.query.schema,
title:
props.route.query.title() ?? props.route.query.schema.title,
description:
props.route.query.description() ??
props.route.query.schema.description,
},
},
},
]
: []),
...(props.route.body
]
: []),
...(props.route.body
? [
{
key: props.route.body.key,
schema: {
...props.route.body.schema,
description:
props.route.body.description() ??
props.route.body.schema.description,
},
},
]
: []),
].map((o) => [o.key, cast(o.schema)]);
if (properties.some(([_k, v]) => v === null)) return null;

// COMPOSE PARAMETERS
const parameters: Schema[] = props.options.keyword
? [
{
key: props.route.body.key,
schema: {
...props.route.body.schema,
description:
props.route.body.description() ??
props.route.body.schema.description,
},
},
type: "object",
properties: Object.fromEntries(properties as [string, Schema][]),
additionalProperties: false,
} as any as Schema,
]
: []),
].map((o) => [o.key, cast(o.schema)]);
if (properties.some(([_k, v]) => v === null)) return null;
: properties.map(([_k, v]) => v!);
const operation: OpenApi.IOperation = props.route.operation();

// COMPOSE PARAMETERS
const parameters: Schema[] = props.options.keyword
? [
{
type: "object",
properties: Object.fromEntries(properties as [string, Schema][]),
additionalProperties: false,
} as any as Schema,
]
: properties.map(([_k, v]) => v!);
const operation: OpenApi.IOperation = props.route.operation();

// FINALIZATION
return {
method: props.route.method as "get",
path: props.route.path,
name: props.route.accessor.join("_"),
strict: true,
parameters,
separated: props.options.separate
? separateParameters({
model: props.model,
predicate: props.options.separate,
parameters,
})
: undefined,
output,
description: (() => {
if (operation.summary && operation.description) {
return operation.description.startsWith(operation.summary)
? operation.description
: [
operation.summary,
operation.summary.endsWith(".") ? "" : ".",
"\n\n",
operation.description,
].join("");
}
return operation.description ?? operation.summary;
})(),
deprecated: operation.deprecated,
tags: operation.tags,
route: () => props.route as any,
operation: () => props.route.operation(),
};
};

const separateParameters = <
Model extends IHttpLlmApplication.Model,
Schema extends ILlmSchemaV3 | ILlmSchemaV3_1 | IChatGptSchema | IGeminiSchema,
>(props: {
model: Model;
parameters: Schema[];
predicate: (schema: Schema) => boolean;
}): IHttpLlmFunction.ISeparated<Schema> => {
const separator: (props: {
predicate: (schema: Schema) => boolean;
schema: Schema;
}) => [Schema | null, Schema | null] = SEPARATORS[props.model] as any;
const indexes: Array<[Schema | null, Schema | null]> = props.parameters.map(
(schema) =>
separator({
predicate: props.predicate,
schema,
}),
);
return {
llm: indexes
.map(([llm], index) => ({
index,
schema: llm!,
}))
.filter(({ schema }) => schema !== null),
human: indexes
.map(([, human], index) => ({
index,
schema: human!,
}))
.filter(({ schema }) => schema !== null),
// FINALIZATION
return {
method: props.route.method as "get",
path: props.route.path,
name: props.route.accessor.join("_"),
strict: true,
parameters,
separated: props.options.separate
? separateParameters({
model: props.model,
predicate: props.options.separate,
parameters,
})
: undefined,
output,
description: (() => {
if (operation.summary && operation.description) {
return operation.description.startsWith(operation.summary)
? operation.description
: [
operation.summary,
operation.summary.endsWith(".") ? "" : ".",
"\n\n",
operation.description,
].join("");
}
return operation.description ?? operation.summary;
})(),
deprecated: operation.deprecated,
tags: operation.tags,
route: () => props.route as any,
operation: () => props.route.operation(),
};
};
};
}

const CASTERS = {
"3.0": (props: {
Expand Down
2 changes: 1 addition & 1 deletion src/structures/ILlmApplication.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ export namespace ILlmApplication {
| ILlmSchemaV3
| ILlmSchemaV3_1
| IChatGptSchema
| IGeminiSchema,
| IGeminiSchema = ILlmApplication.ModelSchema[Model],
> {
/**
* Whether to allow recursive types or not.
Expand Down
50 changes: 50 additions & 0 deletions test/features/llm/chatgpt/test_chatgpt_schema_recursive_array.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { TestValidator } from "@nestia/e2e";
import { ChatGptConverter } from "@samchon/openapi/lib/converters/ChatGptConverter";

export const test_chatgpt_schema_recursive_array = (): void => {
const schema = ChatGptConverter.schema({
components: {
schemas: {
Department: {
type: "object",
properties: {
name: {
type: "string",
},
children: {
type: "array",
items: {
$ref: "#/components/schemas/Department",
},
},
},
required: ["name", "children"],
},
},
},
schema: {
$ref: "#/components/schemas/Department",
},
});
TestValidator.equals("recursive")(schema)({
$ref: "#/$defs/Department",
$defs: {
Department: {
type: "object",
properties: {
name: {
type: "string",
},
children: {
type: "array",
items: {
$ref: "#/$defs/Department",
},
},
},
required: ["name", "children"],
additionalProperties: false,
},
},
});
};
Loading

0 comments on commit 2863849

Please sign in to comment.