feat: add support for JSON Schema in AIFunction inputSchema in addition to zod schemas

pull/700/head
Travis Fischer 2025-03-24 00:46:06 +08:00
rodzic a9e0389bf3
commit 2db62b0ca2
18 zmienionych plików z 687 dodań i 70 usunięć

Wyświetl plik

@ -56,11 +56,13 @@
"group": "Tools",
"pages": [
"tools/apollo",
"tools/arxiv",
"tools/bing",
"tools/calculator",
"tools/clearbit",
"tools/dexa",
"tools/diffbot",
"tools/duck-duck-go",
"tools/e2b",
"tools/exa",
"tools/firecrawl",
@ -70,6 +72,7 @@
"tools/jina",
"tools/leadmagic",
"tools/midjourney",
"tools/mcp",
"tools/novu",
"tools/people-data-labs",
"tools/perigon",

Wyświetl plik

@ -9,9 +9,11 @@
},
"dependencies": {
"@agentic/ai-sdk": "workspace:*",
"@agentic/mcp": "workspace:*",
"@agentic/weather": "workspace:*",
"@ai-sdk/openai": "catalog:",
"ai": "catalog:",
"exit-hook": "^4.0.0",
"openai": "catalog:",
"zod": "catalog:"
},

Wyświetl plik

