Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/fair-memes-open.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@voltagent/core": minor
---

feat(core): support valibot and other schema libraries via xsschema, not just zod
1 change: 1 addition & 0 deletions packages/anthropic-ai/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
"dependencies": {
"@anthropic-ai/sdk": "^0.40.0",
"@voltagent/core": "^0.1.22",
"xsschema": "0.3.0-beta.2",
"zod": "3.24.2"
},
"devDependencies": {
Expand Down
33 changes: 21 additions & 12 deletions packages/anthropic-ai/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import type {
StreamTextOptions,
VoltAgentError,
} from "@voltagent/core";
import type { z } from "zod";
import * as xsschema from "xsschema";
import type {
AnthropicMessage,
AnthropicProviderOptions,
Expand All @@ -29,7 +29,6 @@ import {
handleStepFinish,
processContent,
processResponseContent,
zodToJsonSchema,
} from "./utils";

export class AnthropicProvider implements LLMProvider<string> {
Expand Down Expand Up @@ -80,10 +79,20 @@ export class AnthropicProvider implements LLMProvider<string> {
}

toTool(tool: BaseTool): AnthropicTool {
const jsonSchema = xsschema.toJsonSchemaSync(tool.parameters);
if (jsonSchema.type !== "object") {
throw new Error("Tool parameters must be an object");
}

return {
name: tool.name,
description: tool.description,
input_schema: zodToJsonSchema(tool.parameters),
input_schema: {
...jsonSchema,
// Already checked that the type is object above
type: "object",
properties: jsonSchema.properties ?? {},
},
};
}

Expand Down Expand Up @@ -243,11 +252,11 @@ export class AnthropicProvider implements LLMProvider<string> {
}
}

async generateObject<TSchema extends z.ZodType>(
async generateObject<TSchema extends xsschema.Schema>(
options: GenerateObjectOptions<string, TSchema>,
): Promise<ProviderObjectResponse<any, z.infer<TSchema>>> {
): Promise<ProviderObjectResponse<any, xsschema.Infer<TSchema>>> {
const { temperature = 0.2, maxTokens = 1024, topP, stopSequences } = options.provider || {};
const JsonSchema = zodToJsonSchema(options.schema);
const JsonSchema = await xsschema.toJsonSchema(options.schema);
const systemPrompt = `${getSystemMessage(options.messages)}. Response Schema: ${JSON.stringify(JsonSchema)}. You must return the response in valid JSON Format with proper schema, nothing else `;

const anthropicMessages = this.getAnthropicMessages(options.messages);
Expand Down Expand Up @@ -283,7 +292,7 @@ export class AnthropicProvider implements LLMProvider<string> {
throw new Error(`The JSON returned by Anthropic API is not valid \n ${err}`);
}

const parsedResult = options.schema.safeParse(parsedObject);
const parsedResult = await xsschema.validate(parsedObject, options.schema);
if (!parsedResult.success) {
throw new Error(
`the response doesn't match the specified schema: ${parsedResult.error.message}`,
Expand Down Expand Up @@ -320,12 +329,12 @@ export class AnthropicProvider implements LLMProvider<string> {
}
}

async streamObject<TSchema extends z.ZodType>(
async streamObject<TSchema extends xsschema.Schema>(
options: StreamObjectOptions<string, TSchema>,
): Promise<ProviderObjectStreamResponse<any, z.infer<TSchema>>> {
): Promise<ProviderObjectStreamResponse<any, xsschema.Infer<TSchema>>> {
try {
const anthropicMessages = this.getAnthropicMessages(options.messages);
const JsonSchema = zodToJsonSchema(options.schema);
const JsonSchema = await xsschema.toJsonSchema(options.schema);
const systemPrompt = `${getSystemMessage(options.messages)}. Response Schema: ${JSON.stringify(JsonSchema)}. You must return the response in valid JSON Format with proper schema, nothing else `;
const { temperature = 0.2, maxTokens = 1024, topP, stopSequences } = options.provider || {};

Expand Down Expand Up @@ -353,7 +362,7 @@ export class AnthropicProvider implements LLMProvider<string> {
// Try to parse partial JSON as it comes in
try {
const partialObject = JSON.parse(accumulatedText);
const parseResult = options.schema.safeParse(partialObject);
const parseResult = await xsschema.validate(partialObject, options.schema);

if (parseResult.success) {
controller.enqueue(parseResult.data);
Expand All @@ -366,7 +375,7 @@ export class AnthropicProvider implements LLMProvider<string> {
if (chunk.type === "message_stop") {
try {
const parsedObject = JSON.parse(accumulatedText);
const parsedResult = options.schema.safeParse(parsedObject);
const parsedResult = await xsschema.validate(parsedObject, options.schema);

if (parsedResult.success) {
controller.enqueue(parsedResult.data);
Expand Down
106 changes: 0 additions & 106 deletions packages/anthropic-ai/src/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import type {
StepWithContent,
VoltAgentError,
} from "@voltagent/core";
import { z } from "zod";

/**
* Processes text content into a text content block
Expand Down Expand Up @@ -171,111 +170,6 @@ export function processContentPart(part: any): ContentBlockParam | null {
return null;
}

/**
* Converts a Zod schema to JSON Schema format that Anthropic expects
* @param {z.ZodType<any>} schema - The Zod schema to convert
* @returns {Object} A JSON Schema object with type, properties, and required fields
* @throws {Error} If the schema is not a Zod object
*/
export function zodToJsonSchema(schema: z.ZodType<any>): {
type: "object";
properties: Record<string, unknown>;
required?: string[];
} {
// Check if it's a ZodObject by checking for the typeName property
if (
schema &&
typeof schema === "object" &&
"_def" in schema &&
schema._def &&
typeof schema._def === "object" &&
"typeName" in schema._def &&
schema._def.typeName === "ZodObject"
) {
// Use a safer type assertion approach
const def = schema._def as unknown as { shape: () => Record<string, z.ZodTypeAny> };
const shape = def.shape();
const properties: Record<string, unknown> = {};
const required: string[] = [];

for (const [key, value] of Object.entries(shape)) {
const fieldSchema = convertZodField(value as z.ZodTypeAny);
properties[key] = fieldSchema;

// Check if the field is required
if (!(value instanceof z.ZodOptional)) {
required.push(key);
}
}

return {
type: "object" as const,
properties,
...(required.length > 0 ? { required } : {}),
};
}

throw new Error("Root schema must be a Zod object");
}

/**
* Helper function to create a base schema with type and optional description
* @param {z.ZodType} field - The Zod field to extract description from
* @param {string} type - The type string to use
* @returns {Object} Schema object with type and optional description
*/
function getBaseSchema(field: z.ZodType, type: string) {
return {
type,
...(field.description ? { description: field.description } : {}),
};
}

/**
* Helper function to handle primitive type fields
* @param {z.ZodTypeAny} field - The Zod field to process
* @param {string} type - The type string to use
* @returns {Object} Schema object with type and optional description
*/
function handlePrimitiveType(field: z.ZodTypeAny, type: string) {
return getBaseSchema(field, type);
}

/**
* Converts a Zod field to a JSON Schema field
* @param {z.ZodTypeAny} zodField - The Zod field to convert
* @returns {any} The JSON Schema representation of the field
*/
export function convertZodField(zodField: z.ZodTypeAny): any {
if (zodField instanceof z.ZodString) {
return handlePrimitiveType(zodField, "string");
}
if (zodField instanceof z.ZodNumber) {
return handlePrimitiveType(zodField, "number");
}
if (zodField instanceof z.ZodBoolean) {
return handlePrimitiveType(zodField, "boolean");
}
if (zodField instanceof z.ZodArray) {
return {
type: "array",
items: convertZodField(zodField.element),
...(zodField.description ? { description: zodField.description } : {}),
};
}
if (zodField instanceof z.ZodEnum) {
return {
type: "string",
enum: zodField._def.values,
...(zodField.description ? { description: zodField.description } : {}),
};
}
if (zodField instanceof z.ZodOptional) {
return convertZodField(zodField.unwrap());
}
return { type: "string" };
}

/**
* Creates a response object from Anthropic's response
* @param {Message} response - The response from Anthropic
Expand Down
1 change: 1 addition & 0 deletions packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
"uuid": "^9.0.1",
"ws": "^8.18.1",
"zod": "3.24.2",
"xsschema": "0.3.0-beta.2",
"zod-from-json-schema": "^0.0.5"
},
"devDependencies": {
Expand Down
19 changes: 12 additions & 7 deletions packages/core/src/agent/index.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
// @ts-ignore - To prevent errors when loading Jest mocks
import { z } from "zod";
import type * as xsschema from "xsschema";
import { AgentEventEmitter } from "../events";
import type { MemoryMessage, Memory } from "../memory/types";
import { AgentRegistry } from "../server/registry";
Expand Down Expand Up @@ -245,11 +246,13 @@ class MockProvider implements LLMProvider<MockModelType> {
};
}

async generateObject<T extends z.ZodType>(options: {
async generateObject<T extends xsschema.Schema>(options: {
messages: BaseMessage[];
model: MockModelType;
schema: T;
}): Promise<ProviderObjectResponse<MockGenerateObjectResult<z.infer<T>>, z.infer<T>>> {
}): Promise<
ProviderObjectResponse<MockGenerateObjectResult<xsschema.Infer<T>>, xsschema.Infer<T>>
> {
this.generateObjectCalls++;
this.lastMessages = options.messages;

Expand All @@ -258,7 +261,7 @@ class MockProvider implements LLMProvider<MockModelType> {
name: "John Doe",
age: 30,
hobbies: ["reading", "gaming"],
} as z.infer<T>,
} as xsschema.Infer<T>,
};

return {
Expand All @@ -273,11 +276,13 @@ class MockProvider implements LLMProvider<MockModelType> {
};
}

async streamObject<T extends z.ZodType>(options: {
async streamObject<T extends xsschema.Schema>(options: {
messages: BaseMessage[];
model: MockModelType;
schema: T;
}): Promise<ProviderObjectStreamResponse<MockStreamObjectResult<z.infer<T>>, z.infer<T>>> {
}): Promise<
ProviderObjectStreamResponse<MockStreamObjectResult<xsschema.Infer<T>>, xsschema.Infer<T>>
> {
this.streamObjectCalls++;
this.lastMessages = options.messages;

Expand All @@ -291,9 +296,9 @@ class MockProvider implements LLMProvider<MockModelType> {
},
});

const partialObjectStream = new ReadableStream<Partial<z.infer<T>>>({
const partialObjectStream = new ReadableStream<Partial<xsschema.Infer<T>>>({
start(controller) {
controller.enqueue({ name: "John" } as Partial<z.infer<T>>);
controller.enqueue({ name: "John" } as Partial<xsschema.Infer<T>>);
controller.close();
},
});
Expand Down
Loading
Loading