Skip to content

Commit 4ed4b9b

Browse files
authored
feat(core): decompose stringified property of JsonUtil.parse (#531)
1 parent b9e0eed commit 4ed4b9b

File tree

7 files changed

+200
-4
lines changed

7 files changed

+200
-4
lines changed

packages/core/src/orchestrate/call.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -318,7 +318,10 @@ function parseArguments(
318318
life: number,
319319
): AgenticaCallEvent | AgenticaJsonParseErrorEvent {
320320
try {
321-
const data: Record<string, unknown> = JsonUtil.parse(toolCall.function.arguments);
321+
const data: Record<string, unknown> = JsonUtil.parse(
322+
toolCall.function.arguments,
323+
operation.function.parameters,
324+
);
322325
return createCallEvent({
323326
id: toolCall.id,
324327
operation,

packages/core/src/utils/JsonUtil.ts

Lines changed: 61 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
import type { IValidation } from "@samchon/openapi";
1+
import type { ILlmSchema, IValidation } from "@samchon/openapi";
22

3+
import { LlmTypeChecker } from "@samchon/openapi";
34
import { addMissingBraces, removeEmptyObjectPrefix, removeTrailingCommas } from "es-jsonkit";
45
import { jsonrepair } from "jsonrepair";
56
import { Escaper } from "typia/lib/utils/Escaper";
@@ -11,9 +12,66 @@ export const JsonUtil = {
1112

1213
const pipe = (...fns: ((str: string) => string)[]) => (str: string) => fns.reduce((acc, fn) => fn(acc), str);
1314

14-
function parse(str: string) {
15+
function parse(
16+
str: string,
17+
parameters?: ILlmSchema.IParameters,
18+
): any {
1519
str = pipe(removeEmptyObjectPrefix, addMissingBraces, removeTrailingCommas, jsonrepair)(str);
16-
return JSON.parse(str);
20+
const output: any = JSON.parse(str);
21+
if (parameters !== undefined) {
22+
decompose(parameters, output);
23+
}
24+
return output;
25+
}
26+
27+
function decompose(
28+
parameters: ILlmSchema.IParameters,
29+
output: any,
30+
): void {
31+
if (Object.keys(parameters.properties).length !== 1) {
32+
return;
33+
}
34+
else if (typeof output !== "object" || output === null) {
35+
return;
36+
}
37+
38+
const key: string = Object.keys(parameters.properties)[0]!;
39+
const value: any = output[key];
40+
const schema: ILlmSchema = parameters.properties[key]!;
41+
if (
42+
typeof value === "string"
43+
&& isInstanceType({
44+
$defs: parameters.$defs,
45+
schema,
46+
}) === true
47+
) {
48+
try {
49+
output[key] = parse(value);
50+
}
51+
catch {}
52+
}
53+
}
54+
55+
function isInstanceType(props: {
56+
$defs: Record<string, ILlmSchema>;
57+
schema: ILlmSchema;
58+
}): boolean {
59+
if (LlmTypeChecker.isReference(props.schema)) {
60+
return isInstanceType({
61+
$defs: props.$defs,
62+
schema: props.$defs[props.schema.$ref.split("/").pop()!] ?? {},
63+
});
64+
}
65+
return LlmTypeChecker.isNull(props.schema)
66+
|| LlmTypeChecker.isObject(props.schema)
67+
|| LlmTypeChecker.isArray(props.schema)
68+
|| (
69+
LlmTypeChecker.isAnyOf(props.schema)
70+
&& props.schema.anyOf.every(s => isInstanceType({
71+
$defs: props.$defs,
72+
schema: s,
73+
}))
74+
);
1775
}
1876

1977
function stringifyValidationFailure(
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { JsonUtil } from "@agentica/core/src/utils/JsonUtil";
2+
import { TestValidator } from "@nestia/e2e";
3+
import typia from "typia";
4+
5+
export function test_json_parse_decompose_anyOf(): void {
6+
interface IInput {
7+
data: { x: number } | null;
8+
}
9+
10+
const parameters = typia.llm.parameters<IInput>();
11+
12+
// LLM이 anyOf [object, null] 타입을 문자열로 반환한 경우
13+
const input = JSON.stringify({ data: "{\"x\":10}" });
14+
const result = JsonUtil.parse(input, parameters);
15+
16+
TestValidator.equals("decompose_anyOf")(result.data)({ x: 10 });
17+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { JsonUtil } from "@agentica/core/src/utils/JsonUtil";
2+
import { TestValidator } from "@nestia/e2e";
3+
import typia from "typia";
4+
5+
export function test_json_parse_decompose_array(): void {
6+
interface IInput {
7+
items: number[];
8+
}
9+
10+
const parameters = typia.llm.parameters<IInput>();
11+
12+
// LLM이 array를 문자열로 반환한 경우
13+
const input = JSON.stringify({ items: "[1,2,3]" });
14+
const result = JsonUtil.parse(input, parameters);
15+
16+
TestValidator.equals("decompose_array")(result.items)([1, 2, 3]);
17+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { JsonUtil } from "@agentica/core/src/utils/JsonUtil";
2+
import { TestValidator } from "@nestia/e2e";
3+
import typia from "typia";
4+
5+
export function test_json_parse_decompose_object(): void {
6+
interface IInput {
7+
data: {
8+
foo: string;
9+
bar: number;
10+
};
11+
}
12+
13+
const parameters = typia.llm.parameters<IInput>();
14+
15+
// LLM이 object를 문자열로 반환한 경우
16+
const input = JSON.stringify({ data: "{\"foo\":\"hello\",\"bar\":42}" });
17+
const result = JsonUtil.parse(input, parameters);
18+
19+
TestValidator.equals("decompose_object")(result.data)({ foo: "hello", bar: 42 });
20+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { JsonUtil } from "@agentica/core/src/utils/JsonUtil";
2+
import { TestValidator } from "@nestia/e2e";
3+
import typia from "typia";
4+
5+
interface IUser {
6+
id: number;
7+
name: string;
8+
}
9+
10+
interface IInput {
11+
user: IUser;
12+
}
13+
14+
export function test_json_parse_decompose_ref(): void {
15+
const parameters = typia.llm.parameters<IInput>();
16+
17+
// LLM이 $ref로 정의된 object를 문자열로 반환한 경우
18+
const input = JSON.stringify({ user: "{\"id\":123,\"name\":\"John\"}" });
19+
const result = JsonUtil.parse(input, parameters);
20+
21+
TestValidator.equals("decompose_ref")(result.user)({ id: 123, name: "John" });
22+
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import { JsonUtil } from "@agentica/core/src/utils/JsonUtil";
2+
import { TestValidator } from "@nestia/e2e";
3+
import typia from "typia";
4+
5+
export function test_json_parse_decompose_skip(): void {
6+
test_skip_multiple_properties();
7+
test_skip_string_schema();
8+
test_skip_anyOf_with_string();
9+
}
10+
11+
// 프로퍼티가 2개 이상이면 decompose 안함
12+
function test_skip_multiple_properties(): void {
13+
interface IInput {
14+
data1: { a: number };
15+
data2: { b: number };
16+
}
17+
18+
const parameters = typia.llm.parameters<IInput>();
19+
20+
const input = JSON.stringify({
21+
data1: "{\"a\":1}",
22+
data2: "{\"b\":2}",
23+
});
24+
const result = JsonUtil.parse(input, parameters);
25+
26+
// 2개 프로퍼티이므로 decompose 안함 - 문자열 그대로 유지
27+
TestValidator.equals("skip_multi_data1")(result.data1)("{\"a\":1}");
28+
TestValidator.equals("skip_multi_data2")(result.data2)("{\"b\":2}");
29+
}
30+
31+
// 스키마가 string 타입이면 decompose 안함
32+
function test_skip_string_schema(): void {
33+
interface IInput {
34+
data: string;
35+
}
36+
37+
const parameters = typia.llm.parameters<IInput>();
38+
39+
const input = JSON.stringify({ data: "{\"foo\":\"bar\"}" });
40+
const result = JsonUtil.parse(input, parameters);
41+
42+
// string 스키마이므로 decompose 안함
43+
TestValidator.equals("skip_string")(result.data)("{\"foo\":\"bar\"}");
44+
}
45+
46+
// anyOf에 string이 포함되면 decompose 안함
47+
function test_skip_anyOf_with_string(): void {
48+
interface IInput {
49+
data: { x: number } | string;
50+
}
51+
52+
const parameters = typia.llm.parameters<IInput>();
53+
54+
const input = JSON.stringify({ data: "{\"x\":1}" });
55+
const result = JsonUtil.parse(input, parameters);
56+
57+
// anyOf에 string 포함되므로 decompose 안함
58+
TestValidator.equals("skip_anyOf_string")(result.data)("{\"x\":1}");
59+
}

0 commit comments

Comments
 (0)