feat: fix JSON schema parsing in @agentic/core

pull/714/head
Travis Fischer 2025-06-29 05:42:43 -05:00
rodzic 938be099df
commit 66f8c1e900
12 zmienionych plików z 92 dodań i 66 usunięć

Wyświetl plik

@ -1,5 +1,5 @@
import type { AdminDeployment, Tool } from '@agentic/platform-types'
import { assert } from '@agentic/platform-core'
import { assert, HttpError } from '@agentic/platform-core'
import type { GatewayHonoContext } from './types'
import { cfValidateJsonSchema } from './cf-validate-json-schema'
@ -14,6 +14,7 @@ export async function getToolArgsFromRequest(
deployment: AdminDeployment
}
): Promise<Record<string, any>> {
const logger = ctx.get('logger')
const request = ctx.req.raw
assert(
deployment.origin.type !== 'raw',
@ -50,17 +51,29 @@ export async function getToolArgsFromRequest(
try {
incomingRequestArgsRaw = (await request.json()) as Record<string, any>
} catch {
// If the request body is not JSON or malformed, ignore it for now.
// TODO: need to improve on this logic.
} catch (err) {
// Error if the request body is not JSON or is malformed.
logger.error('Error parsing incoming request body', request, err)
throw new HttpError({
message: 'Invalid request body json',
statusCode: 400,
cause: err
})
}
// console.log(
// 'incomingRequestArgsRaw',
// typeof incomingRequestArgsRaw,
// request.headers,
// incomingRequestArgsRaw
// )
// TODO: Proper support for empty params with POST requests
assert(incomingRequestArgsRaw, 400, 'Invalid empty request body')
assert(
typeof incomingRequestArgsRaw === 'object',
400,
'Invalid request body'
`Invalid request body: expected type "object", received type "${typeof incomingRequestArgsRaw}"`
)
assert(!Array.isArray(incomingRequestArgsRaw), 400, 'Invalid request body')
return incomingRequestArgsRaw

Wyświetl plik

@ -41,8 +41,6 @@ async function main() {
})
}
console.log()
{
// Second call to OpenAI to generate a text response
const res = await openai.responses.create({

Wyświetl plik

@ -40,8 +40,6 @@ async function main() {
})
}
console.log()
{
// Second call to OpenAI to generate a text response
const res = await openai.chat.completions.create({
@ -51,7 +49,7 @@ async function main() {
tools: searchTool.functions.toolSpecs
})
const message = res.choices?.[0]?.message
console.log(message)
console.log(message?.content)
}
}

Wyświetl plik

@ -183,20 +183,26 @@ function getSchema(parameters: any[], type: string) {
...paramSchema,
...handleNullableSchema(param.schema)
}
if ('examples' in param) {
paramSchema.examples = getExamples(param.examples)
}
schema.properties[param.name] = paramSchema
} else {
if ('examples' in paramSchema) {
paramSchema.examples = getExamples(paramSchema.examples)
}
schema.properties[param.name] = param.nullable
? handleNullable(paramSchema)
: paramSchema
}
}
// TODO: support openai strict mode by default (all params must be required,
// and optional params without defaults must be nullable)
schema.required = getRequiredParams(params)
}

Wyświetl plik

