@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.
553 lines (474 loc) • 15.4 kB
text/typescript
import { describe, expect, it, vi } from 'vitest';
import type { PipelineContext } from '../../types';
import { ToolCallProcessor } from '../ToolCall';
import type { ToolCallConfig } from '../ToolCall';
describe('ToolCallProcessor', () => {
const createContext = (messages: any[]): PipelineContext => ({
initialState: {
messages: [],
model: 'test-model',
provider: 'test-provider',
},
isAborted: false,
messages,
metadata: {
maxTokens: 4000,
model: 'test-model',
},
});
const defaultConfig: ToolCallConfig = {
model: 'gpt-4',
provider: 'openai',
};
describe('constructor', () => {
it('should initialize with config', () => {
const processor = new ToolCallProcessor(defaultConfig);
expect(processor.name).toBe('ToolCallProcessor');
});
it('should initialize with custom options', () => {
const processor = new ToolCallProcessor(defaultConfig, { debug: true });
expect(processor.name).toBe('ToolCallProcessor');
});
});
describe('process - assistant messages', () => {
it('should convert tools to tool_calls format', async () => {
const processor = new ToolCallProcessor(defaultConfig);
const context = createContext([
{
content: '',
id: 'msg1',
role: 'assistant',
tools: [
{
apiName: 'search',
arguments: '{"query":"test"}',
id: 'call_1',
identifier: 'web',
type: 'builtin',
},
],
},
]);
const result = await processor.process(context);
expect(result.messages).toHaveLength(1);
expect(result.messages[0].role).toBe('assistant');
expect(result.messages[0].tool_calls).toEqual([
{
function: {
arguments: '{"query":"test"}',
name: 'web.search',
},
id: 'call_1',
type: 'function',
},
]);
});
it('should use custom genToolCallingName function', async () => {
const genToolCallingName = vi.fn(
(identifier, apiName, type) => `custom_${identifier}_${apiName}_${type}`,
);
const processor = new ToolCallProcessor({
...defaultConfig,
genToolCallingName,
});
const context = createContext([
{
content: '',
role: 'assistant',
tools: [
{
apiName: 'search',
arguments: '{}',
id: 'call_1',
identifier: 'web',
type: 'builtin',
},
],
},
]);
const result = await processor.process(context);
expect(genToolCallingName).toHaveBeenCalledWith('web', 'search', 'builtin');
expect(result.messages[0].tool_calls[0].function.name).toBe('custom_web_search_builtin');
});
it('should handle multiple tool calls', async () => {
const processor = new ToolCallProcessor(defaultConfig);
const context = createContext([
{
content: '',
role: 'assistant',
tools: [
{
apiName: 'search',
arguments: '{"query":"test1"}',
id: 'call_1',
identifier: 'web',
},
{
apiName: 'translate',
arguments: '{"text":"hello"}',
id: 'call_2',
identifier: 'utils',
},
],
},
]);
const result = await processor.process(context);
expect(result.messages[0].tool_calls).toHaveLength(2);
expect(result.messages[0].tool_calls[0].function.name).toBe('web.search');
expect(result.messages[0].tool_calls[1].function.name).toBe('utils.translate');
});
it('should remove tool_calls and tools when not supported', async () => {
const isCanUseFC = vi.fn(() => false);
const processor = new ToolCallProcessor({
...defaultConfig,
isCanUseFC,
});
const context = createContext([
{
content: 'Using a tool',
role: 'assistant',
tools: [
{
apiName: 'search',
arguments: '{}',
id: 'call_1',
identifier: 'web',
},
],
},
]);
const result = await processor.process(context);
expect(isCanUseFC).toHaveBeenCalledWith('gpt-4', 'openai');
expect(result.messages[0]).not.toHaveProperty('tools');
expect(result.messages[0]).not.toHaveProperty('tool_calls');
expect(result.messages[0].content).toBe('Using a tool');
expect(result.messages[0].role).toBe('assistant');
});
it('should remove empty tool_calls when not supported', async () => {
const isCanUseFC = vi.fn(() => false);
const processor = new ToolCallProcessor({
...defaultConfig,
isCanUseFC,
});
const context = createContext([
{
content: 'Test',
role: 'assistant',
tool_calls: [],
},
]);
const result = await processor.process(context);
expect(result.messages[0]).not.toHaveProperty('tool_calls');
expect(result.messages[0]).not.toHaveProperty('tools');
});
it('should keep message when no tools present', async () => {
const processor = new ToolCallProcessor(defaultConfig);
const context = createContext([
{
content: 'Regular message',
role: 'assistant',
},
]);
const result = await processor.process(context);
expect(result.messages[0]).toEqual({
content: 'Regular message',
role: 'assistant',
});
});
});
describe('process - tool messages', () => {
it('should generate tool name from plugin', async () => {
const processor = new ToolCallProcessor(defaultConfig);
const context = createContext([
{
content: 'Tool result',
plugin: {
apiName: 'search',
identifier: 'web',
type: 'builtin',
},
role: 'tool',
tool_call_id: 'call_1',
},
]);
const result = await processor.process(context);
expect(result.messages[0].name).toBe('web.search');
expect(result.messages[0].tool_call_id).toBe('call_1');
expect(result.messages[0].role).toBe('tool');
});
it('should use custom genToolCallingName for tool messages', async () => {
const genToolCallingName = vi.fn(
(identifier, apiName, type) => `tool_${identifier}_${apiName}_${type}`,
);
const processor = new ToolCallProcessor({
...defaultConfig,
genToolCallingName,
});
const context = createContext([
{
content: 'Result',
plugin: {
apiName: 'search',
identifier: 'web',
type: 'plugin',
},
role: 'tool',
tool_call_id: 'call_1',
},
]);
const result = await processor.process(context);
expect(genToolCallingName).toHaveBeenCalledWith('web', 'search', 'plugin');
expect(result.messages[0].name).toBe('tool_web_search_plugin');
});
it('should convert tool message to user message when not supported', async () => {
const isCanUseFC = vi.fn(() => false);
const processor = new ToolCallProcessor({
...defaultConfig,
isCanUseFC,
});
const context = createContext([
{
content: 'Tool result',
name: 'web.search',
plugin: {
apiName: 'search',
identifier: 'web',
},
role: 'tool',
tool_call_id: 'call_1',
},
]);
const result = await processor.process(context);
expect(result.messages[0].role).toBe('user');
expect(result.messages[0].content).toBe('Tool result');
expect(result.messages[0].name).toBeUndefined();
expect(result.messages[0].plugin).toBeUndefined();
expect(result.messages[0].tool_call_id).toBeUndefined();
});
it('should handle tool message without plugin', async () => {
const processor = new ToolCallProcessor(defaultConfig);
const context = createContext([
{
content: 'Result',
role: 'tool',
tool_call_id: 'call_1',
},
]);
const result = await processor.process(context);
expect(result.messages[0].name).toBeUndefined();
expect(result.messages[0].role).toBe('tool');
expect(result.messages[0].tool_call_id).toBe('call_1');
});
});
describe('process - metadata', () => {
it('should update metadata with processing stats', async () => {
const processor = new ToolCallProcessor(defaultConfig);
const context = createContext([
{
content: '',
role: 'assistant',
tools: [
{
apiName: 'search',
arguments: '{}',
id: 'call_1',
identifier: 'web',
},
],
},
{
content: 'Result',
plugin: {
apiName: 'search',
identifier: 'web',
},
role: 'tool',
tool_call_id: 'call_1',
},
]);
const result = await processor.process(context);
expect(result.metadata.toolCallProcessed).toBe(2);
expect(result.metadata.toolCallsConverted).toBe(1);
expect(result.metadata.toolMessagesConverted).toBe(1);
expect(result.metadata.supportTools).toBe(true);
});
it('should track supportTools when function calling not supported', async () => {
const isCanUseFC = vi.fn(() => false);
const processor = new ToolCallProcessor({
...defaultConfig,
isCanUseFC,
});
const context = createContext([
{
content: 'Test',
role: 'user',
},
]);
const result = await processor.process(context);
expect(result.metadata.supportTools).toBe(false);
});
it('should count zero when no tool messages processed', async () => {
const processor = new ToolCallProcessor(defaultConfig);
const context = createContext([
{
content: 'Regular message',
role: 'user',
},
]);
const result = await processor.process(context);
expect(result.metadata.toolCallProcessed).toBe(0);
expect(result.metadata.toolCallsConverted).toBe(0);
expect(result.metadata.toolMessagesConverted).toBe(0);
});
});
describe('process - error handling', () => {
it('should continue processing when message processing fails', async () => {
const processor = new ToolCallProcessor(defaultConfig);
// Create a message that might cause an error but should be handled gracefully
const context = createContext([
{
content: 'Valid message',
role: 'user',
},
{
content: '',
// Invalid tools structure
role: 'assistant',
tools: null,
},
{
content: 'Another valid message',
role: 'user',
},
]);
const result = await processor.process(context);
// Should process all messages despite potential errors
expect(result.messages).toHaveLength(3);
});
});
describe('process - mixed scenarios', () => {
it('should handle conversation with mixed message types', async () => {
const processor = new ToolCallProcessor(defaultConfig);
const context = createContext([
{
content: 'System prompt',
role: 'system',
},
{
content: 'User question',
role: 'user',
},
{
content: '',
role: 'assistant',
tools: [
{
apiName: 'search',
arguments: '{"query":"test"}',
id: 'call_1',
identifier: 'web',
},
],
},
{
content: 'Search results',
plugin: {
apiName: 'search',
identifier: 'web',
},
role: 'tool',
tool_call_id: 'call_1',
},
{
content: 'Final response',
role: 'assistant',
},
]);
const result = await processor.process(context);
expect(result.messages).toHaveLength(5);
expect(result.messages[0].role).toBe('system');
expect(result.messages[1].role).toBe('user');
expect(result.messages[2].role).toBe('assistant');
expect(result.messages[2].tool_calls).toBeDefined();
expect(result.messages[3].role).toBe('tool');
expect(result.messages[3].name).toBe('web.search');
expect(result.messages[4].role).toBe('assistant');
});
it('should not mutate original context', async () => {
const processor = new ToolCallProcessor(defaultConfig);
const originalMessage = {
content: '',
id: 'msg1',
role: 'assistant',
tools: [
{
apiName: 'search',
arguments: '{}',
id: 'call_1',
identifier: 'web',
},
],
};
const context = createContext([originalMessage]);
await processor.process(context);
// Original message should be unchanged
expect(originalMessage).toHaveProperty('tools');
expect(originalMessage).not.toHaveProperty('tool_calls');
});
it('should return valid context after processing', async () => {
const processor = new ToolCallProcessor(defaultConfig);
const context = createContext([
{
content: 'Test',
role: 'user',
},
]);
const result = await processor.process(context);
expect(result).toBeDefined();
expect(result.messages).toBeDefined();
expect(result.metadata).toBeDefined();
expect(result.metadata.supportTools).toBeDefined();
});
});
describe('isCanUseFC integration', () => {
it('should call isCanUseFC with correct parameters', async () => {
const isCanUseFC = vi.fn(() => true);
const processor = new ToolCallProcessor({
isCanUseFC,
model: 'claude-3',
provider: 'anthropic',
});
const context = createContext([
{
content: 'Test',
role: 'user',
},
]);
await processor.process(context);
expect(isCanUseFC).toHaveBeenCalledWith('claude-3', 'anthropic');
});
it('should default to supporting tools when isCanUseFC not provided', async () => {
const processor = new ToolCallProcessor({
model: 'test-model',
provider: 'test-provider',
});
const context = createContext([
{
content: '',
role: 'assistant',
tools: [
{
apiName: 'search',
arguments: '{}',
id: 'call_1',
identifier: 'web',
},
],
},
]);
const result = await processor.process(context);
expect(result.metadata.supportTools).toBe(true);
expect(result.messages[0].tool_calls).toBeDefined();
});
});
});