UNPKG

@lobehub/chat

Version:

Lobe Chat - an open-source, high-performance chatbot framework that supports speech synthesis, multimodal, and extensible Function Call plugin system. Supports one-click free deployment of your private ChatGPT/LLM web application.

1,127 lines (971 loc) 35.1 kB
import { describe, expect, it, vi } from 'vitest'; import { ToolsEngine } from '../ToolsEngine'; import type { LobeToolManifest } from '../types'; // Mock manifest schemas for testing const mockWebBrowsingManifest: LobeToolManifest = { api: [ { description: 'Search the web', name: 'search', parameters: { type: 'object', properties: { query: { type: 'string', description: 'Search query' }, }, required: ['query'], }, }, ], identifier: 'lobe-web-browsing', meta: { title: 'Web Browsing', description: 'Browse the web', }, type: 'builtin', }; const mockDalleManifest: LobeToolManifest = { api: [ { description: 'Generate images', name: 'generateImage', parameters: { type: 'object', properties: { prompt: { type: 'string', description: 'Image prompt' }, }, required: ['prompt'], }, }, ], identifier: 'dalle', meta: { title: 'DALL-E', description: 'Generate images', }, type: 'builtin', }; describe('ToolsEngine', () => { describe('constructor', () => { it('should initialize with manifest schemas', () => { const engine = new ToolsEngine({ manifestSchemas: [mockWebBrowsingManifest, mockDalleManifest], }); expect(engine.hasPlugin('lobe-web-browsing')).toBe(true); expect(engine.hasPlugin('dalle')).toBe(true); expect(engine.hasPlugin('non-existent')).toBe(false); }); it('should store available plugins', () => { const engine = new ToolsEngine({ manifestSchemas: [mockWebBrowsingManifest, mockDalleManifest], }); const availablePlugins = engine.getAvailablePlugins(); expect(availablePlugins).toEqual(['lobe-web-browsing', 'dalle']); }); }); describe('generateTools', () => { it('should return undefined when function calling is not supported', () => { const mockFunctionCallChecker = vi.fn().mockReturnValue(false); const engine = new ToolsEngine({ manifestSchemas: [mockWebBrowsingManifest], functionCallChecker: mockFunctionCallChecker, }); const result = engine.generateTools({ toolIds: ['lobe-web-browsing'], model: 'gpt-3.5-turbo', provider: 'openai', }); expect(result).toBeUndefined(); }); it('should return undefined when no plugins are enabled', () => { const mockEnableChecker = vi.fn().mockReturnValue(false); const engine = new ToolsEngine({ manifestSchemas: [mockWebBrowsingManifest], enableChecker: mockEnableChecker, functionCallChecker: () => true, }); const result = engine.generateTools({ toolIds: ['lobe-web-browsing'], model: 'gpt-4', provider: 'openai', }); expect(result).toBeUndefined(); expect(mockEnableChecker).toHaveBeenCalledWith({ pluginId: 'lobe-web-browsing', manifest: mockWebBrowsingManifest, model: 'gpt-4', provider: 'openai', context: undefined, }); }); it('should generate tools when plugins are enabled', () => { const engine = new ToolsEngine({ manifestSchemas: [mockWebBrowsingManifest], enableChecker: () => true, functionCallChecker: () => true, }); const result = engine.generateTools({ toolIds: ['lobe-web-browsing'], model: 'gpt-4', provider: 'openai', }); expect(result).toEqual([ { type: 'function', function: { name: 'lobe-web-browsing____search____builtin', description: 'Search the web', parameters: { type: 'object', properties: { query: { type: 'string', description: 'Search query' }, }, required: ['query'], }, }, }, ]); }); it('should pass context to enable checker', () => { const mockEnableChecker = vi.fn().mockReturnValue(true); const engine = new ToolsEngine({ manifestSchemas: [mockWebBrowsingManifest], enableChecker: mockEnableChecker, functionCallChecker: () => true, }); const context = { isSearchEnabled: true }; engine.generateTools({ toolIds: ['lobe-web-browsing'], model: 'gpt-4', provider: 'openai', context, }); expect(mockEnableChecker).toHaveBeenCalledWith({ pluginId: 'lobe-web-browsing', manifest: mockWebBrowsingManifest, model: 'gpt-4', provider: 'openai', context, }); }); it('should handle non-existent plugins gracefully', () => { const engine = new ToolsEngine({ manifestSchemas: [mockWebBrowsingManifest], enableChecker: () => true, functionCallChecker: () => true, }); const result = engine.generateTools({ toolIds: ['lobe-web-browsing', 'non-existent'], model: 'gpt-4', provider: 'openai', }); expect(result).toBeDefined(); expect(result).toHaveLength(1); }); }); describe('generateToolsDetailed', () => { it('should return detailed results with filtered plugins', () => { const engine = new ToolsEngine({ manifestSchemas: [mockWebBrowsingManifest, mockDalleManifest], enableChecker: ({ pluginId }) => pluginId === 'lobe-web-browsing', functionCallChecker: () => true, }); const result = engine.generateToolsDetailed({ toolIds: ['lobe-web-browsing', 'dalle', 'non-existent'], model: 'gpt-4', provider: 'openai', }); expect(result.tools).toHaveLength(1); expect(result.enabledToolIds).toEqual(['lobe-web-browsing']); expect(result.filteredTools).toEqual([ { id: 'dalle', reason: 'disabled' }, { id: 'non-existent', reason: 'not_found' }, ]); }); it('should filter all plugins as incompatible when function calling is not supported', () => { const mockFunctionCallChecker = vi.fn().mockReturnValue(false); const engine = new ToolsEngine({ manifestSchemas: [mockWebBrowsingManifest, mockDalleManifest], functionCallChecker: mockFunctionCallChecker, }); const result = engine.generateToolsDetailed({ toolIds: ['lobe-web-browsing', 'dalle'], model: 'gpt-5-chat-latest', provider: 'openai', }); expect(mockFunctionCallChecker).toHaveBeenCalledWith('gpt-5-chat-latest', 'openai'); expect(result.tools).toBeUndefined(); expect(result.enabledToolIds).toEqual([]); expect(result.filteredTools).toEqual([ { id: 'lobe-web-browsing', reason: 'incompatible' }, { id: 'dalle', reason: 'incompatible' }, ]); }); it('should combine incompatible and not_found reasons when FC is not supported', () => { const engine = new ToolsEngine({ manifestSchemas: [mockWebBrowsingManifest], functionCallChecker: () => false, }); const result = engine.generateToolsDetailed({ toolIds: ['lobe-web-browsing', 'non-existent', 'dalle'], model: 'gpt-5-chat-latest', provider: 'openai', }); expect(result.tools).toBeUndefined(); expect(result.enabledToolIds).toEqual([]); expect(result.filteredTools).toEqual([ { id: 'lobe-web-browsing', reason: 'incompatible' }, { id: 'non-existent', reason: 'not_found' }, { id: 'dalle', reason: 'not_found' }, ]); }); it('should still call enableChecker when FC is supported', () => { const mockEnableChecker = vi.fn().mockReturnValue(false); const engine = new ToolsEngine({ manifestSchemas: [mockWebBrowsingManifest, mockDalleManifest], enableChecker: mockEnableChecker, functionCallChecker: () => true, }); const result = engine.generateToolsDetailed({ toolIds: ['lobe-web-browsing', 'dalle'], model: 'gpt-4', provider: 'openai', }); expect(mockEnableChecker).toHaveBeenCalledTimes(2); expect(result.tools).toBeUndefined(); expect(result.enabledToolIds).toEqual([]); expect(result.filteredTools).toEqual([ { id: 'lobe-web-browsing', reason: 'disabled' }, { id: 'dalle', reason: 'disabled' }, ]); }); it('should not call enableChecker when FC is not supported', () => { const mockEnableChecker = vi.fn().mockReturnValue(true); const engine = new ToolsEngine({ manifestSchemas: [mockWebBrowsingManifest, mockDalleManifest], enableChecker: mockEnableChecker, functionCallChecker: () => false, }); const result = engine.generateToolsDetailed({ toolIds: ['lobe-web-browsing', 'dalle'], model: 'gpt-5-chat-latest', provider: 'openai', }); expect(mockEnableChecker).not.toHaveBeenCalled(); expect(result.tools).toBeUndefined(); expect(result.enabledToolIds).toEqual([]); expect(result.filteredTools).toEqual([ { id: 'lobe-web-browsing', reason: 'incompatible' }, { id: 'dalle', reason: 'incompatible' }, ]); }); }); describe('plugin management', () => { it('should allow adding new plugin manifest', () => { const engine = new ToolsEngine({ manifestSchemas: [mockWebBrowsingManifest], }); expect(engine.hasPlugin('dalle')).toBe(false); engine.addPluginManifest(mockDalleManifest); expect(engine.hasPlugin('dalle')).toBe(true); expect(engine.getPluginManifest('dalle')).toBe(mockDalleManifest); }); it('should allow removing plugin manifest', () => { const engine = new ToolsEngine({ manifestSchemas: [mockWebBrowsingManifest, mockDalleManifest], }); expect(engine.hasPlugin('dalle')).toBe(true); const removed = engine.removePluginManifest('dalle'); expect(removed).toBe(true); expect(engine.hasPlugin('dalle')).toBe(false); }); it('should allow updating all manifest schemas', () => { const engine = new ToolsEngine({ manifestSchemas: [mockWebBrowsingManifest], }); expect(engine.getAvailablePlugins()).toEqual(['lobe-web-browsing']); engine.updateManifestSchemas([mockDalleManifest]); expect(engine.getAvailablePlugins()).toEqual(['dalle']); }); }); describe('getEnabledPluginManifests', () => { it('should return empty Map when no tool IDs provided', () => { const engine = new ToolsEngine({ manifestSchemas: [mockWebBrowsingManifest, mockDalleManifest], }); const result = engine.getEnabledPluginManifests([]); expect(result.size).toBe(0); }); it('should return Map with plugin manifests for given tool IDs', () => { const engine = new ToolsEngine({ manifestSchemas: [mockWebBrowsingManifest, mockDalleManifest], }); const result = engine.getEnabledPluginManifests(['lobe-web-browsing', 'dalle']); expect(result.size).toBe(2); expect(result.get('lobe-web-browsing')).toBe(mockWebBrowsingManifest); expect(result.get('dalle')).toBe(mockDalleManifest); }); it('should include default tool IDs in the result', () => { const engine = new ToolsEngine({ manifestSchemas: [mockWebBrowsingManifest, mockDalleManifest], defaultToolIds: ['dalle'], }); const result = engine.getEnabledPluginManifests(['lobe-web-browsing']); expect(result.size).toBe(2); expect(result.has('lobe-web-browsing')).toBe(true); expect(result.has('dalle')).toBe(true); }); it('should handle non-existent plugins gracefully', () => { const engine = new ToolsEngine({ manifestSchemas: [mockWebBrowsingManifest], }); const result = engine.getEnabledPluginManifests(['lobe-web-browsing', 'non-existent']); expect(result.size).toBe(1); expect(result.has('lobe-web-browsing')).toBe(true); expect(result.has('non-existent')).toBe(false); }); it('should return all default tools when called without arguments', () => { const engine = new ToolsEngine({ manifestSchemas: [mockWebBrowsingManifest, mockDalleManifest], defaultToolIds: ['dalle', 'lobe-web-browsing'], }); const result = engine.getEnabledPluginManifests(); expect(result.size).toBe(2); expect(result.has('lobe-web-browsing')).toBe(true); expect(result.has('dalle')).toBe(true); }); it('should not duplicate tools when same ID appears in both toolIds and defaultToolIds', () => { const engine = new ToolsEngine({ manifestSchemas: [mockWebBrowsingManifest, mockDalleManifest], defaultToolIds: ['dalle'], }); const result = engine.getEnabledPluginManifests(['dalle']); expect(result.size).toBe(1); expect(result.get('dalle')).toBe(mockDalleManifest); }); }); describe('getAllPluginManifests', () => { it('should return all plugin manifests', () => { const engine = new ToolsEngine({ manifestSchemas: [mockWebBrowsingManifest, mockDalleManifest], }); const result = engine.getAllPluginManifests(); expect(result.size).toBe(2); expect(result.get('lobe-web-browsing')).toBe(mockWebBrowsingManifest); expect(result.get('dalle')).toBe(mockDalleManifest); }); it('should return empty Map when no manifests are loaded', () => { const engine = new ToolsEngine({ manifestSchemas: [], }); const result = engine.getAllPluginManifests(); expect(result.size).toBe(0); }); it('should return a new Map instance (not the internal one)', () => { const engine = new ToolsEngine({ manifestSchemas: [mockWebBrowsingManifest], }); const result1 = engine.getAllPluginManifests(); const result2 = engine.getAllPluginManifests(); // Should be different Map instances expect(result1).not.toBe(result2); // But have the same content expect(result1.size).toBe(result2.size); expect(result1.get('lobe-web-browsing')).toBe(result2.get('lobe-web-browsing')); }); it('should reflect changes after adding a plugin', () => { const engine = new ToolsEngine({ manifestSchemas: [mockWebBrowsingManifest], }); let result = engine.getAllPluginManifests(); expect(result.size).toBe(1); engine.addPluginManifest(mockDalleManifest); result = engine.getAllPluginManifests(); expect(result.size).toBe(2); expect(result.has('dalle')).toBe(true); }); it('should reflect changes after removing a plugin', () => { const engine = new ToolsEngine({ manifestSchemas: [mockWebBrowsingManifest, mockDalleManifest], }); let result = engine.getAllPluginManifests(); expect(result.size).toBe(2); engine.removePluginManifest('dalle'); result = engine.getAllPluginManifests(); expect(result.size).toBe(1); expect(result.has('dalle')).toBe(false); }); }); describe('default behavior', () => { it('should use default enable checker when none provided', () => { const engine = new ToolsEngine({ manifestSchemas: [mockWebBrowsingManifest], functionCallChecker: () => true, }); const result = engine.generateTools({ toolIds: ['lobe-web-browsing'], model: 'gpt-4', provider: 'openai', }); expect(result).toBeDefined(); expect(result).toHaveLength(1); }); it('should use default function call checker when none provided', () => { const engine = new ToolsEngine({ manifestSchemas: [mockWebBrowsingManifest], enableChecker: () => true, }); const result = engine.generateTools({ toolIds: ['lobe-web-browsing'], model: 'gpt-4', provider: 'openai', }); expect(result).toBeDefined(); expect(result).toHaveLength(1); }); }); describe('ToolsEngine Integration Tests (migrated from enabledSchema)', () => { // Mock manifest data similar to the original tool selector tests const mockManifests: LobeToolManifest[] = [ { identifier: 'plugin-1', api: [{ name: 'api-1', description: 'API 1', parameters: {} }], meta: { title: 'Plugin 1', description: 'Plugin 1 description', avatar: '🔧', }, type: 'default', }, { identifier: 'plugin-2', api: [{ name: 'api-2', description: 'API 2', parameters: {} }], meta: { title: 'Plugin 2', description: 'Plugin 2 description', avatar: '⚙️', }, type: 'default', }, { identifier: 'plugin-3', api: [ { name: 'api-3', description: '123123', parameters: { type: 'object', properties: { a: { type: 'string' } }, }, // This should not appear in the final result url: 'bac', }, ], meta: { title: 'Plugin 3', description: 'Plugin 3 description', avatar: '🛠️', }, type: 'default', }, { identifier: 'plugin-4', api: [{ name: 'api-4', description: 'API 4', parameters: {} }], meta: { title: 'Plugin 4', description: 'Plugin 4 description', avatar: '🔩', }, type: 'standalone', }, { identifier: 'long-long-plugin-with-id', api: [ { name: 'long-long-manifest-long-long-apiName', description: 'Long API', parameters: {}, }, ], meta: { title: 'Long Plugin', description: 'Long plugin description', avatar: '📏', }, type: 'default', }, { identifier: 'builtin-1', api: [{ name: 'builtin-api-1', description: 'Builtin API 1', parameters: {} }], meta: { title: 'Builtin 1', description: 'Builtin 1 description', avatar: '🏗️', }, type: 'builtin', }, ]; const createTestEngine = () => { return new ToolsEngine({ manifestSchemas: mockManifests, functionCallChecker: () => true, // Always allow function calls for tests }); }; describe('basic tool generation', () => { it('should return correct tools array for multiple plugins', () => { const engine = createTestEngine(); const result = engine.generateTools({ toolIds: ['plugin-1', 'plugin-2'], model: 'gpt-4', provider: 'openai', }); expect(result).toEqual([ { type: 'function', function: { name: 'plugin-1____api-1', description: 'API 1', parameters: {}, }, }, { type: 'function', function: { name: 'plugin-2____api-2', description: 'API 2', parameters: {}, }, }, ]); }); it('should handle standalone plugin type correctly', () => { const engine = createTestEngine(); const result = engine.generateTools({ toolIds: ['plugin-4'], model: 'gpt-4', provider: 'openai', }); expect(result).toEqual([ { type: 'function', function: { name: 'plugin-4____api-4____standalone', description: 'API 4', parameters: {}, }, }, ]); }); it('should handle builtin plugin type correctly', () => { const engine = createTestEngine(); const result = engine.generateTools({ toolIds: ['builtin-1'], model: 'gpt-4', provider: 'openai', }); expect(result).toEqual([ { type: 'function', function: { name: 'builtin-1____builtin-api-1____builtin', description: 'Builtin API 1', parameters: {}, }, }, ]); }); it('should return empty array when no plugins match', () => { const engine = createTestEngine(); const result = engine.generateTools({ toolIds: [], model: 'gpt-4', provider: 'openai', }); expect(result).toBeUndefined(); }); it('should handle non-existent plugins gracefully', () => { const engine = createTestEngine(); const result = engine.generateTools({ toolIds: ['non-existent-plugin'], model: 'gpt-4', provider: 'openai', }); expect(result).toBeUndefined(); }); }); describe('long name handling (MD5 hash)', () => { it('should return MD5 hash for long API names', () => { const engine = createTestEngine(); const result = engine.generateTools({ toolIds: ['long-long-plugin-with-id'], model: 'gpt-4', provider: 'openai', }); expect(result).toHaveLength(1); expect(result![0]).toEqual({ type: 'function', function: { // The long action name should be hashed name: expect.stringMatching(/^long-long-plugin-with-id____MD5HASH_[a-f0-9]+$/), description: 'Long API', parameters: {}, }, }); // Verify the specific hash matches expected value from original test expect(result![0].function.name).toContain('MD5HASH_'); }); }); describe('parameter handling and filtering', () => { // fix https://github.com/lobehub/lobe-chat/issues/2036 it('should not include URL field in function parameters', () => { const engine = createTestEngine(); const result = engine.generateTools({ toolIds: ['plugin-3'], model: 'gpt-4', provider: 'openai', }); expect(result).toHaveLength(1); expect(result![0].function).toEqual({ description: '123123', name: 'plugin-3____api-3', parameters: { properties: { a: { type: 'string', }, }, type: 'object', }, }); // Ensure URL is not included expect(result![0].function).not.toHaveProperty('url'); }); it('should preserve all other API properties correctly', () => { const engine = createTestEngine(); const result = engine.generateTools({ toolIds: ['plugin-3'], model: 'gpt-4', provider: 'openai', }); const func = result![0].function; expect(func.description).toBe('123123'); expect(func.name).toBe('plugin-3____api-3'); expect(func.parameters).toEqual({ type: 'object', properties: { a: { type: 'string' } }, }); }); }); describe('function calling support', () => { it('should return undefined when function calling is not supported', () => { const engine = new ToolsEngine({ manifestSchemas: mockManifests, functionCallChecker: () => false, // Disable function calls }); const result = engine.generateTools({ toolIds: ['plugin-1'], model: 'gpt-3.5-turbo', provider: 'openai', }); expect(result).toBeUndefined(); }); it('should respect function calling checker logic', () => { const engine = new ToolsEngine({ manifestSchemas: mockManifests, functionCallChecker: (model: string) => model.includes('gpt-4'), // Only allow GPT-4 }); // Should work with GPT-4 const result1 = engine.generateTools({ toolIds: ['plugin-1'], model: 'gpt-4', provider: 'openai', }); expect(result1).toBeDefined(); expect(result1).toHaveLength(1); // Should not work with GPT-3.5 const result2 = engine.generateTools({ toolIds: ['plugin-1'], model: 'gpt-3.5-turbo', provider: 'openai', }); expect(result2).toBeUndefined(); }); }); describe('plugin enable filtering', () => { it('should respect enable checker logic', () => { const engine = new ToolsEngine({ manifestSchemas: mockManifests, functionCallChecker: () => true, enableChecker: ({ pluginId }) => pluginId === 'plugin-1', // Only enable plugin-1 }); const result = engine.generateTools({ toolIds: ['plugin-1', 'plugin-2'], model: 'gpt-4', provider: 'openai', }); expect(result).toHaveLength(1); expect(result![0].function.name).toBe('plugin-1____api-1'); }); it('should return undefined when no plugins are enabled', () => { const engine = new ToolsEngine({ manifestSchemas: mockManifests, functionCallChecker: () => true, enableChecker: () => false, // Disable all plugins }); const result = engine.generateTools({ toolIds: ['plugin-1', 'plugin-2'], model: 'gpt-4', provider: 'openai', }); expect(result).toBeUndefined(); }); }); describe('detailed generation results', () => { it('should provide detailed filtering information', () => { const engine = new ToolsEngine({ manifestSchemas: mockManifests, functionCallChecker: () => true, enableChecker: ({ pluginId }) => pluginId === 'plugin-1', }); const result = engine.generateToolsDetailed({ toolIds: ['plugin-1', 'plugin-2', 'non-existent'], model: 'gpt-4', provider: 'openai', }); expect(result.tools).toHaveLength(1); expect(result.enabledToolIds).toEqual(['plugin-1']); expect(result.filteredTools).toEqual([ { id: 'plugin-2', reason: 'disabled' }, { id: 'non-existent', reason: 'not_found' }, ]); }); }); }); /** * Migration tests: Demonstrating that ToolsEngine can replace enabledSchema * These tests show the equivalence between the old selector-based approach * and the new ToolsEngine-based approach */ describe('enabledSchema Migration to ToolsEngine', () => { // Sample manifest data that mimics the old toolSelectors test data const sampleManifests: LobeToolManifest[] = [ { identifier: 'plugin-1', api: [{ name: 'api-1', description: 'API 1', parameters: {} }], meta: { title: 'Plugin 1', description: 'Plugin 1 description', avatar: '🔧', }, type: 'default', }, { identifier: 'plugin-2', api: [{ name: 'api-2', description: 'API 2', parameters: {} }], meta: { title: 'Plugin 2', description: 'Plugin 2 description', avatar: '⚙️', }, type: 'default', }, { identifier: 'standalone-plugin', api: [{ name: 'standalone-api', description: 'Standalone API', parameters: {} }], meta: { title: 'Standalone Plugin', description: 'Standalone plugin description', avatar: '🔩', }, type: 'standalone', }, ]; describe('basic functionality comparison', () => { it('should generate the same tool names as enabledSchema did', () => { const engine = new ToolsEngine({ manifestSchemas: sampleManifests, functionCallChecker: () => true, }); const result = engine.generateTools({ toolIds: ['plugin-1', 'plugin-2'], model: 'gpt-4', provider: 'openai', }); // These should match the format that enabledSchema produced expect(result).toEqual([ { type: 'function', function: { name: 'plugin-1____api-1', description: 'API 1', parameters: {}, }, }, { type: 'function', function: { name: 'plugin-2____api-2', description: 'API 2', parameters: {}, }, }, ]); }); it('should handle type suffixes the same way enabledSchema did', () => { const engine = new ToolsEngine({ manifestSchemas: sampleManifests, functionCallChecker: () => true, }); const result = engine.generateTools({ toolIds: ['standalone-plugin'], model: 'gpt-4', provider: 'openai', }); expect(result![0].function.name).toBe('standalone-plugin____standalone-api____standalone'); }); }); describe('advantages of ToolsEngine over enabledSchema', () => { it('provides detailed filtering information that enabledSchema could not', () => { const engine = new ToolsEngine({ manifestSchemas: sampleManifests, functionCallChecker: () => true, enableChecker: ({ pluginId }) => pluginId === 'plugin-1', }); const result = engine.generateToolsDetailed({ toolIds: ['plugin-1', 'plugin-2', 'non-existent'], model: 'gpt-4', provider: 'openai', }); expect(result).toEqual({ tools: [ { type: 'function', function: { name: 'plugin-1____api-1', description: 'API 1', parameters: {}, }, }, ], enabledToolIds: ['plugin-1'], filteredTools: [ { id: 'plugin-2', reason: 'disabled' }, { id: 'non-existent', reason: 'not_found' }, ], }); }); it('supports function calling checks that enabledSchema relied on external logic for', () => { const engine = new ToolsEngine({ manifestSchemas: sampleManifests, functionCallChecker: (model: string) => model.includes('gpt-4'), }); // Should work with GPT-4 const result1 = engine.generateTools({ toolIds: ['plugin-1'], model: 'gpt-4', provider: 'openai', }); expect(result1).toBeDefined(); // Should not work with older models const result2 = engine.generateTools({ toolIds: ['plugin-1'], model: 'gpt-3.5-turbo', provider: 'openai', }); expect(result2).toBeUndefined(); }); it('encapsulates all tool generation logic in one place', () => { // This demonstrates that ToolsEngine combines: // 1. Manifest filtering (what enabledSchema did) // 2. Function calling support checks (what prepareTools did) // 3. Tool name generation (what genToolCallingName did) // 4. Plugin enable checking (custom logic) const engine = new ToolsEngine({ manifestSchemas: sampleManifests, functionCallChecker: (model: string, provider: string) => { return model.includes('gpt') && provider === 'openai'; }, enableChecker: ({ pluginId, model }) => { // Custom business logic that was scattered before if (model === 'gpt-3.5-turbo') return false; return pluginId !== 'plugin-2'; // Skip plugin-2 for demo }, }); const result = engine.generateTools({ toolIds: ['plugin-1', 'plugin-2'], model: 'gpt-4', provider: 'openai', }); // Only plugin-1 should be enabled due to custom logic expect(result).toHaveLength(1); expect(result![0].function.name).toBe('plugin-1____api-1'); }); }); }); describe('deduplication', () => { it('should deduplicate tool IDs in toolIds array', () => { const engine = new ToolsEngine({ manifestSchemas: [mockWebBrowsingManifest, mockDalleManifest], enableChecker: () => true, functionCallChecker: () => true, }); const result = engine.generateTools({ toolIds: ['lobe-web-browsing', 'lobe-web-browsing', 'dalle'], model: 'gpt-4', provider: 'openai', }); // Should only generate 2 tools, not 3 expect(result).toHaveLength(2); expect(result![0].function.name).toBe('lobe-web-browsing____search____builtin'); expect(result![1].function.name).toBe('dalle____generateImage____builtin'); }); it('should deduplicate between toolIds and defaultToolIds', () => { const engine = new ToolsEngine({ manifestSchemas: [mockWebBrowsingManifest, mockDalleManifest], defaultToolIds: ['lobe-web-browsing'], enableChecker: () => true, functionCallChecker: () => true, }); const result = engine.generateTools({ toolIds: ['lobe-web-browsing', 'dalle'], model: 'gpt-4', provider: 'openai', }); // Should only generate 2 tools (lobe-web-browsing should appear once) expect(result).toHaveLength(2); expect(result![0].function.name).toBe('lobe-web-browsing____search____builtin'); expect(result![1].function.name).toBe('dalle____generateImage____builtin'); }); it('should deduplicate in generateToolsDetailed', () => { const engine = new ToolsEngine({ manifestSchemas: [mockWebBrowsingManifest, mockDalleManifest], defaultToolIds: ['dalle'], enableChecker: () => true, functionCallChecker: () => true, }); const result = engine.generateToolsDetailed({ toolIds: ['lobe-web-browsing', 'dalle', 'dalle'], model: 'gpt-4', provider: 'openai', }); // Should only generate 2 unique tools expect(result.tools).toHaveLength(2); expect(result.enabledToolIds).toEqual(['lobe-web-browsing', 'dalle']); expect(result.filteredTools).toEqual([]); }); it('should handle complex deduplication scenarios', () => { const engine = new ToolsEngine({ manifestSchemas: [mockWebBrowsingManifest, mockDalleManifest], defaultToolIds: ['lobe-web-browsing', 'dalle'], enableChecker: () => true, functionCallChecker: () => true, }); const result = engine.generateTools({ toolIds: ['dalle', 'lobe-web-browsing', 'dalle', 'lobe-web-browsing'], model: 'gpt-4', provider: 'openai', }); // Should only generate 2 unique tools despite multiple duplicates expect(result).toHaveLength(2); }); }); });