@ -63,6 +63,9 @@ export class AgenticToolClient extends AIFunctionsProvider {
name: tool.name,
description: tool.description ?? '',
inputSchema: createJsonSchema(tool.inputSchema),
// TODO: we should make sure all agentic tools support OpenAI strict
// mode by default.
strict: false,
execute: async (json) => {
return ky
.post(

Wyświetl plik

@ -476,7 +476,7 @@ catalogs:
version: 3.25.67
zod-to-json-schema:
specifier: ^3.24.5
version: 3.24.5
version: 3.24.6
zod-validation-error:
specifier: ^3.5.2
version: 3.5.2
@ -1429,7 +1429,7 @@ importers:
version: 5.1.0
zod-to-json-schema:
specifier: 'catalog:'
version: 3.24.5(zod@3.25.67)
version: 3.24.6(zod@3.25.67)
packages/validators:
dependencies:
@ -1473,6 +1473,9 @@ importers:
ky:
specifier: 'catalog:'
version: 1.8.1
openai-zod-to-json-schema:
specifier: ^1.1.1
version: 1.1.1(zod@3.25.67)
p-throttle:
specifier: 'catalog:'
version: 6.2.0
@ -1482,9 +1485,6 @@ importers:
zod:
specifier: 'catalog:'
version: 3.25.67
zod-to-json-schema:
specifier: 'catalog:'
version: 3.24.5(zod@3.25.67)
zod-validation-error:
specifier: 'catalog:'
version: 3.5.2(zod@3.25.67)
@ -6338,11 +6338,6 @@ packages:
resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==}
engines: {node: '>=8'}
browserslist@4.25.0:
resolution: {integrity: sha512-PJ8gYKeS5e/whHBh8xrwYK+dAvEj7JXtz6uTucnMRB8OiGTsKccFekoRrjajPBHV8oOY+2tI4uxeceSimKwMFA==}
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
hasBin: true
browserslist@4.25.1:
resolution: {integrity: sha512-KGj0KoOMXLpSNkkEI6Z6mShmQy0bc1I+T7K9N81k4WWMrfz+6fQ6es80B/YLAeRoKvjYE1YSHHOW1qe9xIVzHw==}
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
@ -6436,9 +6431,6 @@ packages:
peerDependencies:
three: '>=0.126.1'
caniuse-lite@1.0.30001723:
resolution: {integrity: sha512-1R/elMjtehrFejxwmexeXAtae5UO9iSyFn6G/I806CYC/BLyyBk1EPhrKBkWhy6wM6Xnm47dSJQec+tLJ39WHw==}
caniuse-lite@1.0.30001726:
resolution: {integrity: sha512-VQAUIUzBiZ/UnlM28fSp2CRF3ivUn1BWEvxMcVTNwpw91Py1pGbPIyIKtd+tzct9C3ouceCVdGAXxZOpZAsgdw==}
@ -7109,9 +7101,6 @@ packages:
ee-first@1.1.1:
resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==}
electron-to-chromium@1.5.167:
resolution: {integrity: sha512-LxcRvnYO5ez2bMOFpbuuVuAI5QNeY1ncVytE/KXaL6ZNfzX1yPlAO0nSOyIHx2fVAuUprMqPs/TdVhUFZy7SIQ==}
electron-to-chromium@1.5.177:
resolution: {integrity: sha512-7EH2G59nLsEMj97fpDuvVcYi6lwTcM1xuWw3PssD8xzboAW7zj7iB3COEEEATUfjLHrs5uKBLQT03V/8URx06g==}
@ -9514,8 +9503,8 @@ packages:
resolution: {integrity: sha512-8EcOGJk/JXFaoGjeFM53Z3zBnwOpKtZeu5X0wts67WqA1PTnsmwRgUw9aGAsQ5V6cuTfJUv282h1ypFgDGPDSA==}
engines: {node: '>=18'}
openai-zod-to-json-schema@1.0.3:
resolution: {integrity: sha512-CFU+KtOmX1dk2nPCZcGYgbrI3YLJJgMSehx1mLbH1A2fsRmZevHzMau6vFIhtkCpHWkGQ3ossA4a0OzVHlGrkw==}
openai-zod-to-json-schema@1.1.1:
resolution: {integrity: sha512-WIsQn2aXqqhRKVoDAQ7UG2W7K6q4FuJL7sn6lrj4bIHC6bbTYNFDfIw10yWCW3GX/zP2Psvubcmcb2NOCnSzsA==}
engines: {node: '>=18'}
peerDependencies:
zod: ^3.23.8
@ -11644,11 +11633,6 @@ packages:
zod-from-json-schema@0.0.5:
resolution: {integrity: sha512-zYEoo86M1qpA1Pq6329oSyHLS785z/mTwfr9V1Xf/ZLhuuBGaMlDGu/pDVGVUe4H4oa1EFgWZT53DP0U3oT9CQ==}
zod-to-json-schema@3.24.5:
resolution: {integrity: sha512-/AuWwMP+YqiPbsJx5D6TfgRTc4kTLjsh5SOcd4bLsfUg2RcEXrFMJl1DGgdHy2aCfsIA/cr/1JM0xcB2GZji8g==}
peerDependencies:
zod: ^3.24.1
zod-to-json-schema@3.24.6:
resolution: {integrity: sha512-h/z3PKvcTcTetyjl1fkj79MHNEjm+HpD6NXheWjzOekY7kV+lwDYnHw+ivHkijnCSMz1yJaWBD9vu/Fcmk+vEg==}
peerDependencies:
@ -11713,7 +11697,7 @@ snapshots:
delay: 6.0.0
jsonrepair: 3.12.0
ky: 1.8.1
openai-zod-to-json-schema: 1.0.3(zod@3.25.67)
openai-zod-to-json-schema: 1.1.1(zod@3.25.67)
p-throttle: 6.2.0
type-fest: 4.41.0
zod: 3.25.67
@ -16690,14 +16674,13 @@ snapshots:
'@types/pg-pool@2.0.6':
dependencies:
'@types/pg': 8.6.1
'@types/pg': 8.15.4
'@types/pg@8.15.4':
dependencies:
'@types/node': 24.0.7
pg-protocol: 1.10.3
pg-types: 2.2.0
optional: true
'@types/pg@8.6.1':
dependencies:
@ -17165,8 +17148,8 @@ snapshots:
autoprefixer@10.4.21(postcss@8.5.6):
dependencies:
browserslist: 4.25.0
caniuse-lite: 1.0.30001723
browserslist: 4.25.1
caniuse-lite: 1.0.30001726
fraction.js: 4.3.7
normalize-range: 0.1.2
picocolors: 1.1.1
@ -17300,13 +17283,6 @@ snapshots:
dependencies:
fill-range: 7.1.1
browserslist@4.25.0:
dependencies:
caniuse-lite: 1.0.30001723
electron-to-chromium: 1.5.167
node-releases: 2.0.19
update-browserslist-db: 1.1.3(browserslist@4.25.0)
browserslist@4.25.1:
dependencies:
caniuse-lite: 1.0.30001726
@ -17417,8 +17393,6 @@ snapshots:
dependencies:
three: 0.177.0
caniuse-lite@1.0.30001723: {}
caniuse-lite@1.0.30001726: {}
cannon-es-debugger@1.0.0(cannon-es@0.20.0)(three@0.177.0)(typescript@5.8.3):
@ -17954,8 +17928,6 @@ snapshots:
ee-first@1.1.1: {}
electron-to-chromium@1.5.167: {}
electron-to-chromium@1.5.177: {}
email-validator@2.0.4: {}
@ -20778,7 +20750,7 @@ snapshots:
'@swc/counter': 0.1.3
'@swc/helpers': 0.5.15
busboy: 1.6.0
caniuse-lite: 1.0.30001723
caniuse-lite: 1.0.30001726
postcss: 8.4.31
react: 19.1.0
react-dom: 19.1.0(react@19.1.0)
@ -20983,7 +20955,7 @@ snapshots:
dependencies:
ky: 1.8.1
openai-zod-to-json-schema@1.0.3(zod@3.25.67):
openai-zod-to-json-schema@1.1.1(zod@3.25.67):
dependencies:
zod: 3.25.67
@ -23074,12 +23046,6 @@ snapshots:
unpipe@1.0.0: {}
update-browserslist-db@1.1.3(browserslist@4.25.0):
dependencies:
browserslist: 4.25.0
escalade: 3.2.0
picocolors: 1.1.1
update-browserslist-db@1.1.3(browserslist@4.25.1):
dependencies:
browserslist: 4.25.1
@ -23473,10 +23439,6 @@ snapshots:
dependencies:
zod: 3.25.67
zod-to-json-schema@3.24.5(zod@3.25.67):
dependencies:
zod: 3.25.67
zod-to-json-schema@3.24.6(zod@3.25.67):
dependencies:
zod: 3.25.67

Wyświetl plik

@ -0,0 +1,2 @@
../.. | WARN `node_modules` is present. Lockfile only installation will make it out-of-date
../.. | Progress: resolved 1, reused 0, downloaded 0, added 0

Wyświetl plik

@ -35,7 +35,7 @@
"delay": "catalog:",
"jsonrepair": "catalog:",
"ky": "catalog:",
"zod-to-json-schema": "catalog:",
"openai-zod-to-json-schema": "^1.1.1",
"p-throttle": "catalog:",
"type-fest": "catalog:",
"zod-validation-error": "catalog:"

Wyświetl plik

@ -114,7 +114,7 @@ export function createAIFunction<
const args = input.function_call?.arguments
assert(
args,
`Missing required function_call.arguments for function ${name}`
`Missing required function_call.arguments for function "${name}"`
)
return inputAgenticSchema.parse(args)
}

