first commit
Security: Sync from Public / sync-from-public (push) Has been cancelled
Test: Benchmark Nightly / build (push) Has been cancelled
Test: Benchmark Nightly / Notify Cats on failure (push) Has been cancelled
CI: Python / Checks (push) Has been cancelled
Test: Evals Python / Workflow Comparison Python (push) Has been cancelled
Util: Check Docs URLs / check-docs-urls (push) Has been cancelled
Test: Visual Storybook / Cloudflare Pages (push) Has been cancelled
Test: E2E Performance / build-and-test-performance (push) Has been cancelled
Test: Workflows Nightly / Run Workflow Tests (push) Has been cancelled
Util: Cleanup CI Docker Images / Delete stale CI images (push) Has been cancelled
Test: Benchmark Destroy Env / build (push) Has been cancelled
Util: Update Node Popularity / update-popularity (push) Has been cancelled
Test: E2E Coverage Weekly / Coverage Tests (push) Has been cancelled
Security: Sync from Public / sync-from-public (push) Has been cancelled
Test: Benchmark Nightly / build (push) Has been cancelled
Test: Benchmark Nightly / Notify Cats on failure (push) Has been cancelled
CI: Python / Checks (push) Has been cancelled
Test: Evals Python / Workflow Comparison Python (push) Has been cancelled
Util: Check Docs URLs / check-docs-urls (push) Has been cancelled
Test: Visual Storybook / Cloudflare Pages (push) Has been cancelled
Test: E2E Performance / build-and-test-performance (push) Has been cancelled
Test: Workflows Nightly / Run Workflow Tests (push) Has been cancelled
Util: Cleanup CI Docker Images / Delete stale CI images (push) Has been cancelled
Test: Benchmark Destroy Env / build (push) Has been cancelled
Util: Update Node Popularity / update-popularity (push) Has been cancelled
Test: E2E Coverage Weekly / Coverage Tests (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,404 @@
|
||||
import { AIMessage, HumanMessage } from '@langchain/core/messages';
|
||||
import type { Tool } from '@langchain/core/tools';
|
||||
import { BufferWindowMemory } from '@langchain/classic/memory';
|
||||
import { z } from 'zod';
|
||||
import { zodToJsonSchema } from 'zod-to-json-schema';
|
||||
|
||||
import {
|
||||
formatToOpenAIFunction,
|
||||
formatToOpenAITool,
|
||||
formatToOpenAIAssistantTool,
|
||||
formatToOpenAIResponsesTool,
|
||||
getChatMessages,
|
||||
} from '../helpers/utils';
|
||||
|
||||
jest.mock('zod-to-json-schema', () => ({
|
||||
zodToJsonSchema: jest.fn(),
|
||||
}));
|
||||
const mockZodToJsonSchema = jest.mocked(zodToJsonSchema);
|
||||
|
||||
describe('OpenAI message history', () => {
|
||||
it('should only get a limited number of messages', async () => {
|
||||
const memory = new BufferWindowMemory({
|
||||
returnMessages: true,
|
||||
k: 2,
|
||||
});
|
||||
expect(await getChatMessages(memory)).toEqual([]);
|
||||
|
||||
await memory.saveContext(
|
||||
[new HumanMessage({ content: 'human 1' })],
|
||||
[new AIMessage({ content: 'ai 1' })],
|
||||
);
|
||||
// `k` means turns, but `getChatMessages` returns messages, so a Human and an AI message.
|
||||
expect((await getChatMessages(memory)).length).toEqual(2);
|
||||
|
||||
await memory.saveContext(
|
||||
[new HumanMessage({ content: 'human 2' })],
|
||||
[new AIMessage({ content: 'ai 2' })],
|
||||
);
|
||||
expect((await getChatMessages(memory)).length).toEqual(4);
|
||||
expect((await getChatMessages(memory)).map((msg) => msg.content)).toEqual([
|
||||
'human 1',
|
||||
'ai 1',
|
||||
'human 2',
|
||||
'ai 2',
|
||||
]);
|
||||
|
||||
// We expect this to be trimmed...
|
||||
await memory.saveContext(
|
||||
[new HumanMessage({ content: 'human 3' })],
|
||||
[new AIMessage({ content: 'ai 3' })],
|
||||
);
|
||||
expect((await getChatMessages(memory)).length).toEqual(4);
|
||||
expect((await getChatMessages(memory)).map((msg) => msg.content)).toEqual([
|
||||
'human 2',
|
||||
'ai 2',
|
||||
'human 3',
|
||||
'ai 3',
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('OpenAI formatting functions', () => {
|
||||
const createMockTool = (name: string, description: string): Tool =>
|
||||
({
|
||||
name,
|
||||
description,
|
||||
schema: z.object({}),
|
||||
func: jest.fn(),
|
||||
call: jest.fn(),
|
||||
returnDirect: false,
|
||||
verboseParsingErrors: false,
|
||||
lc_namespace: ['test'],
|
||||
lc_serializable: true,
|
||||
}) as unknown as Tool;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('formatToOpenAIFunction', () => {
|
||||
it('should format a tool to OpenAI function format', () => {
|
||||
const mockSchema = { type: 'object', properties: { name: { type: 'string' } } };
|
||||
mockZodToJsonSchema.mockReturnValue(mockSchema);
|
||||
|
||||
const tool = createMockTool('test-tool', 'A test tool');
|
||||
|
||||
const result = formatToOpenAIFunction(tool);
|
||||
|
||||
expect(result).toEqual({
|
||||
name: 'test-tool',
|
||||
description: 'A test tool',
|
||||
parameters: mockSchema,
|
||||
});
|
||||
expect(mockZodToJsonSchema).toHaveBeenCalledWith(tool.schema);
|
||||
});
|
||||
|
||||
it('should handle tool with empty description', () => {
|
||||
const mockSchema = { type: 'object' };
|
||||
mockZodToJsonSchema.mockReturnValue(mockSchema);
|
||||
|
||||
const tool = createMockTool('test-tool', '');
|
||||
|
||||
const result = formatToOpenAIFunction(tool);
|
||||
|
||||
expect(result).toEqual({
|
||||
name: 'test-tool',
|
||||
description: '',
|
||||
parameters: mockSchema,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('formatToOpenAITool', () => {
|
||||
it('should format a tool to OpenAI tool format', () => {
|
||||
const mockSchema = { type: 'object', properties: { value: { type: 'number' } } };
|
||||
mockZodToJsonSchema.mockReturnValue(mockSchema);
|
||||
|
||||
const tool = createMockTool('calculator', 'A calculator tool');
|
||||
|
||||
const result = formatToOpenAITool(tool);
|
||||
|
||||
expect(result).toEqual({
|
||||
type: 'function',
|
||||
function: {
|
||||
name: 'calculator',
|
||||
description: 'A calculator tool',
|
||||
parameters: mockSchema,
|
||||
},
|
||||
});
|
||||
expect(mockZodToJsonSchema).toHaveBeenCalledWith(tool.schema);
|
||||
});
|
||||
|
||||
it('should handle complex schema', () => {
|
||||
const mockSchema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
query: { type: 'string' },
|
||||
limit: { type: 'number' },
|
||||
options: { type: 'object' },
|
||||
},
|
||||
required: ['query'],
|
||||
};
|
||||
mockZodToJsonSchema.mockReturnValue(mockSchema);
|
||||
|
||||
const tool = createMockTool('search-tool', 'Search functionality');
|
||||
|
||||
const result = formatToOpenAITool(tool);
|
||||
|
||||
expect(result).toEqual({
|
||||
type: 'function',
|
||||
function: {
|
||||
name: 'search-tool',
|
||||
description: 'Search functionality',
|
||||
parameters: mockSchema,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('formatToOpenAIAssistantTool', () => {
|
||||
it('should format a tool to OpenAI assistant tool format', () => {
|
||||
const mockSchema = { type: 'object', properties: { message: { type: 'string' } } };
|
||||
mockZodToJsonSchema.mockReturnValue(mockSchema);
|
||||
|
||||
const tool = createMockTool('message-tool', 'Send a message');
|
||||
|
||||
const result = formatToOpenAIAssistantTool(tool);
|
||||
|
||||
expect(result).toEqual({
|
||||
type: 'function',
|
||||
function: {
|
||||
name: 'message-tool',
|
||||
description: 'Send a message',
|
||||
parameters: mockSchema,
|
||||
},
|
||||
});
|
||||
expect(mockZodToJsonSchema).toHaveBeenCalledWith(tool.schema);
|
||||
});
|
||||
|
||||
it('should handle tool with no required fields', () => {
|
||||
const mockSchema = { type: 'object', properties: {} };
|
||||
mockZodToJsonSchema.mockReturnValue(mockSchema);
|
||||
|
||||
const tool = createMockTool('simple-tool', 'A simple tool');
|
||||
|
||||
const result = formatToOpenAIAssistantTool(tool);
|
||||
|
||||
expect(result).toEqual({
|
||||
type: 'function',
|
||||
function: {
|
||||
name: 'simple-tool',
|
||||
description: 'A simple tool',
|
||||
parameters: mockSchema,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('formatToOpenAIResponsesTool', () => {
|
||||
it('should format a tool to OpenAI responses tool format with strict mode', () => {
|
||||
const mockSchema = {
|
||||
type: 'object',
|
||||
properties: { input: { type: 'string' } },
|
||||
required: ['input'],
|
||||
};
|
||||
mockZodToJsonSchema.mockReturnValue(mockSchema);
|
||||
|
||||
const tool = createMockTool('input-tool', 'Process input');
|
||||
|
||||
const result = formatToOpenAIResponsesTool(tool);
|
||||
|
||||
expect(result).toEqual({
|
||||
type: 'function',
|
||||
name: 'input-tool',
|
||||
parameters: mockSchema,
|
||||
strict: true,
|
||||
description: 'Process input',
|
||||
});
|
||||
expect(mockZodToJsonSchema).toHaveBeenCalledWith(tool.schema);
|
||||
});
|
||||
|
||||
it('should format a tool with non-strict mode when not all properties are required', () => {
|
||||
const mockSchema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
required: { type: 'string' },
|
||||
optional: { type: 'string' },
|
||||
},
|
||||
required: ['required'],
|
||||
};
|
||||
mockZodToJsonSchema.mockReturnValue(mockSchema);
|
||||
|
||||
const tool = createMockTool('mixed-tool', 'Tool with required and optional fields');
|
||||
|
||||
const result = formatToOpenAIResponsesTool(tool);
|
||||
|
||||
expect(result).toEqual({
|
||||
type: 'function',
|
||||
name: 'mixed-tool',
|
||||
parameters: mockSchema,
|
||||
strict: false,
|
||||
description: 'Tool with required and optional fields',
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle schema without required field', () => {
|
||||
const mockSchema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
required: { type: 'string' },
|
||||
optional: { type: 'string' },
|
||||
},
|
||||
required: ['required'],
|
||||
};
|
||||
mockZodToJsonSchema.mockReturnValue(mockSchema);
|
||||
|
||||
const tool = createMockTool('no-required-tool', 'Tool with no required fields');
|
||||
|
||||
const result = formatToOpenAIResponsesTool(tool);
|
||||
|
||||
expect(result).toEqual({
|
||||
type: 'function',
|
||||
name: 'no-required-tool',
|
||||
parameters: mockSchema,
|
||||
strict: false,
|
||||
description: 'Tool with no required fields',
|
||||
});
|
||||
});
|
||||
|
||||
it('should keep additionalProperties:false when strict is true', () => {
|
||||
const mockSchema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
required: { type: 'string' },
|
||||
},
|
||||
required: ['required'],
|
||||
additionalProperties: false,
|
||||
};
|
||||
mockZodToJsonSchema.mockReturnValue(mockSchema);
|
||||
|
||||
const tool = createMockTool(
|
||||
'empty-additional-properties-tool',
|
||||
'Tool with empty additional properties',
|
||||
);
|
||||
|
||||
const result = formatToOpenAIResponsesTool(tool);
|
||||
|
||||
expect(result).toEqual({
|
||||
type: 'function',
|
||||
name: 'empty-additional-properties-tool',
|
||||
parameters: { ...mockSchema, additionalProperties: false },
|
||||
strict: true,
|
||||
description: 'Tool with empty additional properties',
|
||||
});
|
||||
});
|
||||
|
||||
it('should keep additionalProperties:true when strict is true', () => {
|
||||
const mockSchema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
required: { type: 'string' },
|
||||
},
|
||||
required: ['required'],
|
||||
additionalProperties: true,
|
||||
};
|
||||
mockZodToJsonSchema.mockReturnValue(mockSchema);
|
||||
|
||||
const tool = createMockTool(
|
||||
'additional-properties-true-tool',
|
||||
'Tool with additional properties true',
|
||||
);
|
||||
|
||||
const result = formatToOpenAIResponsesTool(tool);
|
||||
|
||||
expect(result).toEqual({
|
||||
type: 'function',
|
||||
name: 'additional-properties-true-tool',
|
||||
parameters: { ...mockSchema, additionalProperties: true },
|
||||
strict: true,
|
||||
description: 'Tool with additional properties true',
|
||||
});
|
||||
});
|
||||
|
||||
it('should keep not empty additionalProperties when strict is true', () => {
|
||||
const mockSchema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
required: { type: 'string' },
|
||||
},
|
||||
required: ['required'],
|
||||
additionalProperties: { type: 'string' },
|
||||
};
|
||||
mockZodToJsonSchema.mockReturnValue(mockSchema);
|
||||
|
||||
const tool = createMockTool(
|
||||
'not-empty-additional-properties-tool',
|
||||
'Tool with not empty additional properties',
|
||||
);
|
||||
|
||||
const result = formatToOpenAIResponsesTool(tool);
|
||||
|
||||
expect(result).toEqual({
|
||||
type: 'function',
|
||||
name: 'not-empty-additional-properties-tool',
|
||||
parameters: { ...mockSchema, additionalProperties: { type: 'string' } },
|
||||
strict: true,
|
||||
description: 'Tool with not empty additional properties',
|
||||
});
|
||||
});
|
||||
|
||||
it('should change empty additionalProperties to false when strict is true', () => {
|
||||
const mockSchema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
required: { type: 'string' },
|
||||
},
|
||||
required: ['required'],
|
||||
additionalProperties: {},
|
||||
};
|
||||
mockZodToJsonSchema.mockReturnValue(mockSchema);
|
||||
|
||||
const tool = createMockTool(
|
||||
'empty-additional-properties-tool',
|
||||
'Tool with empty additional properties',
|
||||
);
|
||||
|
||||
const result = formatToOpenAIResponsesTool(tool);
|
||||
|
||||
expect(result).toEqual({
|
||||
type: 'function',
|
||||
name: 'empty-additional-properties-tool',
|
||||
parameters: { ...mockSchema, additionalProperties: false },
|
||||
strict: true,
|
||||
description: 'Tool with empty additional properties',
|
||||
});
|
||||
});
|
||||
|
||||
it('should keep empty additionalProperties when strict is false', () => {
|
||||
const mockSchema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
optional: { type: 'string' },
|
||||
},
|
||||
additionalProperties: {},
|
||||
};
|
||||
mockZodToJsonSchema.mockReturnValue(mockSchema);
|
||||
|
||||
const tool = createMockTool(
|
||||
'empty-additional-properties-tool',
|
||||
'Tool with empty additional properties',
|
||||
);
|
||||
|
||||
const result = formatToOpenAIResponsesTool(tool);
|
||||
|
||||
expect(result).toEqual({
|
||||
type: 'function',
|
||||
name: 'empty-additional-properties-tool',
|
||||
parameters: { ...mockSchema, additionalProperties: {} },
|
||||
strict: false,
|
||||
description: 'Tool with empty additional properties',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user