@ -1,5 +1,10 @@
import { type AIFunctionLike, AIFunctionSet } from '@agentic/core'
import { tool } from 'ai'
import {
type AIFunctionLike,
AIFunctionSet,
asSchema,
isZodSchema
} from '@agentic/core'
import { jsonSchema, tool } from 'ai'
/**
* Converts a set of Agentic stdlib AI functions to an object compatible with
@ -13,7 +18,9 @@ export function createAISDKTools(...aiFunctionLikeTools: AIFunctionLike[]) {
fn.spec.name,
tool({
description: fn.spec.description,
parameters: fn.inputSchema,
parameters: isZodSchema(fn.inputSchema)
? fn.inputSchema
: jsonSchema(asSchema(fn.inputSchema).jsonSchema),
execute: fn.execute
})
])

Wyświetl plik

@ -4,7 +4,9 @@ import { z } from 'zod'
import { createAIFunction } from './create-ai-function'
import { type Msg } from './message'
const fullName = createAIFunction(
// TODO: Add tests for passing JSON schema directly.
const fullNameAIFunction = createAIFunction(
{
name: 'fullName',
description: 'Returns the full name of a person.',
@ -20,11 +22,11 @@ const fullName = createAIFunction(
describe('createAIFunction()', () => {
test('exposes OpenAI function calling spec', () => {
expect(fullName.spec.name).toEqual('fullName')
expect(fullName.spec.description).toEqual(
expect(fullNameAIFunction.spec.name).toEqual('fullName')
expect(fullNameAIFunction.spec.description).toEqual(
'Returns the full name of a person.'
)
expect(fullName.spec.parameters).toEqual({
expect(fullNameAIFunction.spec.parameters).toEqual({
properties: {
first: { type: 'string' },
last: { type: 'string' }
@ -36,9 +38,9 @@ describe('createAIFunction()', () => {
})
test('executes the function with JSON string', async () => {
expect(await fullName('{"first": "John", "last": "Doe"}')).toEqual(
'John Doe'
)
expect(
await fullNameAIFunction('{"first": "John", "last": "Doe"}')
).toEqual('John Doe')
})
test('executes the function with OpenAI Message', async () => {
@ -51,6 +53,6 @@ describe('createAIFunction()', () => {
}
}
expect(await fullName(message)).toEqual('Jane Smith')
expect(await fullNameAIFunction(message)).toEqual('Jane Smith')
})
})

Wyświetl plik

@ -1,9 +1,6 @@
import type { z } from 'zod'
import type * as types from './types'
import { parseStructuredOutput } from './parse-structured-output'
import { asSchema } from './schema'
import { assert } from './utils'
import { zodToJsonSchema } from './zod-to-json-schema'
/**
* Create a function meant to be used with OpenAI tool or function calling.
@ -14,7 +11,10 @@ import { zodToJsonSchema } from './zod-to-json-schema'
* The `spec` property of the returned function is the spec for adding the
* function to the OpenAI API `functions` property.
*/
export function createAIFunction<InputSchema extends z.ZodObject<any>, Output>(
export function createAIFunction<
InputSchema extends types.AIFunctionInputSchema,
Output
>(
spec: {
/** Name of the function. */
name: string
@ -29,7 +29,9 @@ export function createAIFunction<InputSchema extends z.ZodObject<any>, Output>(
strict?: boolean
},
/** Implementation of the function to call with the parsed arguments. */
implementation: (params: z.infer<InputSchema>) => types.MaybePromise<Output>
implementation: (
params: types.inferInput<InputSchema>
) => types.MaybePromise<Output>
): types.AIFunction<InputSchema, Output> {
assert(spec.name, 'createAIFunction missing required "spec.name"')
assert(
@ -42,17 +44,22 @@ export function createAIFunction<InputSchema extends z.ZodObject<any>, Output>(
'createAIFunction "implementation" must be a function'
)
const strict = !!spec.strict
const inputSchema = asSchema(spec.inputSchema, { strict })
/** Parse the arguments string, optionally reading from a message. */
const parseInput = (input: string | types.Msg) => {
const parseInput = (
input: string | types.Msg
): types.inferInput<InputSchema> => {
if (typeof input === 'string') {
return parseStructuredOutput(input, spec.inputSchema)
return inputSchema.parse(input)
} else {
const args = input.function_call?.arguments
assert(
args,
`Missing required function_call.arguments for function ${spec.name}`
)
return parseStructuredOutput(args, spec.inputSchema)
return inputSchema.parse(args)
}
}
@ -71,19 +78,19 @@ export function createAIFunction<InputSchema extends z.ZodObject<any>, Output>(
writable: false
})
const strict = !!spec.strict
aiFunction.inputSchema = spec.inputSchema
aiFunction.parseInput = parseInput
aiFunction.spec = {
name: spec.name,
description: spec.description?.trim() ?? '',
parameters: zodToJsonSchema(spec.inputSchema, { strict }),
parameters: inputSchema.jsonSchema,
type: 'function',
strict
}
aiFunction.execute = (
params: z.infer<InputSchema>
params: types.inferInput<InputSchema>
): types.MaybePromise<Output> => {
return implementation(params)
}

Wyświetl plik

@ -1,5 +1,3 @@
import type { z } from 'zod'
import type * as types from './types'
import { AIFunctionSet } from './ai-function-set'
import { createAIFunction } from './create-ai-function'
@ -8,7 +6,7 @@ import { assert } from './utils'
export interface PrivateAIFunctionMetadata {
name: string
description: string
inputSchema: z.AnyZodObject
inputSchema: types.AIFunctionInputSchema
methodName: string
strict?: boolean
}
@ -35,7 +33,7 @@ if (typeof Symbol === 'function' && Symbol.metadata) {
}
export abstract class AIFunctionsProvider {
private _functions?: AIFunctionSet
protected _functions?: AIFunctionSet
/**
* An `AIFunctionSet` containing all of the AI-compatible functions exposed
@ -70,7 +68,7 @@ export abstract class AIFunctionsProvider {
export function aiFunction<
This extends AIFunctionsProvider,
InputSchema extends z.SomeZodObject,
InputSchema extends types.AIFunctionInputSchema,
OptionalArgs extends Array<undefined>,
Return extends types.MaybePromise<any>
>({
@ -87,14 +85,14 @@ export function aiFunction<
return (
_targetMethod: (
this: This,
input: z.infer<InputSchema>,
input: types.inferInput<InputSchema>,
...optionalArgs: OptionalArgs
) => Return,
context: ClassMethodDecoratorContext<
This,
(
this: This,
input: z.infer<InputSchema>,
input: types.inferInput<InputSchema>,
...optionalArgs: OptionalArgs
) => Return
>

Wyświetl plik

@ -0,0 +1,18 @@
import { expect, test } from 'vitest'
import { z } from 'zod'
import { asSchema, createJsonSchema, isZodSchema } from './schema'
test('isZodSchema', () => {
expect(isZodSchema(z.object({}))).toBe(true)
expect(isZodSchema({})).toBe(false)
})
test('asSchema', () => {
expect(asSchema(z.object({})).jsonSchema).toEqual({
type: 'object',
properties: {},
additionalProperties: false
})
expect(asSchema(createJsonSchema({})).jsonSchema).toEqual({})
})

Wyświetl plik

@ -39,6 +39,11 @@ export type Schema<TData = unknown> = {
* Schema type for inference.
*/
_type: TData
/**
* Source Zod schema if this object was created from a Zod schema.
*/
_source?: any
}
export function isSchema(value: unknown): value is Schema {
@ -56,10 +61,8 @@ export function isZodSchema(value: unknown): value is z.ZodType {
return (
typeof value === 'object' &&
value !== null &&
'_type' in value &&
'_output' in value &&
'_input' in value &&
'_def' in value &&
'~standard' in value &&
'parse' in value &&
'safeParse' in value
)
@ -73,16 +76,25 @@ export function asSchema<TData>(
}
/**
* Create a schema from a JSON Schema.
* Create a Schema from a JSON Schema.
*
* All `AIFunction` input schemas accept either a Zod schema or a custom JSON
* Schema. Use this function to wrap JSON schemas for use with `AIFunction`.
*
* Note that JSON Schemas are not validated by default, so you have to pass
* in an optional `parse` function (using `ajv`, for instance) if you'd like to
* validate them at runtime.
*/
export function createSchema<TData = unknown>(
export function createJsonSchema<TData = unknown>(
jsonSchema: types.JSONSchema,
{
parse = (value) => value as TData,
safeParse
safeParse,
source
}: {
parse?: types.ParseFn<TData>
safeParse?: types.SafeParseFn<TData>
source?: any
} = {}
): Schema<TData> {
safeParse ??= (value: unknown) => {
@ -99,7 +111,8 @@ export function createSchema<TData = unknown>(
_type: undefined as TData,
jsonSchema,
parse,
safeParse
safeParse,
_source: source
}
}
@ -107,10 +120,11 @@ export function createSchemaFromZodSchema<TData>(
zodSchema: z.Schema<TData>,
opts: { strict?: boolean } = {}
): Schema<TData> {
return createSchema(zodToJsonSchema(zodSchema, opts), {
return createJsonSchema(zodToJsonSchema(zodSchema, opts), {
parse: (value) => {
return parseStructuredOutput(value, zodSchema)
}
},
source: zodSchema
})
}

Wyświetl plik

@ -4,6 +4,7 @@ import type { z } from 'zod'
import type { AIFunctionSet } from './ai-function-set'
import type { AIFunctionsProvider } from './fns'
import type { Msg } from './message'
import type { Schema } from './schema'
export type { Msg } from './message'
export type { Schema } from './schema'
@ -52,6 +53,20 @@ export interface AIToolSpec {
function: AIFunctionSpec
}
/**
* A Zod object schema or a custom schema created from a JSON schema via
* `createSchema()`.
*/
export type AIFunctionInputSchema = z.ZodObject<any> | Schema<any>
// eslint-disable-next-line @typescript-eslint/naming-convention
export type inferInput<InputSchema extends AIFunctionInputSchema> =
InputSchema extends Schema<any>
? InputSchema['_type']
: InputSchema extends z.ZodTypeAny
? z.infer<InputSchema>
: never
/** The implementation of the function, with arg parsing and validation. */
export type AIFunctionImpl<Return> = Omit<
(input: string | Msg) => MaybePromise<Return>,
@ -65,13 +80,18 @@ export type AIFunctionImpl<Return> = Omit<
* via the `.functions` property
* - `AIFunction` - Individual functions
*/
export type AIFunctionLike = AIFunctionsProvider | AIFunction | AIFunctionSet
export type AIFunctionLike =
| AIFunctionsProvider
| AIFunction<AIFunctionInputSchema>
| AIFunctionSet
/**
* A function meant to be used with LLM function calling.
*/
export interface AIFunction<
InputSchema extends z.ZodObject<any> = z.ZodObject<any>,
// TODO
// InputSchema extends AIFunctionInputSchema = z.ZodObject<any>,
InputSchema extends AIFunctionInputSchema = AIFunctionInputSchema,
Output = any
> {
/**
@ -81,11 +101,11 @@ export interface AIFunction<
*/
(input: string | Msg): MaybePromise<Output>
/** The Zod schema for the input object. */
/** The schema for the input object (zod or custom schema). */
inputSchema: InputSchema
/** Parse the function arguments from a message. */
parseInput(input: string | Msg): z.infer<InputSchema>
parseInput(input: string | Msg): inferInput<InputSchema>
/** The JSON schema function spec for the OpenAI API `functions` property. */
spec: AIFunctionSpec
@ -94,7 +114,7 @@ export interface AIFunction<
* The underlying function implementation without any arg parsing or validation.
*/
// TODO: this `any` shouldn't be necessary, but it is for `createAIFunction` results to be assignable to `AIFunctionLike`
execute: (params: z.infer<InputSchema> | any) => MaybePromise<Output>
execute: (params: inferInput<InputSchema> | any) => MaybePromise<Output>
}
export type SafeParseResult<TData> =

Wyświetl plik

@ -1,4 +1,4 @@
import { type AIFunctionLike, AIFunctionSet } from '@agentic/core'
import { type AIFunctionLike, AIFunctionSet, isZodSchema } from '@agentic/core'
import { createAIFunction } from '@dexaai/dexter'
/**
@ -10,8 +10,14 @@ export function createDexterFunctions(
) {
const fns = new AIFunctionSet(aiFunctionLikeTools)
return fns.map((fn) =>
createAIFunction(
return fns.map((fn) => {
if (!isZodSchema(fn.inputSchema)) {
throw new Error(
`Dexter tools only support Zod schemas: ${fn.spec.name} tool uses a custom JSON Schema, which is currently not supported.`
)
}
return createAIFunction(
{
name: fn.spec.name,
description: fn.spec.description,
@ -19,5 +25,5 @@ export function createDexterFunctions(
},
fn.execute
)
)
})
}

Wyświetl plik

@ -1,5 +1,10 @@
import type { Genkit } from 'genkit'
import { type AIFunctionLike, AIFunctionSet } from '@agentic/core'
import {
type AIFunctionLike,
AIFunctionSet,
asSchema,
isZodSchema
} from '@agentic/core'
import { z } from 'zod'
/**
@ -12,15 +17,22 @@ export function createGenkitTools(
) {
const fns = new AIFunctionSet(aiFunctionLikeTools)
return fns.map((fn) =>
genkit.defineTool(
return fns.map((fn) => {
const inputSchemaKey = isZodSchema(fn.inputSchema)
? ('inputSchema' as const)
: ('inputJsonSchema' as const)
return genkit.defineTool(
{
name: fn.spec.name,
description: fn.spec.description,
inputSchema: fn.inputSchema,
// TODO: This schema handling should be able to be cleaned up.
[inputSchemaKey]: isZodSchema(fn.inputSchema)
? fn.inputSchema
: asSchema(fn.inputSchema).jsonSchema,
outputSchema: z.any()
},
fn.execute
)
)
})
}

Wyświetl plik

@ -1,4 +1,4 @@
import { type AIFunctionLike, AIFunctionSet } from '@agentic/core'
import { type AIFunctionLike, AIFunctionSet, isZodSchema } from '@agentic/core'
import { createTool } from '@mastra/core/tools'
/**
@ -9,14 +9,22 @@ export function createMastraTools(...aiFunctionLikeTools: AIFunctionLike[]) {
const fns = new AIFunctionSet(aiFunctionLikeTools)
return Object.fromEntries(
fns.map((fn) => [
fn.spec.name,
createTool({
id: fn.spec.name,
description: fn.spec.description,
inputSchema: fn.inputSchema,
execute: (ctx) => fn.execute(ctx.context)
})
])
fns.map((fn) => {
if (!isZodSchema(fn.inputSchema)) {
throw new Error(
`Mastra tools only support Zod schemas: ${fn.spec.name} tool uses a custom JSON Schema, which is currently not supported.`
)
}
return [
fn.spec.name,
createTool({
id: fn.spec.name,
description: fn.spec.description,
inputSchema: fn.inputSchema,
execute: (ctx) => fn.execute(ctx.context)
})
]
})
)
}

Wyświetl plik

@ -32,12 +32,14 @@
},
"dependencies": {
"@agentic/apollo": "workspace:*",
"@agentic/arxiv": "workspace:*",
"@agentic/bing": "workspace:*",
"@agentic/calculator": "workspace:*",
"@agentic/clearbit": "workspace:*",
"@agentic/core": "workspace:*",
"@agentic/dexa": "workspace:*",
"@agentic/diffbot": "workspace:*",
"@agentic/duck-duck-go": "workspace:*",
"@agentic/e2b": "workspace:*",
"@agentic/exa": "workspace:*",
"@agentic/firecrawl": "workspace:*",
@ -48,6 +50,7 @@
"@agentic/jina": "workspace:*",
"@agentic/leadmagic": "workspace:*",
"@agentic/midjourney": "workspace:*",
"@agentic/mcp": "workspace:*",
"@agentic/novu": "workspace:*",
"@agentic/people-data-labs": "workspace:*",
"@agentic/perigon": "workspace:*",

Wyświetl plik

@ -1,9 +1,11 @@
export * from '@agentic/apollo'
export * from '@agentic/arxiv'
export * from '@agentic/bing'
export * from '@agentic/calculator'
export * from '@agentic/clearbit'
export * from '@agentic/dexa'
export * from '@agentic/diffbot'
export * from '@agentic/duck-duck-go'
export * from '@agentic/e2b'
export * from '@agentic/exa'
export * from '@agentic/firecrawl'
@ -13,6 +15,7 @@ export * from '@agentic/hacker-news'
export * from '@agentic/hunter'
export * from '@agentic/jina'
export * from '@agentic/leadmagic'
export * from '@agentic/mcp'
export * from '@agentic/midjourney'
export * from '@agentic/novu'
export * from '@agentic/people-data-labs'

Wyświetl plik

@ -1,4 +1,4 @@
import { type AIFunctionLike, AIFunctionSet } from '@agentic/core'
import { type AIFunctionLike, AIFunctionSet, isZodSchema } from '@agentic/core'
import { tool, type ToolResult } from '@xsai/tool'
/**
@ -11,13 +11,19 @@ export function createXSAITools(
const fns = new AIFunctionSet(aiFunctionLikeTools)
return Promise.all(
fns.map((fn) =>
tool({
fns.map((fn) => {
if (!isZodSchema(fn.inputSchema)) {
throw new Error(
`xsAI tools only support Standard schemas like Zod: ${fn.spec.name} tool uses a custom JSON Schema, which is currently not supported.`
)
}
return tool({
name: fn.spec.name,
description: fn.spec.description,
parameters: fn.inputSchema,
execute: fn.execute
})
)
})
)
}

Plik diff jest za duży Load Diff

Wyświetl plik

@ -23,6 +23,8 @@ catalog:
type-fest: ^4.37.0
wikibase-sdk: ^10.2.2
'@types/jsrsasign': ^10.5.15
fast-xml-parser: ^5.0.9
'@modelcontextprotocol/sdk': ^1.7.0
# vercel ai sdk
ai: ^4.1.61

Wyświetl plik

@ -133,11 +133,13 @@ Full docs are available at [agentic.so](https://agentic.so).
| Service / Tool | Package | Docs | Description |
| ------------------------------------------------------------------------ | --------------------------- | ------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [Apollo](https://docs.apollo.io) | `@agentic/apollo` | [docs](https://agentic.so/tools/apollo) | B2B person and company enrichment API. |
| [ArXiv](https://arxiv.org) | `@agentic/arxiv` | [docs](https://agentic.so/tools/arxiv) | Search for research articles. |
| [Bing](https://www.microsoft.com/en-us/bing/apis/bing-web-search-api) | `@agentic/bing` | [docs](https://agentic.so/tools/bing) | Bing web search. |
| [Calculator](https://github.com/josdejong/mathjs) | `@agentic/calculator` | [docs](https://agentic.so/tools/calculator) | Basic calculator for simple mathematical expressions. |
| [Clearbit](https://dashboard.clearbit.com/docs) | `@agentic/clearbit` | [docs](https://agentic.so/tools/clearbit) | Resolving and enriching people and company data. |
| [Dexa](https://dexa.ai) | `@agentic/dexa` | [docs](https://agentic.so/tools/dexa) | Answers questions from the world's best podcasters. |
| [Diffbot](https://docs.diffbot.com) | `@agentic/diffbot` | [docs](https://agentic.so/tools/diffbot) | Web page classification and scraping; person and company data enrichment. |
| [DuckDuckGo](https://duckduckgo.com) | `@agentic/duck-duck-go` | [docs](https://agentic.so/tools/duck-duck-go) | Privacy-focused web search API. |
| [E2B](https://e2b.dev) | `@agentic/e2b` | [docs](https://agentic.so/tools/e2b) | Hosted Python code interpreter sandbox which is really useful for data analysis, flexible code execution, and advanced reasoning on-the-fly. |
| [Exa](https://docs.exa.ai) | `@agentic/exa` | [docs](https://agentic.so/tools/exa) | Web search tailored for LLMs. |
| [Firecrawl](https://www.firecrawl.dev) | `@agentic/firecrawl` | [docs](https://agentic.so/tools/firecrawl) | Website scraping and structured data extraction. |
@ -147,6 +149,7 @@ Full docs are available at [agentic.so](https://agentic.so).
| [Jina](https://jina.ai/reader) | `@agentic/jina` | [docs](https://agentic.so/tools/jina) | URL scraper and web search. |
| [LeadMagic](https://leadmagic.io) | `@agentic/leadmagic` | [docs](https://agentic.so/tools/leadmagic) | B2B person, company, and email enrichment API. |
| [Midjourney](https://www.imagineapi.dev) | `@agentic/midjourney` | [docs](https://agentic.so/tools/midjourney) | Unofficial Midjourney client for generative images. |
| [McpTools](https://modelcontextprotocol.io) | `@agentic/mcp` | [docs](https://agentic.so/tools/mcp) | Model Context Protocol (MCP) adapter, supporting any MCP server. Use [createMcpTools](https://agentic.so/tools/mcp) to spawn or connect to an MCP server. |
| [Novu](https://novu.co) | `@agentic/novu` | [docs](https://agentic.so/tools/novu) | Sending notifications (email, SMS, in-app, push, etc). |
| [People Data Labs](https://www.peopledatalabs.com) | `@agentic/people-data-labs` | [docs](https://agentic.so/tools/people-data-labs) | People & company data (WIP). |
| [Perigon](https://www.goperigon.com/products/news-api) | `@agentic/perigon` | [docs](https://agentic.so/tools/perigon) | Real-time news API and web content data from 140,000+ sources. Structured and enriched by AI, primed for LLMs. |