mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-01-27 20:56:12 +01:00
🎉 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:
parent
affcebd48c
commit
1a815f5e19
189 changed files with 5056 additions and 1815 deletions
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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) =>
|
||||
|
|
|
|||
|
|
@ -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. */
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ export enum QueryKeys {
|
|||
fileConfig = 'fileConfig',
|
||||
tools = 'tools',
|
||||
toolAuth = 'toolAuth',
|
||||
toolCalls = 'toolCalls',
|
||||
agentTools = 'agentTools',
|
||||
actions = 'actions',
|
||||
assistantDocs = 'assistantDocs',
|
||||
|
|
|
|||
|
|
@ -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(() => ({}));
|
||||
|
|
|
|||
|
|
@ -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[];
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
>;
|
||||
|
|
|
|||
|
|
@ -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[];
|
||||
|
|
|
|||
467
packages/data-provider/src/zod.spec.ts
Normal file
467
packages/data-provider/src/zod.spec.ts
Normal 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');
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
66
packages/data-provider/src/zod.ts
Normal file
66
packages/data-provider/src/zod.ts
Normal 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;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue