@aigne/core 1.61.0-beta.3 → 1.61.0-beta.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,28 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.61.0-beta.5](https://github.com/AIGNE-io/aigne-framework/compare/core-v1.61.0-beta.4...core-v1.61.0-beta.5) (2025-09-25)
4
+
5
+
6
+ ### Features
7
+
8
+ * **core:** add automatic JSON parsing and validation for structured outputs ([#548](https://github.com/AIGNE-io/aigne-framework/issues/548)) ([9077f93](https://github.com/AIGNE-io/aigne-framework/commit/9077f93856865915aaf5e8caa5638ef0b7f05b1e))
9
+
10
+
11
+ ### Dependencies
12
+
13
+ * The following workspace dependencies were updated
14
+ * dependencies
15
+ * @aigne/observability-api bumped to 0.11.0-beta
16
+
17
+ ## [1.61.0-beta.4](https://github.com/AIGNE-io/aigne-framework/compare/core-v1.61.0-beta.3...core-v1.61.0-beta.4) (2025-09-25)
18
+
19
+
20
+ ### Dependencies
21
+
22
+ * The following workspace dependencies were updated
23
+ * dependencies
24
+ * @aigne/observability-api bumped to 0.10.5-beta
25
+
3
26
  ## [1.61.0-beta.3](https://github.com/AIGNE-io/aigne-framework/compare/core-v1.61.0-beta.2...core-v1.61.0-beta.3) (2025-09-24)
4
27
 
5
28
 
@@ -111,6 +111,15 @@ export declare abstract class ChatModel extends Model<ChatModelInput, ChatModelO
111
111
  */
112
112
  abstract process(input: ChatModelInput, options: AgentInvokeOptions): PromiseOrValue<AgentProcessResult<ChatModelOutput>>;
113
113
  protected processAgentOutput(input: ChatModelInput, output: Exclude<AgentResponse<ChatModelOutput>, AgentResponseStream<ChatModelOutput>>, options: AgentInvokeOptions): Promise<ChatModelOutput>;
114
+ protected validateJsonSchema<T>(schema: object, data: T, options?: {
115
+ safe?: false;
116
+ }): T;
117
+ protected validateJsonSchema<T>(schema: object, data: T, options: {
118
+ safe: true;
119
+ }): z.SafeParseReturnType<T, T>;
120
+ protected validateJsonSchema<T>(schema: object, data: T, options?: {
121
+ safe?: boolean;
122
+ }): T | z.SafeParseReturnType<T, T>;
114
123
  }
115
124
  /**
116
125
  * Input message format for ChatModel
@@ -35,8 +35,9 @@ var __importStar = (this && this.__importStar) || (function () {
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.chatModelOutputUsageSchema = exports.unionContentSchema = exports.textContentSchema = exports.roleSchema = exports.ChatModel = exports.StructuredOutputError = void 0;
37
37
  const index_js_1 = require("@aigne/platform-helpers/nodejs/index.js");
38
- const ajv_1 = require("ajv");
39
38
  const zod_1 = require("zod");
39
+ const zod_from_json_schema_1 = require("zod-from-json-schema");
40
+ const json_schema_js_1 = require("../utils/json-schema.js");
40
41
  const type_utils_js_1 = require("../utils/type-utils.js");
41
42
  const agent_js_1 = require("./agent.js");
42
43
  const model_js_1 = require("./model.js");
@@ -213,19 +214,6 @@ class ChatModel extends model_js_1.Model {
213
214
  * @param options Options for invoking the agent
214
215
  */
215
216
  async postprocess(input, output, options) {
216
- // Restore original tool names in the output
217
- if (output.toolCalls?.length) {
218
- const toolsMap = input._toolsMap;
219
- if (toolsMap) {
220
- for (const toolCall of output.toolCalls) {
221
- const originalTool = toolsMap[toolCall.function.name];
222
- if (!originalTool) {
223
- throw new Error(`Tool "${toolCall.function.name}" not found in tools map`);
224
- }
225
- toolCall.function.name = originalTool.function.name;
226
- }
227
- }
228
- }
229
217
  super.postprocess(input, output, options);
230
218
  const { usage } = output;
231
219
  if (usage) {
@@ -245,16 +233,38 @@ class ChatModel extends model_js_1.Model {
245
233
  }
246
234
  // Remove fields with `null` value for validation
247
235
  output = (0, type_utils_js_1.omitByDeep)(output, (value) => (0, type_utils_js_1.isNil)(value));
236
+ const toolCalls = output.toolCalls;
248
237
  if (input.responseFormat?.type === "json_schema" &&
249
238
  // NOTE: Should not validate if there are tool calls
250
- !output.toolCalls?.length) {
251
- const ajv = new ajv_1.Ajv();
252
- if (!ajv.validate(input.responseFormat.jsonSchema.schema, output.json)) {
253
- throw new StructuredOutputError(`Output JSON does not conform to the provided JSON schema: ${ajv.errorsText()}`);
239
+ !toolCalls?.length) {
240
+ output.json = this.validateJsonSchema(input.responseFormat.jsonSchema.schema, output.json);
241
+ }
242
+ // Restore original tool names in the output
243
+ if (toolCalls?.length) {
244
+ const toolsMap = input._toolsMap;
245
+ if (toolsMap) {
246
+ for (const toolCall of toolCalls) {
247
+ const originalTool = toolsMap[toolCall.function.name];
248
+ if (!originalTool) {
249
+ throw new Error(`Tool "${toolCall.function.name}" not found in tools map`);
250
+ }
251
+ toolCall.function.name = originalTool.function.name;
252
+ toolCall.function.arguments = this.validateJsonSchema(originalTool.function.parameters, toolCall.function.arguments);
253
+ }
254
254
  }
255
255
  }
256
256
  return super.processAgentOutput(input, output, options);
257
257
  }
258
+ validateJsonSchema(schema, data, options) {
259
+ const s = (0, json_schema_js_1.wrapAutoParseJsonSchema)((0, zod_from_json_schema_1.convertJsonSchemaToZod)(schema));
260
+ const r = s.safeParse(data);
261
+ if (options?.safe)
262
+ return r;
263
+ if (r.error) {
264
+ throw new StructuredOutputError(`Output JSON does not conform to the provided JSON schema: ${r.error.errors.map((i) => `${i.path}: ${i.message}`).join(", ")}`);
265
+ }
266
+ return r.data;
267
+ }
258
268
  }
259
269
  exports.ChatModel = ChatModel;
260
270
  exports.roleSchema = zod_1.z.union([
@@ -1,4 +1,4 @@
1
- import type { ZodType, z } from "zod";
1
+ import { type ZodType, z } from "zod";
2
2
  import type { Message } from "../agents/agent.js";
3
3
  export declare function outputSchemaToResponseFormatSchema(agentOutput: ZodType<Message>): Record<string, unknown>;
4
4
  export declare function parseJSON(json: string): any;
@@ -11,3 +11,4 @@ export declare function parseJSON(json: string): any;
11
11
  */
12
12
  export declare function ensureZodUnionArray<T extends z.ZodType>(union: T[]): [T, T, ...T[]];
13
13
  export declare function isZodSchema(schema: ZodType): schema is ZodType;
14
+ export declare const wrapAutoParseJsonSchema: <T extends ZodType>(schema: T) => T;
@@ -1,10 +1,13 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.wrapAutoParseJsonSchema = void 0;
3
4
  exports.outputSchemaToResponseFormatSchema = outputSchemaToResponseFormatSchema;
4
5
  exports.parseJSON = parseJSON;
5
6
  exports.ensureZodUnionArray = ensureZodUnionArray;
6
7
  exports.isZodSchema = isZodSchema;
8
+ const zod_1 = require("zod");
7
9
  const zod_to_json_schema_1 = require("zod-to-json-schema");
10
+ const json_utils_js_1 = require("./json-utils.js");
8
11
  const logger_js_1 = require("./logger.js");
9
12
  const type_utils_js_1 = require("./type-utils.js");
10
13
  function outputSchemaToResponseFormatSchema(agentOutput) {
@@ -90,3 +93,25 @@ function convertNullableToOptional(schema) {
90
93
  }
91
94
  return [schema, false];
92
95
  }
96
+ const wrapJsonParse = (schema) => {
97
+ return zod_1.z.preprocess((val) => (typeof val === "string" ? (0, json_utils_js_1.safeParseJSON)(val) : val), schema);
98
+ };
99
+ const wrapAutoParseJsonSchema = (schema) => {
100
+ if (schema instanceof zod_1.z.ZodNullable)
101
+ return zod_1.z.nullable((0, exports.wrapAutoParseJsonSchema)(schema._def.innerType));
102
+ if (schema instanceof zod_1.z.ZodOptional)
103
+ return zod_1.z.optional((0, exports.wrapAutoParseJsonSchema)(schema._def.innerType));
104
+ if (schema instanceof zod_1.z.ZodObject) {
105
+ const newSchema = schema.extend(Object.fromEntries(Object.entries(schema.shape).map(([key, value]) => [
106
+ key,
107
+ (0, exports.wrapAutoParseJsonSchema)(value),
108
+ ])));
109
+ return wrapJsonParse(newSchema);
110
+ }
111
+ if (schema instanceof zod_1.z.ZodArray) {
112
+ schema._def.type = (0, exports.wrapAutoParseJsonSchema)(schema._def.type);
113
+ return wrapJsonParse(schema);
114
+ }
115
+ return schema;
116
+ };
117
+ exports.wrapAutoParseJsonSchema = wrapAutoParseJsonSchema;
@@ -111,6 +111,15 @@ export declare abstract class ChatModel extends Model<ChatModelInput, ChatModelO
111
111
  */
112
112
  abstract process(input: ChatModelInput, options: AgentInvokeOptions): PromiseOrValue<AgentProcessResult<ChatModelOutput>>;
113
113
  protected processAgentOutput(input: ChatModelInput, output: Exclude<AgentResponse<ChatModelOutput>, AgentResponseStream<ChatModelOutput>>, options: AgentInvokeOptions): Promise<ChatModelOutput>;
114
+ protected validateJsonSchema<T>(schema: object, data: T, options?: {
115
+ safe?: false;
116
+ }): T;
117
+ protected validateJsonSchema<T>(schema: object, data: T, options: {
118
+ safe: true;
119
+ }): z.SafeParseReturnType<T, T>;
120
+ protected validateJsonSchema<T>(schema: object, data: T, options?: {
121
+ safe?: boolean;
122
+ }): T | z.SafeParseReturnType<T, T>;
114
123
  }
115
124
  /**
116
125
  * Input message format for ChatModel
@@ -1,4 +1,4 @@
1
- import type { ZodType, z } from "zod";
1
+ import { type ZodType, z } from "zod";
2
2
  import type { Message } from "../agents/agent.js";
3
3
  export declare function outputSchemaToResponseFormatSchema(agentOutput: ZodType<Message>): Record<string, unknown>;
4
4
  export declare function parseJSON(json: string): any;
@@ -11,3 +11,4 @@ export declare function parseJSON(json: string): any;
11
11
  */
12
12
  export declare function ensureZodUnionArray<T extends z.ZodType>(union: T[]): [T, T, ...T[]];
13
13
  export declare function isZodSchema(schema: ZodType): schema is ZodType;
14
+ export declare const wrapAutoParseJsonSchema: <T extends ZodType>(schema: T) => T;
@@ -111,6 +111,15 @@ export declare abstract class ChatModel extends Model<ChatModelInput, ChatModelO
111
111
  */
112
112
  abstract process(input: ChatModelInput, options: AgentInvokeOptions): PromiseOrValue<AgentProcessResult<ChatModelOutput>>;
113
113
  protected processAgentOutput(input: ChatModelInput, output: Exclude<AgentResponse<ChatModelOutput>, AgentResponseStream<ChatModelOutput>>, options: AgentInvokeOptions): Promise<ChatModelOutput>;
114
+ protected validateJsonSchema<T>(schema: object, data: T, options?: {
115
+ safe?: false;
116
+ }): T;
117
+ protected validateJsonSchema<T>(schema: object, data: T, options: {
118
+ safe: true;
119
+ }): z.SafeParseReturnType<T, T>;
120
+ protected validateJsonSchema<T>(schema: object, data: T, options?: {
121
+ safe?: boolean;
122
+ }): T | z.SafeParseReturnType<T, T>;
114
123
  }
115
124
  /**
116
125
  * Input message format for ChatModel
@@ -1,6 +1,7 @@
1
1
  import { nodejs } from "@aigne/platform-helpers/nodejs/index.js";
2
- import { Ajv } from "ajv";
3
2
  import { z } from "zod";
3
+ import { convertJsonSchemaToZod } from "zod-from-json-schema";
4
+ import { wrapAutoParseJsonSchema } from "../utils/json-schema.js";
4
5
  import { checkArguments, isNil, omitByDeep } from "../utils/type-utils.js";
5
6
  import { agentOptionsSchema, } from "./agent.js";
6
7
  import { fileContentSchema, fileUnionContentSchema, localContentSchema, Model, urlContentSchema, } from "./model.js";
@@ -176,19 +177,6 @@ export class ChatModel extends Model {
176
177
  * @param options Options for invoking the agent
177
178
  */
178
179
  async postprocess(input, output, options) {
179
- // Restore original tool names in the output
180
- if (output.toolCalls?.length) {
181
- const toolsMap = input._toolsMap;
182
- if (toolsMap) {
183
- for (const toolCall of output.toolCalls) {
184
- const originalTool = toolsMap[toolCall.function.name];
185
- if (!originalTool) {
186
- throw new Error(`Tool "${toolCall.function.name}" not found in tools map`);
187
- }
188
- toolCall.function.name = originalTool.function.name;
189
- }
190
- }
191
- }
192
180
  super.postprocess(input, output, options);
193
181
  const { usage } = output;
194
182
  if (usage) {
@@ -208,16 +196,38 @@ export class ChatModel extends Model {
208
196
  }
209
197
  // Remove fields with `null` value for validation
210
198
  output = omitByDeep(output, (value) => isNil(value));
199
+ const toolCalls = output.toolCalls;
211
200
  if (input.responseFormat?.type === "json_schema" &&
212
201
  // NOTE: Should not validate if there are tool calls
213
- !output.toolCalls?.length) {
214
- const ajv = new Ajv();
215
- if (!ajv.validate(input.responseFormat.jsonSchema.schema, output.json)) {
216
- throw new StructuredOutputError(`Output JSON does not conform to the provided JSON schema: ${ajv.errorsText()}`);
202
+ !toolCalls?.length) {
203
+ output.json = this.validateJsonSchema(input.responseFormat.jsonSchema.schema, output.json);
204
+ }
205
+ // Restore original tool names in the output
206
+ if (toolCalls?.length) {
207
+ const toolsMap = input._toolsMap;
208
+ if (toolsMap) {
209
+ for (const toolCall of toolCalls) {
210
+ const originalTool = toolsMap[toolCall.function.name];
211
+ if (!originalTool) {
212
+ throw new Error(`Tool "${toolCall.function.name}" not found in tools map`);
213
+ }
214
+ toolCall.function.name = originalTool.function.name;
215
+ toolCall.function.arguments = this.validateJsonSchema(originalTool.function.parameters, toolCall.function.arguments);
216
+ }
217
217
  }
218
218
  }
219
219
  return super.processAgentOutput(input, output, options);
220
220
  }
221
+ validateJsonSchema(schema, data, options) {
222
+ const s = wrapAutoParseJsonSchema(convertJsonSchemaToZod(schema));
223
+ const r = s.safeParse(data);
224
+ if (options?.safe)
225
+ return r;
226
+ if (r.error) {
227
+ throw new StructuredOutputError(`Output JSON does not conform to the provided JSON schema: ${r.error.errors.map((i) => `${i.path}: ${i.message}`).join(", ")}`);
228
+ }
229
+ return r.data;
230
+ }
221
231
  }
222
232
  export const roleSchema = z.union([
223
233
  z.literal("system"),
@@ -1,4 +1,4 @@
1
- import type { ZodType, z } from "zod";
1
+ import { type ZodType, z } from "zod";
2
2
  import type { Message } from "../agents/agent.js";
3
3
  export declare function outputSchemaToResponseFormatSchema(agentOutput: ZodType<Message>): Record<string, unknown>;
4
4
  export declare function parseJSON(json: string): any;
@@ -11,3 +11,4 @@ export declare function parseJSON(json: string): any;
11
11
  */
12
12
  export declare function ensureZodUnionArray<T extends z.ZodType>(union: T[]): [T, T, ...T[]];
13
13
  export declare function isZodSchema(schema: ZodType): schema is ZodType;
14
+ export declare const wrapAutoParseJsonSchema: <T extends ZodType>(schema: T) => T;
@@ -1,4 +1,6 @@
1
+ import { z } from "zod";
1
2
  import { zodToJsonSchema } from "zod-to-json-schema";
3
+ import { safeParseJSON } from "./json-utils.js";
2
4
  import { logger } from "./logger.js";
3
5
  import { isEmpty, isRecord } from "./type-utils.js";
4
6
  export function outputSchemaToResponseFormatSchema(agentOutput) {
@@ -84,3 +86,24 @@ function convertNullableToOptional(schema) {
84
86
  }
85
87
  return [schema, false];
86
88
  }
89
+ const wrapJsonParse = (schema) => {
90
+ return z.preprocess((val) => (typeof val === "string" ? safeParseJSON(val) : val), schema);
91
+ };
92
+ export const wrapAutoParseJsonSchema = (schema) => {
93
+ if (schema instanceof z.ZodNullable)
94
+ return z.nullable(wrapAutoParseJsonSchema(schema._def.innerType));
95
+ if (schema instanceof z.ZodOptional)
96
+ return z.optional(wrapAutoParseJsonSchema(schema._def.innerType));
97
+ if (schema instanceof z.ZodObject) {
98
+ const newSchema = schema.extend(Object.fromEntries(Object.entries(schema.shape).map(([key, value]) => [
99
+ key,
100
+ wrapAutoParseJsonSchema(value),
101
+ ])));
102
+ return wrapJsonParse(newSchema);
103
+ }
104
+ if (schema instanceof z.ZodArray) {
105
+ schema._def.type = wrapAutoParseJsonSchema(schema._def.type);
106
+ return wrapJsonParse(schema);
107
+ }
108
+ return schema;
109
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aigne/core",
3
- "version": "1.61.0-beta.3",
3
+ "version": "1.61.0-beta.5",
4
4
  "description": "The functional core of agentic AI",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -71,7 +71,6 @@
71
71
  "@opentelemetry/api": "^1.9.0",
72
72
  "@opentelemetry/sdk-trace-base": "^2.1.0",
73
73
  "@types/debug": "^4.1.12",
74
- "ajv": "^8.17.1",
75
74
  "camelize-ts": "^3.0.0",
76
75
  "content-type": "^1.0.5",
77
76
  "debug": "^4.4.3",
@@ -91,8 +90,9 @@
91
90
  "uuid": "^13.0.0",
92
91
  "yaml": "^2.8.1",
93
92
  "zod": "^3.25.67",
93
+ "zod-from-json-schema": "^0.0.5",
94
94
  "zod-to-json-schema": "^3.24.6",
95
- "@aigne/observability-api": "^0.10.4",
95
+ "@aigne/observability-api": "^0.11.0-beta",
96
96
  "@aigne/platform-helpers": "^0.6.3-beta"
97
97
  },
98
98
  "devDependencies": {