🎉 feat: Code Interpreter API and Agents Release (#4860)

* feat: Code Interpreter API & File Search Agent Uploads

chore: add back code files

wip: first pass, abstract key dialog

refactor: influence checkbox on key changes

refactor: update localization keys for 'execute code' to 'run code'

wip: run code button

refactor: add throwError parameter to loadAuthValues and getUserPluginAuthValue functions

feat: first pass, API tool calling

fix: handle missing toolId in callTool function and return 404 for non-existent tools

feat: show code outputs

fix: improve error handling in callTool function and log errors

fix: handle potential null value for filepath in attachment destructuring

fix: normalize language before rendering and prevent null return

fix: add loading indicator in RunCode component while executing code

feat: add support for conditional code execution in Markdown components

feat: attachments

refactor: remove bash

fix: pass abort signal to graph/run

refactor: debounce and rate limit tool call

refactor: increase debounce delay for execute function

feat: set code output attachments

feat: image attachments

refactor: apply message context

refactor: pass `partIndex`

feat: toolCall schema/model/methods

feat: block indexing

feat: get tool calls

chore: imports

chore: typing

chore: condense type imports

feat: get tool calls

fix: block indexing

chore: typing

refactor: update tool calls mapping to support multiple results

fix: add unique key to nav link for rendering

wip: first pass, tool call results

refactor: update query cache from successful tool call mutation

style: improve result switcher styling

chore: note on using \`.toObject()\`

feat: add agent_id field to conversation schema

chore: typing

refactor: rename agentMap to agentsMap for consistency

feat: Agent Name as chat input placeholder

chore: bump agents

📦 chore: update @langchain dependencies to latest versions to match agents package

📦 chore: update @librechat/agents dependency to version 1.8.0

fix: Aborting agent stream removes sender; fix(bedrock): completion removes preset name label

refactor: remove direct file parameter to use req.file, add `processAgentFileUpload` for image uploads

feat: upload menu

feat: prime message_file resources

feat: implement conversation access validation in chat route

refactor: remove file parameter from processFileUpload and use req.file instead

feat: add savedMessageIds set to track saved message IDs in BaseClient, to prevent unnecessary double-write to db

feat: prevent duplicate message saves by checking savedMessageIds in AgentController

refactor: skip legacy RAG API handling for agents

feat: add files field to convoSchema

refactor: update request type annotations from Express.Request to ServerRequest in file processing functions

feat: track conversation files

fix: resendFiles, addPreviousAttachments handling

feat: add ID validation for session_id and file_id in download route

feat: entity_id for code file uploads/downloads

fix: code file edge cases

feat: delete related tool calls

feat: add stream rate handling for LLM configuration

feat: enhance system content with attached file information

fix: improve error logging in resource priming function

* WIP: PoC, sequential agents

WIP: PoC Sequential Agents, first pass content data + bump agents package

fix: package-lock

WIP: PoC, o1 support, refactor bufferString

feat: convertJsonSchemaToZod

fix: form issues and schema defining erroneous model

fix: max length issue on agent form instructions, limit conversation messages to sequential agents

feat: add abort signal support to createRun function and AgentClient

feat: PoC, hide prior sequential agent steps

fix: update parameter naming from config to metadata in event handlers for clarity, add model to usage data

refactor: use only last contentData, track model for usage data

chore: bump agents package

fix: content parts issue

refactor: filter contentParts to include tool calls and relevant indices

feat: show function calls

refactor: filter context messages to exclude tool calls when no tools are available to the agent

fix: ensure tool call content is not undefined in formatMessages

feat: add agent_id field to conversationPreset schema

feat: hide sequential agents

feat: increase upload toast duration to 10 seconds

* refactor: tool context handling & update Code API Key Dialog

feat: toolContextMap

chore: skipSpecs -> useSpecs

ci: fix handleTools tests

feat: API Key Dialog

* feat: Agent Permissions Admin Controls

feat: replace label with button for prompt permission toggle

feat: update agent permissions

feat: enable experimental agents and streamline capability configuration

feat: implement access control for agents and enhance endpoint menu items

feat: add welcome message for agent selection in localization

feat: add agents permission to access control and update version to 0.7.57

* fix: update types in useAssistantListMap and useMentions hooks for better null handling

* feat: mention agents

* fix: agent tool resource race conditions when deleting agent tool resource files

* feat: add error handling for code execution with user feedback

* refactor: rename AdminControls to AdminSettings for clarity

* style: add gap to button in AdminSettings for improved layout

* refactor: separate agent query hooks and check access to enable fetching

* fix: remove unused provider from agent initialization options, creates issue with custom endpoints

* refactor: remove redundant/deprecated modelOptions from AgentClient processes

* chore: update @librechat/agents to version 1.8.5 in package.json and package-lock.json

* fix: minor styling issues + agent panel uniformity

* fix: agent edge cases when set endpoint is no longer defined

* refactor: remove unused cleanup function call from AppService

* fix: update link in ApiKeyDialog to point to pricing page

* fix: improve type handling and layout calculations in SidePanel component

* fix: add missing localization string for agent selection in SidePanel

* chore: form styling and localizations for upload filesearch/code interpreter

* fix: model selection placeholder logic in AgentConfig component

* style: agent capabilities

* fix: add localization for provider selection and improve dropdown styling in ModelPanel

* refactor: use gpt-4o-mini > gpt-3.5-turbo

* fix: agents configuration for loadDefaultInterface and update related tests

* feat: DALLE Agents support
This commit is contained in:
Danny Avila 2024-12-04 15:48:13 -05:00 committed by GitHub
parent affcebd48c
commit 1a815f5e19
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
189 changed files with 5056 additions and 1815 deletions

View file

@ -1,6 +1,6 @@
{
"name": "librechat-data-provider",
"version": "0.7.56",
"version": "0.7.57",
"description": "data services for librechat apps",
"main": "dist/index.js",
"module": "dist/index.es.js",

View file

@ -207,8 +207,8 @@ export const getAllPromptGroups = () => `${prompts()}/all`;
/* Roles */
export const roles = () => '/api/roles';
export const getRole = (roleName: string) => `${roles()}/${roleName.toLowerCase()}`;
export const updatePromptPermissions = (roleName: string) =>
`${roles()}/${roleName.toLowerCase()}/prompts`;
export const updatePromptPermissions = (roleName: string) => `${getRole(roleName)}/prompts`;
export const updateAgentPermissions = (roleName: string) => `${getRole(roleName)}/agents`;
/* Conversation Tags */
export const conversationTags = (tag?: string) =>

View file

@ -140,6 +140,8 @@ export enum Capabilities {
}
export enum AgentCapabilities {
hide_sequential_outputs = 'hide_sequential_outputs',
end_after_tools = 'end_after_tools',
execute_code = 'execute_code',
file_search = 'file_search',
actions = 'actions',
@ -475,6 +477,7 @@ export const configSchema = z.object({
bookmarks: z.boolean().optional(),
presets: z.boolean().optional(),
prompts: z.boolean().optional(),
agents: z.boolean().optional(),
})
.default({
endpointsMenu: true,
@ -485,6 +488,7 @@ export const configSchema = z.object({
multiConvo: true,
bookmarks: true,
prompts: true,
agents: true,
}),
fileStrategy: fileSourceSchema.default(FileSources.local),
registration: z
@ -932,6 +936,10 @@ export enum ViolationTypes {
* Verify Conversation Access violation.
*/
CONVO_ACCESS = 'convo_access',
/**
* Tool Call Limit Violation.
*/
TOOL_CALL_LIMIT = 'tool_call_limit',
}
/**
@ -1086,6 +1094,8 @@ export enum Constants {
NO_PARENT = '00000000-0000-0000-0000-000000000000',
/** Standard value for the initial conversationId before a request is sent */
NEW_CONVO = 'new',
/** Standard value for the conversationId used for search queries */
SEARCH = 'search',
/** Fixed, encoded domain length for Azure OpenAI Assistants Function name parsing. */
ENCODED_DOMAIN_LENGTH = 10,
/** Identifier for using current_model in multi-model requests. */

View file

@ -314,6 +314,30 @@ export const getVerifyAgentToolAuth = (
);
};
export const callTool = <T extends m.ToolId>({
toolId,
toolParams,
}: {
toolId: T;
toolParams: m.ToolParams<T>;
}): Promise<m.ToolCallResponse> => {
return request.post(
endpoints.agents({
path: `tools/${toolId}/call`,
}),
toolParams,
);
};
export const getToolCalls = (params: q.GetToolCallParams): Promise<q.ToolCallResults> => {
return request.get(
endpoints.agents({
path: 'tools/calls',
options: params,
}),
);
};
/* Files */
export const getFiles = (): Promise<f.TFile[]> => {
@ -669,10 +693,16 @@ export function getRole(roleName: string): Promise<r.TRole> {
export function updatePromptPermissions(
variables: m.UpdatePromptPermVars,
): Promise<m.UpdatePromptPermResponse> {
): Promise<m.UpdatePermResponse> {
return request.put(endpoints.updatePromptPermissions(variables.roleName), variables.updates);
}
export function updateAgentPermissions(
variables: m.UpdateAgentPermVars,
): Promise<m.UpdatePermResponse> {
return request.put(endpoints.updateAgentPermissions(variables.roleName), variables.updates);
}
/* Tags */
export function getConversationTags(): Promise<t.TConversationTagsResponse> {
return request.get(endpoints.conversationTags());

View file

@ -7,6 +7,7 @@ export * from './file-config';
export * from './artifacts';
/* schema helpers */
export * from './parsers';
export * from './zod';
/* custom/dynamic configurations */
export * from './generate';
export * from './models';

View file

@ -26,6 +26,7 @@ export enum QueryKeys {
fileConfig = 'fileConfig',
tools = 'tools',
toolAuth = 'toolAuth',
toolCalls = 'toolCalls',
agentTools = 'agentTools',
actions = 'actions',
assistantDocs = 'assistantDocs',

View file

@ -1096,13 +1096,14 @@ export type TBanner = z.infer<typeof tBannerSchema>;
export const compactAgentsSchema = tConversationSchema
.pick({
model: true,
agent_id: true,
instructions: true,
additional_instructions: true,
spec: true,
// model: true,
iconURL: true,
greeting: true,
spec: true,
agent_id: true,
resendFiles: true,
instructions: true,
additional_instructions: true,
})
.transform(removeNullishValues)
.catch(() => ({}));

View file

@ -1,6 +1,7 @@
/* eslint-disable @typescript-eslint/no-namespace */
import { StepTypes, ContentTypes, ToolCallTypes } from './runs';
import type { FunctionToolCall } from './assistants';
import type { TAttachment } from 'src/schemas';
export namespace Agents {
export type MessageType = 'human' | 'ai' | 'generic' | 'system' | 'function' | 'tool' | 'remove';
@ -218,3 +219,14 @@ export namespace Agents {
}
export type ContentType = ContentTypes.TEXT | ContentTypes.IMAGE_URL | string;
}
export type ToolCallResult = {
user: string;
toolId: string;
result?: unknown;
messageId: string;
partIndex?: number;
blockIndex?: number;
conversationId: string;
attachments?: TAttachment[];
};

View file

@ -147,6 +147,7 @@ export type File = {
export type AgentParameterValue = number | null;
export type AgentModelParameters = {
model?: string;
temperature: AgentParameterValue;
max_context_tokens: AgentParameterValue;
max_output_tokens: AgentParameterValue;
@ -165,6 +166,10 @@ export interface ExecuteCodeResource {
* There can be a maximum of 20 files associated with the tool.
*/
file_ids?: Array<string>;
/**
* A list of files already fetched.
*/
files?: Array<TFile>;
}
export interface AgentFileSearchResource {
@ -178,6 +183,10 @@ export interface AgentFileSearchResource {
* To be used before vector stores are implemented.
*/
file_ids?: Array<string>;
/**
* A list of files already fetched.
*/
files?: Array<TFile>;
}
export type Agent = {
@ -201,6 +210,9 @@ export type Agent = {
conversation_starters?: string[];
isCollaborative?: boolean;
tool_resources?: AgentToolResources;
agent_ids?: string[];
end_after_tools?: boolean;
hide_sequential_outputs?: boolean;
};
export type TAgentsMap = Record<string, Agent | undefined>;
@ -215,7 +227,7 @@ export type AgentCreateParams = {
provider: AgentProvider;
model: string | null;
model_parameters: AgentModelParameters;
};
} & Pick<Agent, 'agent_ids' | 'end_after_tools' | 'hide_sequential_outputs'>;
export type AgentUpdateParams = {
name?: string | null;
@ -231,7 +243,7 @@ export type AgentUpdateParams = {
projectIds?: string[];
removeProjectIds?: string[];
isCollaborative?: boolean;
};
} & Pick<Agent, 'agent_ids' | 'end_after_tools' | 'hide_sequential_outputs'>;
export type AgentListParams = {
limit?: number;

View file

@ -1,6 +1,7 @@
import * as types from '../types';
import * as r from '../roles';
import {
Tools,
Assistant,
AssistantCreateParams,
AssistantUpdateParams,
@ -222,18 +223,29 @@ export type RegistrationOptions = MutationOptions<
types.TError
>;
export type UpdatePromptPermVars = {
export type UpdatePermVars<T> = {
roleName: string;
updates: Partial<r.TPromptPermissions>;
updates: Partial<T>;
};
export type UpdatePromptPermResponse = r.TRole;
export type UpdatePromptPermVars = UpdatePermVars<r.TPromptPermissions>;
export type UpdateAgentPermVars = UpdatePermVars<r.TAgentPermissions>;
export type UpdatePermResponse = r.TRole;
export type UpdatePromptPermOptions = MutationOptions<
UpdatePromptPermResponse,
UpdatePermResponse,
UpdatePromptPermVars,
unknown,
types.TError
types.TError | null | undefined
>;
export type UpdateAgentPermOptions = MutationOptions<
UpdatePermResponse,
UpdateAgentPermVars,
unknown,
types.TError | null | undefined
>;
export type UpdateConversationTagOptions = MutationOptions<
@ -251,3 +263,24 @@ export type AcceptTermsMutationOptions = MutationOptions<
/* Tools */
export type UpdatePluginAuthOptions = MutationOptions<types.TUser, types.TUpdateUserPlugins>;
export type ToolParamsMap = {
[Tools.execute_code]: {
lang: string;
code: string;
};
};
export type ToolId = keyof ToolParamsMap;
export type ToolParams<T extends ToolId> = ToolParamsMap[T] & {
messageId: string;
partIndex?: number;
blockIndex?: number;
conversationId: string;
};
export type ToolCallResponse = { result: unknown; attachments?: types.TAttachment[] };
export type ToolCallMutationOptions<T extends ToolId> = MutationOptions<
ToolCallResponse,
ToolParams<T>
>;

View file

@ -1,4 +1,5 @@
import type { InfiniteData } from '@tanstack/react-query';
import type * as a from '../types/agents';
import type * as s from '../schemas';
import type * as t from '../types';
@ -75,3 +76,6 @@ export type ConversationTagsResponse = s.TConversationTag[];
export type VerifyToolAuthParams = { toolId: string };
export type VerifyToolAuthResponse = { authenticated: boolean; message?: string | s.AuthType };
export type GetToolCallParams = { conversationId: string };
export type ToolCallResults = a.ToolCallResult[];

View file

@ -0,0 +1,467 @@
/* eslint-disable jest/no-conditional-expect */
/* eslint-disable @typescript-eslint/no-explicit-any */
// zod.spec.ts
import { z } from 'zod';
import { convertJsonSchemaToZod } from './zod';
import type { JsonSchemaType } from './zod';
describe('convertJsonSchemaToZod', () => {
describe('primitive types', () => {
it('should convert string schema', () => {
const schema: JsonSchemaType = {
type: 'string',
};
const zodSchema = convertJsonSchemaToZod(schema);
expect(zodSchema.parse('test')).toBe('test');
expect(() => zodSchema.parse(123)).toThrow();
});
it('should convert string enum schema', () => {
const schema: JsonSchemaType = {
type: 'string',
enum: ['foo', 'bar', 'baz'],
};
const zodSchema = convertJsonSchemaToZod(schema);
expect(zodSchema.parse('foo')).toBe('foo');
expect(() => zodSchema.parse('invalid')).toThrow();
});
it('should convert number schema', () => {
const schema: JsonSchemaType = {
type: 'number',
};
const zodSchema = convertJsonSchemaToZod(schema);
expect(zodSchema.parse(123)).toBe(123);
expect(() => zodSchema.parse('123')).toThrow();
});
it('should convert boolean schema', () => {
const schema: JsonSchemaType = {
type: 'boolean',
};
const zodSchema = convertJsonSchemaToZod(schema);
expect(zodSchema.parse(true)).toBe(true);
expect(() => zodSchema.parse('true')).toThrow();
});
});
describe('array types', () => {
it('should convert array of strings schema', () => {
const schema: JsonSchemaType = {
type: 'array',
items: { type: 'string' },
};
const zodSchema = convertJsonSchemaToZod(schema);
expect(zodSchema.parse(['a', 'b', 'c'])).toEqual(['a', 'b', 'c']);
expect(() => zodSchema.parse(['a', 123, 'c'])).toThrow();
});
it('should convert array of numbers schema', () => {
const schema: JsonSchemaType = {
type: 'array',
items: { type: 'number' },
};
const zodSchema = convertJsonSchemaToZod(schema);
expect(zodSchema.parse([1, 2, 3])).toEqual([1, 2, 3]);
expect(() => zodSchema.parse([1, '2', 3])).toThrow();
});
});
describe('object types', () => {
it('should convert simple object schema', () => {
const schema: JsonSchemaType = {
type: 'object',
properties: {
name: { type: 'string' },
age: { type: 'number' },
},
};
const zodSchema = convertJsonSchemaToZod(schema);
expect(zodSchema.parse({ name: 'John', age: 30 })).toEqual({ name: 'John', age: 30 });
expect(() => zodSchema.parse({ name: 123, age: 30 })).toThrow();
});
it('should handle required fields', () => {
const schema: JsonSchemaType = {
type: 'object',
properties: {
name: { type: 'string' },
age: { type: 'number' },
},
required: ['name'],
};
const zodSchema = convertJsonSchemaToZod(schema);
expect(zodSchema.parse({ name: 'John' })).toEqual({ name: 'John' });
expect(() => zodSchema.parse({})).toThrow();
});
it('should handle nested objects', () => {
const schema: JsonSchemaType = {
type: 'object',
properties: {
user: {
type: 'object',
properties: {
name: { type: 'string' },
age: { type: 'number' },
},
required: ['name'],
},
},
required: ['user'],
};
const zodSchema = convertJsonSchemaToZod(schema);
expect(zodSchema.parse({ user: { name: 'John', age: 30 } })).toEqual({
user: { name: 'John', age: 30 },
});
expect(() => zodSchema.parse({ user: { age: 30 } })).toThrow();
});
it('should handle objects with arrays', () => {
const schema: JsonSchemaType = {
type: 'object',
properties: {
names: {
type: 'array',
items: { type: 'string' },
},
},
};
const zodSchema = convertJsonSchemaToZod(schema);
expect(zodSchema.parse({ names: ['John', 'Jane'] })).toEqual({ names: ['John', 'Jane'] });
expect(() => zodSchema.parse({ names: ['John', 123] })).toThrow();
});
});
describe('edge cases', () => {
it('should handle empty object schema', () => {
const schema: JsonSchemaType = {
type: 'object',
properties: {},
};
const zodSchema = convertJsonSchemaToZod(schema);
expect(zodSchema.parse({})).toEqual({});
});
it('should handle unknown types as unknown', () => {
const schema = {
type: 'invalid',
} as unknown as JsonSchemaType;
const zodSchema = convertJsonSchemaToZod(schema);
expect(zodSchema.parse('anything')).toBe('anything');
expect(zodSchema.parse(123)).toBe(123);
});
it('should handle empty enum arrays as regular strings', () => {
const schema: JsonSchemaType = {
type: 'string',
enum: [],
};
const zodSchema = convertJsonSchemaToZod(schema);
expect(zodSchema.parse('test')).toBe('test');
});
});
describe('complex schemas', () => {
it('should handle complex nested schema', () => {
const schema: JsonSchemaType = {
type: 'object',
properties: {
id: { type: 'number' },
user: {
type: 'object',
properties: {
name: { type: 'string' },
roles: {
type: 'array',
items: {
type: 'object',
properties: {
name: { type: 'string' },
permissions: {
type: 'array',
items: {
type: 'string',
enum: ['read', 'write', 'admin'],
},
},
},
required: ['name', 'permissions'],
},
},
},
required: ['name', 'roles'],
},
},
required: ['id', 'user'],
};
const zodSchema = convertJsonSchemaToZod(schema);
const validData = {
id: 1,
user: {
name: 'John',
roles: [
{
name: 'moderator',
permissions: ['read', 'write'],
},
],
},
};
expect(zodSchema.parse(validData)).toEqual(validData);
expect(() =>
zodSchema.parse({
id: 1,
user: {
name: 'John',
roles: [
{
name: 'moderator',
permissions: ['invalid'],
},
],
},
}),
).toThrow();
});
});
// zod.spec.ts
describe('schema descriptions', () => {
it('should preserve top-level description', () => {
const schema: JsonSchemaType = {
type: 'object',
description: 'A test schema description',
properties: {
name: { type: 'string' },
},
};
const zodSchema = convertJsonSchemaToZod(schema);
expect(zodSchema.description).toBe('A test schema description');
});
it('should preserve field descriptions', () => {
const schema: JsonSchemaType = {
type: 'object',
properties: {
name: {
type: 'string',
description: 'The user\'s name',
},
age: {
type: 'number',
description: 'The user\'s age',
},
},
};
const zodSchema = convertJsonSchemaToZod(schema);
const shape = (zodSchema as z.ZodObject<any>).shape;
expect(shape.name.description).toBe('The user\'s name');
expect(shape.age.description).toBe('The user\'s age');
});
it('should preserve descriptions in nested objects', () => {
const schema: JsonSchemaType = {
type: 'object',
description: 'User record',
properties: {
user: {
type: 'object',
description: 'User details',
properties: {
name: {
type: 'string',
description: 'The user\'s name',
},
settings: {
type: 'object',
description: 'User preferences',
properties: {
theme: {
type: 'string',
description: 'UI theme preference',
enum: ['light', 'dark'],
},
},
},
},
},
},
};
const zodSchema = convertJsonSchemaToZod(schema);
// Type assertions for better type safety
const shape = zodSchema instanceof z.ZodObject ? zodSchema.shape : {};
expect(zodSchema.description).toBe('User record');
if ('user' in shape) {
expect(shape.user.description).toBe('User details');
const userShape = shape.user instanceof z.ZodObject ? shape.user.shape : {};
if ('name' in userShape && 'settings' in userShape) {
expect(userShape.name.description).toBe('The user\'s name');
expect(userShape.settings.description).toBe('User preferences');
const settingsShape =
userShape.settings instanceof z.ZodObject ? userShape.settings.shape : {};
if ('theme' in settingsShape) {
expect(settingsShape.theme.description).toBe('UI theme preference');
}
}
}
});
it('should preserve descriptions in arrays', () => {
const schema: JsonSchemaType = {
type: 'object',
properties: {
tags: {
type: 'array',
description: 'User tags',
items: {
type: 'string',
description: 'Individual tag',
},
},
scores: {
type: 'array',
description: 'Test scores',
items: {
type: 'number',
description: 'Individual score',
},
},
},
};
const zodSchema = convertJsonSchemaToZod(schema);
const shape = (zodSchema as z.ZodObject<any>).shape;
expect(shape.tags.description).toBe('User tags');
expect(shape.scores.description).toBe('Test scores');
});
it('should preserve descriptions in enums', () => {
const schema: JsonSchemaType = {
type: 'object',
properties: {
role: {
type: 'string',
description: 'User role in the system',
enum: ['admin', 'user', 'guest'],
},
status: {
type: 'string',
description: 'Account status',
enum: ['active', 'suspended', 'deleted'],
},
},
};
const zodSchema = convertJsonSchemaToZod(schema);
const shape = (zodSchema as z.ZodObject<any>).shape;
expect(shape.role.description).toBe('User role in the system');
expect(shape.status.description).toBe('Account status');
});
it('should preserve descriptions in a complex schema', () => {
const schema: JsonSchemaType = {
type: 'object',
description: 'User profile configuration',
properties: {
basicInfo: {
type: 'object',
description: 'Basic user information',
properties: {
name: {
type: 'string',
description: 'Full name of the user',
},
age: {
type: 'number',
description: 'User age in years',
},
},
required: ['name'],
},
preferences: {
type: 'object',
description: 'User preferences',
properties: {
notifications: {
type: 'array',
description: 'Notification settings',
items: {
type: 'object',
description: 'Individual notification preference',
properties: {
type: {
type: 'string',
description: 'Type of notification',
enum: ['email', 'sms', 'push'],
},
enabled: {
type: 'boolean',
description: 'Whether this notification is enabled',
},
},
},
},
theme: {
type: 'string',
description: 'UI theme preference',
enum: ['light', 'dark', 'system'],
},
},
},
},
};
const zodSchema = convertJsonSchemaToZod(schema);
// Test top-level description
expect(zodSchema.description).toBe('User profile configuration');
const shape = zodSchema instanceof z.ZodObject ? zodSchema.shape : {};
// Test basic info descriptions
if ('basicInfo' in shape) {
expect(shape.basicInfo.description).toBe('Basic user information');
const basicInfoShape = shape.basicInfo instanceof z.ZodObject ? shape.basicInfo.shape : {};
if ('name' in basicInfoShape && 'age' in basicInfoShape) {
expect(basicInfoShape.name.description).toBe('Full name of the user');
expect(basicInfoShape.age.description).toBe('User age in years');
}
}
// Test preferences descriptions
if ('preferences' in shape) {
expect(shape.preferences.description).toBe('User preferences');
const preferencesShape =
shape.preferences instanceof z.ZodObject ? shape.preferences.shape : {};
if ('notifications' in preferencesShape && 'theme' in preferencesShape) {
expect(preferencesShape.notifications.description).toBe('Notification settings');
expect(preferencesShape.theme.description).toBe('UI theme preference');
}
}
});
});
});

View file

@ -0,0 +1,66 @@
import { z } from 'zod';
export type JsonSchemaType = {
type: 'string' | 'number' | 'boolean' | 'array' | 'object';
enum?: string[];
items?: JsonSchemaType;
properties?: Record<string, JsonSchemaType>;
required?: string[];
description?: string;
};
export function convertJsonSchemaToZod(schema: JsonSchemaType): z.ZodType {
let zodSchema: z.ZodType;
// Handle primitive types
if (schema.type === 'string') {
if (Array.isArray(schema.enum) && schema.enum.length > 0) {
const [first, ...rest] = schema.enum;
zodSchema = z.enum([first, ...rest] as [string, ...string[]]);
} else {
zodSchema = z.string();
}
} else if (schema.type === 'number') {
zodSchema = z.number();
} else if (schema.type === 'boolean') {
zodSchema = z.boolean();
} else if (schema.type === 'array' && schema.items !== undefined) {
const itemSchema = convertJsonSchemaToZod(schema.items);
zodSchema = z.array(itemSchema);
} else if (schema.type === 'object') {
const shape: Record<string, z.ZodType> = {};
const properties = schema.properties ?? {};
for (const [key, value] of Object.entries(properties)) {
let fieldSchema = convertJsonSchemaToZod(value);
if (value.description != null && value.description !== '') {
fieldSchema = fieldSchema.describe(value.description);
}
shape[key] = fieldSchema;
}
let objectSchema = z.object(shape);
if (Array.isArray(schema.required) && schema.required.length > 0) {
const partial = Object.fromEntries(
Object.entries(shape).map(([key, value]) => [
key,
schema.required?.includes(key) === true ? value : value.optional(),
]),
);
objectSchema = z.object(partial);
} else {
objectSchema = objectSchema.partial();
}
zodSchema = objectSchema;
} else {
zodSchema = z.unknown();
}
// Add description if present
if (schema.description != null && schema.description !== '') {
zodSchema = zodSchema.describe(schema.description);
}
return zodSchema;
}