Wyświetl plik

@ -1,4 +1,5 @@
import type { z } from 'zod'
import { jsonrepair } from 'jsonrepair'
import type * as types from './types'
import { parseStructuredOutput } from './parse-structured-output'
@ -102,7 +103,7 @@ export function asZodOrJsonSchema<TData>(
export function createJsonSchema<TData = unknown>(
jsonSchema: types.JSONSchema,
{
parse = (value) => value as TData,
parse,
safeParse,
source
}: {
@ -111,6 +112,15 @@ export function createJsonSchema<TData = unknown>(
source?: any
} = {}
): AgenticSchema<TData> {
parse ??= (value: unknown) => {
if (typeof value === 'string') {
value = JSON.parse(jsonrepair(value))
}
// TODO: use `cfValidateJsonSchema` from `@agentic/json-schema` here.
return value as TData
}
safeParse ??= (value: unknown) => {
try {
const result = parse(value)

Wyświetl plik

@ -60,4 +60,38 @@ describe('zodToJsonSchema', () => {
}
})
})
test('handles optional properties in strict mode', () => {
const params = zodToJsonSchema(
z.object({
name: z.string().optional(),
age: z.number().optional().default(10)
}),
{
strict: true
}
)
expect(params).toEqual({
additionalProperties: false,
type: 'object',
required: ['name', 'age'],
properties: {
name: {
anyOf: [
{
type: 'string'
},
{
type: 'null'
}
]
},
age: {
type: 'number',
default: 10
}
}
})
})
})

Wyświetl plik

@ -1,5 +1,5 @@
import type { z } from 'zod'
import { zodToJsonSchema as zodToJsonSchemaImpl } from 'zod-to-json-schema'
import { zodToJsonSchema as zodToJsonSchemaImpl } from 'openai-zod-to-json-schema'
import type * as types from './types'
import { omit } from './utils'
@ -16,7 +16,7 @@ export function zodToJsonSchema(
return omit(
zodToJsonSchemaImpl(schema, {
$refStrategy: 'none',
target: strict ? 'openAi' : undefined
openaiStrictMode: strict
}),
'$schema',
'default',