mirror of
https://github.com/lobehub/lobe-chat.git
synced 2026-06-14 19:50:09 +00:00
🐛 fix: fix tools calling long name length >64 issue (#9697)
* add ToolNameResolver * fix ToolName generate tests * fix tests
This commit is contained in:
@@ -23,7 +23,8 @@
|
||||
"@lobechat/utils": "workspace:*",
|
||||
"debug": "^4.3.4",
|
||||
"immer": "^10.0.3",
|
||||
"lodash-es": "^4.17.21"
|
||||
"lodash-es": "^4.17.21",
|
||||
"ts-md5": "^2.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/debug": "^4.1.12",
|
||||
|
||||
@@ -38,4 +38,4 @@ export type {
|
||||
ToolsGenerationContext,
|
||||
ToolsGenerationResult,
|
||||
} from './tools';
|
||||
export { filterValidManifests, ToolsEngine, validateManifest } from './tools';
|
||||
export { filterValidManifests, ToolNameResolver, ToolsEngine, validateManifest } from './tools';
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
import { ChatToolPayload, MessageToolCall } from '@lobechat/types';
|
||||
import { Md5 } from 'ts-md5';
|
||||
|
||||
import { LobeChatPluginApi, LobeChatPluginManifest } from './types';
|
||||
|
||||
// Tool naming constants
|
||||
const PLUGIN_SCHEMA_SEPARATOR = '____';
|
||||
const PLUGIN_SCHEMA_API_MD5_PREFIX = 'MD5HASH_';
|
||||
|
||||
/**
|
||||
* Tool Name Resolver
|
||||
* Handles tool name generation and resolution for function calling
|
||||
*/
|
||||
export class ToolNameResolver {
|
||||
/**
|
||||
* Generate MD5 hash for tool name shortening
|
||||
* @private
|
||||
*/
|
||||
private genHash(name: string): string {
|
||||
return Md5.hashStr(name).toString().slice(0, 12);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate tool calling name
|
||||
* @param identifier - Plugin identifier
|
||||
* @param name - API name
|
||||
* @param type - Plugin type (default: 'default')
|
||||
* @returns Generated tool name (max 64 characters)
|
||||
*/
|
||||
generate(identifier: string, name: string, type: string = 'default'): string {
|
||||
const pluginType = type && type !== 'default' ? `${PLUGIN_SCHEMA_SEPARATOR}${type}` : '';
|
||||
|
||||
// Step 1: Try normal format
|
||||
let apiName = identifier + PLUGIN_SCHEMA_SEPARATOR + name + pluginType;
|
||||
|
||||
// OpenAI GPT function_call name can't be longer than 64 characters
|
||||
// Step 2: If >= 64, hash the name part
|
||||
if (apiName.length >= 64) {
|
||||
const nameHash = PLUGIN_SCHEMA_API_MD5_PREFIX + this.genHash(name);
|
||||
apiName = identifier + PLUGIN_SCHEMA_SEPARATOR + nameHash + pluginType;
|
||||
|
||||
// Step 3: If still >= 64, also hash the identifier
|
||||
if (apiName.length >= 64) {
|
||||
const identifierHash = PLUGIN_SCHEMA_API_MD5_PREFIX + this.genHash(identifier);
|
||||
apiName = identifierHash + PLUGIN_SCHEMA_SEPARATOR + nameHash + pluginType;
|
||||
}
|
||||
}
|
||||
|
||||
return apiName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve tool calls from AI response back to original tool information
|
||||
* @param toolCalls - Tool calls from AI model response
|
||||
* @param manifests - Available tool manifests mapped by identifier
|
||||
* @returns Resolved tool payloads
|
||||
*/
|
||||
resolve(
|
||||
toolCalls: MessageToolCall[],
|
||||
manifests: Record<string, LobeChatPluginManifest>,
|
||||
): ChatToolPayload[] {
|
||||
return toolCalls
|
||||
.map((toolCall): ChatToolPayload | null => {
|
||||
let [identifier, apiName, type] = toolCall.function.name.split(PLUGIN_SCHEMA_SEPARATOR);
|
||||
|
||||
if (!apiName) return null;
|
||||
|
||||
// Step 1: Resolve hashed identifier if needed
|
||||
if (identifier.startsWith(PLUGIN_SCHEMA_API_MD5_PREFIX)) {
|
||||
const identifierMd5 = identifier.replace(PLUGIN_SCHEMA_API_MD5_PREFIX, '');
|
||||
// Find the manifest by hashed identifier
|
||||
const foundIdentifier = Object.keys(manifests).find(
|
||||
(id) => this.genHash(id) === identifierMd5,
|
||||
);
|
||||
if (foundIdentifier) {
|
||||
identifier = foundIdentifier;
|
||||
}
|
||||
}
|
||||
|
||||
let payload: ChatToolPayload = {
|
||||
apiName,
|
||||
arguments: toolCall.function.arguments,
|
||||
id: toolCall.id,
|
||||
identifier,
|
||||
type: (type ?? 'default') as any,
|
||||
};
|
||||
|
||||
// Step 2: Resolve hashed apiName if needed
|
||||
if (apiName.startsWith(PLUGIN_SCHEMA_API_MD5_PREFIX) && manifests[identifier]) {
|
||||
const md5 = apiName.replace(PLUGIN_SCHEMA_API_MD5_PREFIX, '');
|
||||
const manifest = manifests[identifier];
|
||||
|
||||
const api = manifest?.api.find(
|
||||
(api: LobeChatPluginApi) => this.genHash(api.name) === md5,
|
||||
);
|
||||
if (api) {
|
||||
payload.apiName = api.name;
|
||||
}
|
||||
}
|
||||
|
||||
return payload;
|
||||
})
|
||||
.filter(Boolean) as ChatToolPayload[];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,615 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { ToolNameResolver } from '../ToolNameResolver';
|
||||
|
||||
describe('ToolNameResolver', () => {
|
||||
const resolver = new ToolNameResolver();
|
||||
describe('generate - basic functionality', () => {
|
||||
it('should generate tool name with identifier and api name', () => {
|
||||
const result = resolver.generate('test-plugin', 'myAction');
|
||||
expect(result).toBe('test-plugin____myAction');
|
||||
});
|
||||
|
||||
it('should generate tool name with type suffix', () => {
|
||||
const result = resolver.generate('test-plugin', 'myAction', 'builtin');
|
||||
expect(result).toBe('test-plugin____myAction____builtin');
|
||||
});
|
||||
|
||||
it('should handle default type', () => {
|
||||
const result = resolver.generate('test-plugin', 'myAction', 'default');
|
||||
expect(result).toBe('test-plugin____myAction');
|
||||
});
|
||||
|
||||
it('should handle undefined type as default', () => {
|
||||
const result = resolver.generate('test-plugin', 'myAction');
|
||||
expect(result).toBe('test-plugin____myAction');
|
||||
});
|
||||
});
|
||||
|
||||
describe('generate - long name handling', () => {
|
||||
it('should shorten long action names using hash', () => {
|
||||
// Create a normal identifier with a very long action name
|
||||
const identifier = 'my-plugin';
|
||||
const longActionName =
|
||||
'very-long-action-name-that-will-cause-the-total-length-to-exceed-64-characters';
|
||||
const result = resolver.generate(identifier, longActionName, 'builtin');
|
||||
|
||||
// The result should be shorter than the original would have been
|
||||
const originalLength = `${identifier}____${longActionName}____builtin`.length;
|
||||
expect(result.length).toBeLessThan(originalLength);
|
||||
|
||||
// Should contain the identifier, MD5HASH prefix, and type
|
||||
expect(result).toContain(identifier);
|
||||
expect(result).toContain('MD5HASH_');
|
||||
expect(result).toContain('____builtin');
|
||||
expect(result).toMatch(/^my-plugin____MD5HASH_[a-f0-9]+____builtin$/);
|
||||
});
|
||||
|
||||
it('should handle identifier that is itself long', () => {
|
||||
// Test when identifier itself is very long
|
||||
const veryLongIdentifier = 'very-long-plugin-identifier-that-will-cause-overflow';
|
||||
const actionName = 'action';
|
||||
const result = resolver.generate(veryLongIdentifier, actionName, 'standalone');
|
||||
|
||||
// When both identifier and name cause total >= 64, both get hashed
|
||||
expect(result).toContain('MD5HASH_');
|
||||
expect(result).toContain('____standalone');
|
||||
// Result should be shortened
|
||||
const originalLength = `${veryLongIdentifier}____${actionName}____standalone`.length;
|
||||
expect(result.length).toBeLessThan(originalLength);
|
||||
// With 12-char hashes: MD5HASH_xxx(20) + ____(4) + MD5HASH_xxx(20) + ____(4) + standalone(10) = 58
|
||||
expect(result.length).toBeLessThan(64);
|
||||
});
|
||||
|
||||
it('should keep short names unchanged', () => {
|
||||
const result = resolver.generate('short', 'action', 'type');
|
||||
expect(result).toBe('short____action____type');
|
||||
expect(result.length).toBeLessThan(64);
|
||||
});
|
||||
|
||||
it('should handle edge case at exactly 64 characters', () => {
|
||||
// Create a name that's exactly 64 characters
|
||||
const identifier = 'short-id';
|
||||
const actionName = 'b'.repeat(44);
|
||||
const type = 'type'; // 8 + 4 + 44 + 4 + 4 = 64
|
||||
|
||||
const result = resolver.generate(identifier, actionName, type);
|
||||
|
||||
// When total length >= 64, action name should be hashed
|
||||
// Result format: identifier____MD5HASH_xxx____type
|
||||
expect(result).toContain(identifier);
|
||||
expect(result).toContain('MD5HASH_');
|
||||
expect(result).toContain(type);
|
||||
// The result should be shorter than the original would have been
|
||||
const originalLength = `${identifier}____${actionName}____${type}`.length;
|
||||
expect(result.length).toBeLessThan(originalLength);
|
||||
});
|
||||
});
|
||||
|
||||
describe('generate - special characters and edge cases', () => {
|
||||
it('should handle identifiers with special characters', () => {
|
||||
const result = resolver.generate('my-plugin_v2', 'action-name', 'builtin');
|
||||
expect(result).toBe('my-plugin_v2____action-name____builtin');
|
||||
});
|
||||
|
||||
it('should handle empty action name', () => {
|
||||
const result = resolver.generate('plugin', '', 'builtin');
|
||||
expect(result).toBe('plugin________builtin');
|
||||
});
|
||||
|
||||
it('should handle numeric identifiers and action names', () => {
|
||||
const result = resolver.generate('plugin123', 'action456', 'type789');
|
||||
expect(result).toBe('plugin123____action456____type789');
|
||||
});
|
||||
|
||||
it('should be consistent for same inputs', () => {
|
||||
const result1 = resolver.generate('plugin', 'action', 'type');
|
||||
const result2 = resolver.generate('plugin', 'action', 'type');
|
||||
expect(result1).toBe(result2);
|
||||
});
|
||||
|
||||
it('should produce different results for different inputs', () => {
|
||||
const result1 = resolver.generate('plugin1', 'action', 'type');
|
||||
const result2 = resolver.generate('plugin2', 'action', 'type');
|
||||
expect(result1).not.toBe(result2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('generate - hash consistency', () => {
|
||||
it('should generate consistent hash for same long action name', () => {
|
||||
const identifier = 'plugin';
|
||||
const longActionName = 'very-long-action-name-that-will-also-cause-overflow';
|
||||
|
||||
const result1 = resolver.generate(identifier, longActionName, 'builtin');
|
||||
const result2 = resolver.generate(identifier, longActionName, 'builtin');
|
||||
|
||||
expect(result1).toBe(result2);
|
||||
expect(result1).toContain('MD5HASH_');
|
||||
});
|
||||
|
||||
it('should generate different hashes for different long action names', () => {
|
||||
const identifier = 'plugin';
|
||||
const longActionName1 = 'very-long-action-name-that-will-also-cause-overflow-1';
|
||||
const longActionName2 = 'very-long-action-name-that-will-also-cause-overflow-2';
|
||||
|
||||
const result1 = resolver.generate(identifier, longActionName1, 'builtin');
|
||||
const result2 = resolver.generate(identifier, longActionName2, 'builtin');
|
||||
|
||||
expect(result1).not.toBe(result2);
|
||||
expect(result1).toContain('MD5HASH_');
|
||||
expect(result2).toContain('MD5HASH_');
|
||||
});
|
||||
});
|
||||
|
||||
describe('generate - real-world examples', () => {
|
||||
it('should handle builtin tools correctly', () => {
|
||||
const result = resolver.generate('lobe-image-designer', 'text2image', 'builtin');
|
||||
expect(result).toBe('lobe-image-designer____text2image____builtin');
|
||||
});
|
||||
|
||||
it('should handle web browsing tools correctly', () => {
|
||||
const result = resolver.generate('lobe-web-browsing', 'search', 'builtin');
|
||||
expect(result).toBe('lobe-web-browsing____search____builtin');
|
||||
});
|
||||
|
||||
it('should handle plugin tools correctly', () => {
|
||||
const result = resolver.generate('custom-plugin', 'customAction');
|
||||
expect(result).toBe('custom-plugin____customAction');
|
||||
});
|
||||
});
|
||||
|
||||
describe('resolve - basic functionality', () => {
|
||||
it('should resolve normal tool calls without hashing', () => {
|
||||
const toolCalls = [
|
||||
{
|
||||
function: {
|
||||
arguments: '{"query": "test"}',
|
||||
name: 'test-plugin____myAction____builtin',
|
||||
},
|
||||
id: 'call_1',
|
||||
type: 'function',
|
||||
},
|
||||
];
|
||||
|
||||
const manifests = {
|
||||
'test-plugin': {
|
||||
api: [{ description: 'My action', name: 'myAction', parameters: {} }],
|
||||
identifier: 'test-plugin',
|
||||
meta: {},
|
||||
type: 'builtin' as const,
|
||||
},
|
||||
};
|
||||
|
||||
const result = resolver.resolve(toolCalls, manifests);
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0]).toEqual({
|
||||
apiName: 'myAction',
|
||||
arguments: '{"query": "test"}',
|
||||
id: 'call_1',
|
||||
identifier: 'test-plugin',
|
||||
type: 'builtin' as const,
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle default type correctly', () => {
|
||||
const toolCalls = [
|
||||
{
|
||||
function: {
|
||||
arguments: '{}',
|
||||
name: 'plugin____action',
|
||||
},
|
||||
id: 'call_1',
|
||||
type: 'function',
|
||||
},
|
||||
];
|
||||
|
||||
const manifests = {
|
||||
plugin: {
|
||||
api: [{ description: 'Action', name: 'action', parameters: {} }],
|
||||
identifier: 'plugin',
|
||||
meta: {},
|
||||
},
|
||||
};
|
||||
|
||||
const result = resolver.resolve(toolCalls, manifests);
|
||||
|
||||
expect(result[0].type).toBe('default');
|
||||
});
|
||||
|
||||
it('should handle empty tool calls array', () => {
|
||||
const result = resolver.resolve([], {});
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it('should handle multiple tool calls', () => {
|
||||
const toolCalls = [
|
||||
{
|
||||
function: { arguments: '{}', name: 'plugin1____action1' },
|
||||
id: 'call_1',
|
||||
type: 'function',
|
||||
},
|
||||
{
|
||||
function: { arguments: '{}', name: 'plugin2____action2____builtin' },
|
||||
id: 'call_2',
|
||||
type: 'function',
|
||||
},
|
||||
];
|
||||
|
||||
const manifests = {
|
||||
plugin1: {
|
||||
api: [{ description: '', name: 'action1', parameters: {} }],
|
||||
identifier: 'plugin1',
|
||||
meta: {},
|
||||
},
|
||||
plugin2: {
|
||||
api: [{ description: '', name: 'action2', parameters: {} }],
|
||||
identifier: 'plugin2',
|
||||
meta: {},
|
||||
type: 'builtin' as const,
|
||||
},
|
||||
};
|
||||
|
||||
const result = resolver.resolve(toolCalls, manifests);
|
||||
|
||||
expect(result).toHaveLength(2);
|
||||
expect(result[0].identifier).toBe('plugin1');
|
||||
expect(result[1].identifier).toBe('plugin2');
|
||||
});
|
||||
});
|
||||
|
||||
describe('resolve - hashed apiName', () => {
|
||||
it('should resolve hashed apiName back to original', () => {
|
||||
const identifier = 'my-plugin';
|
||||
const longActionName =
|
||||
'very-long-action-name-that-will-cause-the-total-length-to-exceed-64-characters';
|
||||
|
||||
// Generate a hashed tool name
|
||||
const hashedToolName = resolver.generate(identifier, longActionName, 'builtin');
|
||||
|
||||
// Create tool call with hashed name
|
||||
const toolCalls = [
|
||||
{
|
||||
function: {
|
||||
arguments: '{"param": "value"}',
|
||||
name: hashedToolName,
|
||||
},
|
||||
id: 'call_1',
|
||||
type: 'function',
|
||||
},
|
||||
];
|
||||
|
||||
// Create manifest with original api name
|
||||
const manifests = {
|
||||
[identifier]: {
|
||||
api: [{ description: 'Long action', name: longActionName, parameters: {} }],
|
||||
identifier,
|
||||
meta: {},
|
||||
type: 'builtin' as const,
|
||||
},
|
||||
};
|
||||
|
||||
const result = resolver.resolve(toolCalls, manifests);
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].apiName).toBe(longActionName);
|
||||
expect(result[0].identifier).toBe(identifier);
|
||||
expect(result[0].type).toBe('builtin');
|
||||
});
|
||||
|
||||
it('should keep hashed apiName if manifest not found', () => {
|
||||
const toolCalls = [
|
||||
{
|
||||
function: {
|
||||
arguments: '{}',
|
||||
name: 'plugin____MD5HASH_abc123def456',
|
||||
},
|
||||
id: 'call_1',
|
||||
type: 'function',
|
||||
},
|
||||
];
|
||||
|
||||
const result = resolver.resolve(toolCalls, {});
|
||||
|
||||
expect(result[0].apiName).toBe('MD5HASH_abc123def456');
|
||||
});
|
||||
|
||||
it('should keep hashed apiName if api not found in manifest', () => {
|
||||
const toolCalls = [
|
||||
{
|
||||
function: {
|
||||
arguments: '{}',
|
||||
name: 'plugin____MD5HASH_abc123def456',
|
||||
},
|
||||
id: 'call_1',
|
||||
type: 'function',
|
||||
},
|
||||
];
|
||||
|
||||
const manifests = {
|
||||
plugin: {
|
||||
api: [{ description: '', name: 'differentAction', parameters: {} }],
|
||||
identifier: 'plugin',
|
||||
meta: {},
|
||||
},
|
||||
};
|
||||
|
||||
const result = resolver.resolve(toolCalls, manifests);
|
||||
|
||||
expect(result[0].apiName).toBe('MD5HASH_abc123def456');
|
||||
});
|
||||
});
|
||||
|
||||
describe('resolve - hashed identifier', () => {
|
||||
it('should resolve hashed identifier back to original', () => {
|
||||
const veryLongIdentifier = 'very-long-plugin-identifier-that-will-cause-overflow';
|
||||
const actionName = 'action';
|
||||
|
||||
// Generate a hashed tool name (both identifier and name will be hashed)
|
||||
const hashedToolName = resolver.generate(veryLongIdentifier, actionName, 'standalone');
|
||||
|
||||
// Create tool call with hashed name
|
||||
const toolCalls = [
|
||||
{
|
||||
function: {
|
||||
arguments: '{"test": true}',
|
||||
name: hashedToolName,
|
||||
},
|
||||
id: 'call_1',
|
||||
type: 'function',
|
||||
},
|
||||
];
|
||||
|
||||
// Create manifest with original identifier
|
||||
const manifests = {
|
||||
[veryLongIdentifier]: {
|
||||
api: [{ description: 'Action', name: actionName, parameters: {} }],
|
||||
identifier: veryLongIdentifier,
|
||||
meta: {},
|
||||
type: 'standalone' as const,
|
||||
},
|
||||
};
|
||||
|
||||
const result = resolver.resolve(toolCalls, manifests);
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].identifier).toBe(veryLongIdentifier);
|
||||
expect(result[0].apiName).toBe(actionName);
|
||||
expect(result[0].type).toBe('standalone');
|
||||
});
|
||||
|
||||
it('should keep hashed identifier if not found in manifests', () => {
|
||||
const toolCalls = [
|
||||
{
|
||||
function: {
|
||||
arguments: '{}',
|
||||
name: 'MD5HASH_abc123def456____action',
|
||||
},
|
||||
id: 'call_1',
|
||||
type: 'function',
|
||||
},
|
||||
];
|
||||
|
||||
const manifests = {
|
||||
'different-plugin': {
|
||||
api: [{ description: '', name: 'action', parameters: {} }],
|
||||
identifier: 'different-plugin',
|
||||
meta: {},
|
||||
},
|
||||
};
|
||||
|
||||
const result = resolver.resolve(toolCalls, manifests);
|
||||
|
||||
expect(result[0].identifier).toBe('MD5HASH_abc123def456');
|
||||
});
|
||||
});
|
||||
|
||||
describe('resolve - both identifier and apiName hashed', () => {
|
||||
it('should resolve both hashed identifier and apiName', () => {
|
||||
const veryLongIdentifier = 'very-long-plugin-identifier-that-will-cause-overflow';
|
||||
const veryLongActionName = 'very-long-action-name-that-will-also-cause-overflow';
|
||||
|
||||
// Generate hashed tool name (both will be hashed)
|
||||
const hashedToolName = resolver.generate(
|
||||
veryLongIdentifier,
|
||||
veryLongActionName,
|
||||
'standalone',
|
||||
);
|
||||
|
||||
// Verify both are hashed
|
||||
expect(hashedToolName).toContain('MD5HASH_');
|
||||
expect(hashedToolName).not.toContain(veryLongIdentifier);
|
||||
expect(hashedToolName).not.toContain(veryLongActionName);
|
||||
|
||||
// Create tool call with fully hashed name
|
||||
const toolCalls = [
|
||||
{
|
||||
function: {
|
||||
arguments: '{"data": "test"}',
|
||||
name: hashedToolName,
|
||||
},
|
||||
id: 'call_1',
|
||||
type: 'function',
|
||||
},
|
||||
];
|
||||
|
||||
// Create manifest
|
||||
const manifests = {
|
||||
[veryLongIdentifier]: {
|
||||
api: [{ description: 'Long action', name: veryLongActionName, parameters: {} }],
|
||||
identifier: veryLongIdentifier,
|
||||
meta: {},
|
||||
type: 'standalone' as const,
|
||||
},
|
||||
};
|
||||
|
||||
const result = resolver.resolve(toolCalls, manifests);
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].identifier).toBe(veryLongIdentifier);
|
||||
expect(result[0].apiName).toBe(veryLongActionName);
|
||||
expect(result[0].type).toBe('standalone');
|
||||
});
|
||||
});
|
||||
|
||||
describe('resolve - edge cases', () => {
|
||||
it('should filter out invalid tool calls with missing apiName', () => {
|
||||
const toolCalls = [
|
||||
{
|
||||
function: {
|
||||
arguments: '{}',
|
||||
name: 'invalid-name-without-separator',
|
||||
},
|
||||
id: 'call_1',
|
||||
type: 'function',
|
||||
},
|
||||
];
|
||||
|
||||
const result = resolver.resolve(toolCalls, {});
|
||||
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it('should handle tool calls with different types', () => {
|
||||
const toolCalls = [
|
||||
{
|
||||
function: { arguments: '{}', name: 'plugin1____action1____builtin' },
|
||||
id: 'call_1',
|
||||
type: 'function',
|
||||
},
|
||||
{
|
||||
function: { arguments: '{}', name: 'plugin2____action2____standalone' },
|
||||
id: 'call_2',
|
||||
type: 'function',
|
||||
},
|
||||
{
|
||||
function: { arguments: '{}', name: 'plugin3____action3____mcp' },
|
||||
id: 'call_3',
|
||||
type: 'function',
|
||||
},
|
||||
];
|
||||
|
||||
const manifests = {
|
||||
plugin1: {
|
||||
api: [{ description: '', name: 'action1', parameters: {} }],
|
||||
identifier: 'plugin1',
|
||||
meta: {},
|
||||
type: 'builtin' as const,
|
||||
},
|
||||
plugin2: {
|
||||
api: [{ description: '', name: 'action2', parameters: {} }],
|
||||
identifier: 'plugin2',
|
||||
meta: {},
|
||||
type: 'standalone' as const,
|
||||
},
|
||||
plugin3: {
|
||||
api: [{ description: '', name: 'action3', parameters: {} }],
|
||||
identifier: 'plugin3',
|
||||
meta: {},
|
||||
type: 'mcp' as const,
|
||||
},
|
||||
};
|
||||
|
||||
const result = resolver.resolve(toolCalls, manifests);
|
||||
|
||||
expect(result).toHaveLength(3);
|
||||
expect(result[0].type).toBe('builtin');
|
||||
expect(result[1].type).toBe('standalone');
|
||||
expect(result[2].type).toBe('mcp');
|
||||
});
|
||||
});
|
||||
|
||||
describe('resolve - real-world integration', () => {
|
||||
it('should handle complete generate-resolve roundtrip', () => {
|
||||
const identifier = 'lobe-image-designer';
|
||||
const apiName = 'text2image';
|
||||
const type = 'builtin' as const;
|
||||
|
||||
// Generate tool name
|
||||
const toolName = resolver.generate(identifier, apiName, type);
|
||||
|
||||
// Simulate tool call from AI
|
||||
const toolCalls = [
|
||||
{
|
||||
function: {
|
||||
arguments: '{"prompt": "a beautiful sunset", "size": "1024x1024"}',
|
||||
name: toolName,
|
||||
},
|
||||
id: 'call_abc123',
|
||||
type: 'function',
|
||||
},
|
||||
];
|
||||
|
||||
// Create manifest
|
||||
const manifests = {
|
||||
[identifier]: {
|
||||
api: [
|
||||
{
|
||||
description: 'Generate image from text',
|
||||
name: apiName,
|
||||
parameters: {
|
||||
properties: {
|
||||
prompt: { type: 'string' },
|
||||
size: { type: 'string' },
|
||||
},
|
||||
type: 'object',
|
||||
},
|
||||
},
|
||||
],
|
||||
identifier,
|
||||
meta: { avatar: '', description: '', title: 'Image Designer' },
|
||||
type: 'builtin' as const,
|
||||
},
|
||||
};
|
||||
|
||||
// Resolve tool calls
|
||||
const result = resolver.resolve(toolCalls, manifests);
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0]).toEqual({
|
||||
apiName,
|
||||
arguments: '{"prompt": "a beautiful sunset", "size": "1024x1024"}',
|
||||
id: 'call_abc123',
|
||||
identifier,
|
||||
type,
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle roundtrip with long names requiring hashing', () => {
|
||||
const longIdentifier = 'very-long-plugin-identifier-that-exceeds-normal-length';
|
||||
const longApiName = 'very-long-api-name-that-also-exceeds-normal-length-limits';
|
||||
const type = 'standalone' as const;
|
||||
|
||||
// Generate hashed tool name
|
||||
const toolName = resolver.generate(longIdentifier, longApiName, type);
|
||||
expect(toolName.length).toBeLessThan(64);
|
||||
|
||||
// Create tool call
|
||||
const toolCalls = [
|
||||
{
|
||||
function: { arguments: '{"input": "data"}', name: toolName },
|
||||
id: 'call_xyz789',
|
||||
type: 'function',
|
||||
},
|
||||
];
|
||||
|
||||
// Create manifest
|
||||
const manifests = {
|
||||
[longIdentifier]: {
|
||||
api: [{ description: 'Long API', name: longApiName, parameters: {} }],
|
||||
identifier: longIdentifier,
|
||||
meta: {},
|
||||
type: 'standalone' as const,
|
||||
},
|
||||
};
|
||||
|
||||
// Resolve should restore original names
|
||||
const result = resolver.resolve(toolCalls, manifests);
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].identifier).toBe(longIdentifier);
|
||||
expect(result[0].apiName).toBe(longApiName);
|
||||
expect(result[0].type).toBe(type);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,152 +0,0 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { generateToolName } from '../utils';
|
||||
|
||||
describe('generateToolName', () => {
|
||||
describe('basic functionality', () => {
|
||||
it('should generate tool name with identifier and api name', () => {
|
||||
const result = generateToolName('test-plugin', 'myAction');
|
||||
expect(result).toBe('test-plugin____myAction');
|
||||
});
|
||||
|
||||
it('should generate tool name with type suffix', () => {
|
||||
const result = generateToolName('test-plugin', 'myAction', 'builtin');
|
||||
expect(result).toBe('test-plugin____myAction____builtin');
|
||||
});
|
||||
|
||||
it('should handle default type', () => {
|
||||
const result = generateToolName('test-plugin', 'myAction', 'default');
|
||||
expect(result).toBe('test-plugin____myAction');
|
||||
});
|
||||
|
||||
it('should handle undefined type as default', () => {
|
||||
const result = generateToolName('test-plugin', 'myAction');
|
||||
expect(result).toBe('test-plugin____myAction');
|
||||
});
|
||||
});
|
||||
|
||||
describe('long name handling', () => {
|
||||
it('should shorten long action names using hash', () => {
|
||||
// Create a normal identifier with a very long action name
|
||||
const identifier = 'my-plugin';
|
||||
const longActionName = 'very-long-action-name-that-will-cause-the-total-length-to-exceed-64-characters';
|
||||
const result = generateToolName(identifier, longActionName, 'builtin');
|
||||
|
||||
// The result should be shorter than the original would have been
|
||||
const originalLength = `${identifier}____${longActionName}____builtin`.length;
|
||||
expect(result.length).toBeLessThan(originalLength);
|
||||
|
||||
// Should contain the identifier, MD5HASH prefix, and type
|
||||
expect(result).toContain(identifier);
|
||||
expect(result).toContain('MD5HASH_');
|
||||
expect(result).toContain('____builtin');
|
||||
expect(result).toMatch(/^my-plugin____MD5HASH_[a-f0-9]+____builtin$/);
|
||||
});
|
||||
|
||||
it('should handle identifier that is itself long', () => {
|
||||
// Test the original limitation - when identifier itself is very long
|
||||
const veryLongIdentifier = 'very-long-plugin-identifier-that-will-cause-overflow';
|
||||
const actionName = 'action';
|
||||
const result = generateToolName(veryLongIdentifier, actionName, 'builtin');
|
||||
|
||||
// When the total length exceeds 64, even short action names get hashed
|
||||
expect(result).toContain(veryLongIdentifier);
|
||||
expect(result).toContain('MD5HASH_');
|
||||
expect(result).toContain('____builtin');
|
||||
|
||||
// Verify the pattern matches the expected format
|
||||
expect(result).toMatch(new RegExp(`^${veryLongIdentifier.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}____MD5HASH_[a-f0-9]+____builtin$`));
|
||||
});
|
||||
|
||||
it('should keep short names unchanged', () => {
|
||||
const result = generateToolName('short', 'action', 'type');
|
||||
expect(result).toBe('short____action____type');
|
||||
expect(result.length).toBeLessThan(64);
|
||||
});
|
||||
|
||||
it('should handle edge case at exactly 64 characters', () => {
|
||||
// Create a name that's exactly 64 characters
|
||||
const identifier = 'a'.repeat(20);
|
||||
const actionName = 'b'.repeat(20);
|
||||
const type = 'c'.repeat(16); // 20 + 4 + 20 + 4 + 16 = 64
|
||||
|
||||
const result = generateToolName(identifier, actionName, type);
|
||||
|
||||
// Should be shortened because it's >= 64
|
||||
expect(result.length).toBeLessThan(64);
|
||||
expect(result).toContain('MD5HASH_');
|
||||
});
|
||||
});
|
||||
|
||||
describe('special characters and edge cases', () => {
|
||||
it('should handle identifiers with special characters', () => {
|
||||
const result = generateToolName('my-plugin_v2', 'action-name', 'builtin');
|
||||
expect(result).toBe('my-plugin_v2____action-name____builtin');
|
||||
});
|
||||
|
||||
it('should handle empty action name', () => {
|
||||
const result = generateToolName('plugin', '', 'builtin');
|
||||
expect(result).toBe('plugin________builtin');
|
||||
});
|
||||
|
||||
it('should handle numeric identifiers and action names', () => {
|
||||
const result = generateToolName('plugin123', 'action456', 'type789');
|
||||
expect(result).toBe('plugin123____action456____type789');
|
||||
});
|
||||
|
||||
it('should be consistent for same inputs', () => {
|
||||
const result1 = generateToolName('plugin', 'action', 'type');
|
||||
const result2 = generateToolName('plugin', 'action', 'type');
|
||||
expect(result1).toBe(result2);
|
||||
});
|
||||
|
||||
it('should produce different results for different inputs', () => {
|
||||
const result1 = generateToolName('plugin1', 'action', 'type');
|
||||
const result2 = generateToolName('plugin2', 'action', 'type');
|
||||
expect(result1).not.toBe(result2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('hash consistency', () => {
|
||||
it('should generate consistent hash for same long action name', () => {
|
||||
const identifier = 'plugin';
|
||||
const longActionName = 'very-long-action-name-that-will-also-cause-overflow';
|
||||
|
||||
const result1 = generateToolName(identifier, longActionName, 'builtin');
|
||||
const result2 = generateToolName(identifier, longActionName, 'builtin');
|
||||
|
||||
expect(result1).toBe(result2);
|
||||
expect(result1).toContain('MD5HASH_');
|
||||
});
|
||||
|
||||
it('should generate different hashes for different long action names', () => {
|
||||
const identifier = 'plugin';
|
||||
const longActionName1 = 'very-long-action-name-that-will-also-cause-overflow-1';
|
||||
const longActionName2 = 'very-long-action-name-that-will-also-cause-overflow-2';
|
||||
|
||||
const result1 = generateToolName(identifier, longActionName1, 'builtin');
|
||||
const result2 = generateToolName(identifier, longActionName2, 'builtin');
|
||||
|
||||
expect(result1).not.toBe(result2);
|
||||
expect(result1).toContain('MD5HASH_');
|
||||
expect(result2).toContain('MD5HASH_');
|
||||
});
|
||||
});
|
||||
|
||||
describe('real-world examples', () => {
|
||||
it('should handle builtin tools correctly', () => {
|
||||
const result = generateToolName('lobe-image-designer', 'text2image', 'builtin');
|
||||
expect(result).toBe('lobe-image-designer____text2image____builtin');
|
||||
});
|
||||
|
||||
it('should handle web browsing tools correctly', () => {
|
||||
const result = generateToolName('lobe-web-browsing', 'search');
|
||||
expect(result).toBe('lobe-web-browsing____search');
|
||||
});
|
||||
|
||||
it('should handle plugin tools correctly', () => {
|
||||
const result = generateToolName('custom-plugin', 'customAction', 'plugin');
|
||||
expect(result).toBe('custom-plugin____customAction____plugin');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,9 @@
|
||||
// Core ToolsEngine class
|
||||
export { ToolsEngine } from './ToolsEngine';
|
||||
|
||||
// Tool Name Resolver
|
||||
export { ToolNameResolver } from './ToolNameResolver';
|
||||
|
||||
// Types and interfaces
|
||||
export type {
|
||||
FunctionCallChecker,
|
||||
@@ -13,4 +16,4 @@ export type {
|
||||
} from './types';
|
||||
|
||||
// Utility functions
|
||||
export { filterValidManifests, generateToolName, validateManifest } from './utils';
|
||||
export { filterValidManifests, validateManifest } from './utils';
|
||||
|
||||
@@ -1,42 +1,19 @@
|
||||
import { ToolNameResolver } from './ToolNameResolver';
|
||||
import { LobeChatPluginManifest } from './types';
|
||||
|
||||
// Tool naming constants
|
||||
const PLUGIN_SCHEMA_SEPARATOR = '____';
|
||||
const PLUGIN_SCHEMA_API_MD5_PREFIX = 'MD5HASH_';
|
||||
|
||||
/**
|
||||
* Simple hash function for tool name shortening
|
||||
*/
|
||||
const genToolCallShortMD5Hash = (name: string): string => {
|
||||
// Simple hash function for tool names (fallback if no crypto available)
|
||||
let hash = 0;
|
||||
for (let i = 0; i < name.length; i++) {
|
||||
const char = name.charCodeAt(i);
|
||||
hash = ((hash << 5) - hash) + char;
|
||||
hash = hash & hash; // Convert to 32bit integer
|
||||
}
|
||||
return Math.abs(hash).toString(16).slice(0, 16);
|
||||
};
|
||||
// Create a singleton instance for backward compatibility
|
||||
const resolver = new ToolNameResolver();
|
||||
|
||||
/**
|
||||
* Generate tool calling name
|
||||
* Default tool name generation logic (copied from @lobechat/utils)
|
||||
* @deprecated Use ToolNameResolver.generate() instead
|
||||
*/
|
||||
export const generateToolName = (identifier: string, name: string, type: string = 'default'): string => {
|
||||
const pluginType = type && type !== 'default' ? `${PLUGIN_SCHEMA_SEPARATOR + type}` : '';
|
||||
|
||||
// Use plugin identifier as prefix to avoid conflicts
|
||||
let apiName = identifier + PLUGIN_SCHEMA_SEPARATOR + name + pluginType;
|
||||
|
||||
// OpenAI GPT function_call name can't be longer than 64 characters
|
||||
// So we need to use hash to shorten the name
|
||||
// and then find the correct apiName in response by hash
|
||||
if (apiName.length >= 64) {
|
||||
const hashContent = PLUGIN_SCHEMA_API_MD5_PREFIX + genToolCallShortMD5Hash(name);
|
||||
apiName = identifier + PLUGIN_SCHEMA_SEPARATOR + hashContent + pluginType;
|
||||
}
|
||||
|
||||
return apiName;
|
||||
export const generateToolName = (
|
||||
identifier: string,
|
||||
name: string,
|
||||
type: string = 'default',
|
||||
): string => {
|
||||
return resolver.generate(identifier, name, type);
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -8,7 +8,6 @@ export * from './parseModels';
|
||||
export * from './pricing';
|
||||
export * from './safeParseJSON';
|
||||
export * from './sleep';
|
||||
export * from './toolCall';
|
||||
export * from './uriParser';
|
||||
export * from './url';
|
||||
export * from './uuid';
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
import { PLUGIN_SCHEMA_API_MD5_PREFIX, PLUGIN_SCHEMA_SEPARATOR } from '@lobechat/const';
|
||||
import { Md5 } from 'ts-md5';
|
||||
|
||||
// OpenAI GPT function_call name can't be longer than 64 characters
|
||||
// So we need to use md5 to shorten the name
|
||||
export const genToolCallShortMD5Hash = (name: string) => Md5.hashStr(name).toString().slice(0, 16);
|
||||
|
||||
export const genToolCallingName = (identifier: string, name: string, type: string = 'default') => {
|
||||
const pluginType = type && type !== 'default' ? `${PLUGIN_SCHEMA_SEPARATOR + type}` : '';
|
||||
|
||||
// 将插件的 identifier 作为前缀,避免重复
|
||||
let apiName = identifier + PLUGIN_SCHEMA_SEPARATOR + name + pluginType;
|
||||
|
||||
// OpenAI GPT function_call name can't be longer than 64 characters
|
||||
// So we need to use md5 to shorten the name
|
||||
// and then find the correct apiName in response by md5
|
||||
if (apiName.length >= 64) {
|
||||
const md5Content = PLUGIN_SCHEMA_API_MD5_PREFIX + genToolCallShortMD5Hash(name);
|
||||
|
||||
apiName = identifier + PLUGIN_SCHEMA_SEPARATOR + md5Content + pluginType;
|
||||
}
|
||||
|
||||
return apiName;
|
||||
};
|
||||
@@ -1,11 +1,8 @@
|
||||
import { ChatCompletionTool, OpenAIPluginManifest } from '@lobechat/types';
|
||||
import { OpenAIPluginManifest } from '@lobechat/types';
|
||||
import { LobeChatPluginManifest, pluginManifestSchema } from '@lobehub/chat-plugin-sdk';
|
||||
import { uniqBy } from 'lodash-es';
|
||||
|
||||
import { API_ENDPOINTS } from '@/services/_url';
|
||||
|
||||
import { genToolCallingName } from './toolCall';
|
||||
|
||||
const fetchJSON = async <T = any>(url: string, proxy = false): Promise<T> => {
|
||||
// 2. 发送请求
|
||||
let res: Response;
|
||||
@@ -127,20 +124,3 @@ export const getToolManifest = async (
|
||||
|
||||
return data;
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
export const convertPluginManifestToToolsCalling = (
|
||||
manifests: LobeChatPluginManifest[],
|
||||
): ChatCompletionTool[] => {
|
||||
const list = manifests.flatMap((manifest) =>
|
||||
manifest.api.map((m) => ({
|
||||
description: m.description,
|
||||
name: genToolCallingName(manifest.identifier, m.name, manifest.type),
|
||||
parameters: m.parameters,
|
||||
})),
|
||||
);
|
||||
|
||||
return uniqBy(list, 'name').map((i) => ({ function: i, type: 'function' }));
|
||||
};
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
SystemRoleInjector,
|
||||
ToolCallProcessor,
|
||||
ToolMessageReorder,
|
||||
ToolNameResolver,
|
||||
ToolSystemRoleProvider,
|
||||
} from '@lobechat/context-engine';
|
||||
import { historySummaryPrompt } from '@lobechat/prompts';
|
||||
@@ -21,7 +22,6 @@ import { VARIABLE_GENERATORS } from '@lobechat/utils/client';
|
||||
import { isCanUseFC } from '@/helpers/isCanUseFC';
|
||||
import { getToolStoreState } from '@/store/tool';
|
||||
import { toolSelectors } from '@/store/tool/selectors';
|
||||
import { genToolCallingName } from '@/utils/toolCall';
|
||||
|
||||
import { isCanUseVideo, isCanUseVision } from './helper';
|
||||
|
||||
@@ -52,6 +52,8 @@ export const contextEngineering = async ({
|
||||
sessionId,
|
||||
isWelcomeQuestion,
|
||||
}: ContextEngineeringContext): Promise<OpenAIChatMessage[]> => {
|
||||
const toolNameResolver = new ToolNameResolver();
|
||||
|
||||
const pipeline = new ContextEngine({
|
||||
pipeline: [
|
||||
// 1. History truncation (MUST be first, before any message injection)
|
||||
@@ -105,7 +107,12 @@ export const contextEngineering = async ({
|
||||
}),
|
||||
|
||||
// 9. Tool call processing
|
||||
new ToolCallProcessor({ genToolCallingName, isCanUseFC, model, provider }),
|
||||
new ToolCallProcessor({
|
||||
genToolCallingName: toolNameResolver.generate.bind(toolNameResolver),
|
||||
isCanUseFC,
|
||||
model,
|
||||
provider,
|
||||
}),
|
||||
|
||||
// 10. Tool message reordering
|
||||
new ToolMessageReorder(),
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { ToolNameResolver } from '@lobechat/context-engine';
|
||||
import { act, renderHook } from '@testing-library/react';
|
||||
import { Mock, afterEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
@@ -11,7 +12,6 @@ import { useChatStore } from '@/store/chat/store';
|
||||
import { messageMapKey } from '@/store/chat/utils/messageMapKey';
|
||||
import { useToolStore } from '@/store/tool';
|
||||
import { ChatMessage, ChatToolPayload, MessageToolCall } from '@/types/message';
|
||||
import { genToolCallShortMD5Hash } from '@/utils/toolCall';
|
||||
|
||||
const invokeStandaloneTypePlugin = useChatStore.getState().invokeStandaloneTypePlugin;
|
||||
|
||||
@@ -1045,7 +1045,16 @@ describe('ChatPluginAction', () => {
|
||||
|
||||
it('should handle MD5 hashed API names', () => {
|
||||
const apiName = 'testApi';
|
||||
const md5Hash = genToolCallShortMD5Hash(apiName);
|
||||
const resolver = new ToolNameResolver();
|
||||
// Generate a very long name to force MD5 hashing
|
||||
const longApiName =
|
||||
'very-long-action-name-that-will-cause-the-total-length-to-exceed-64-characters';
|
||||
const toolName = resolver.generate('plugin1', longApiName, 'default');
|
||||
|
||||
// Extract the MD5 part from the generated name
|
||||
const parts = toolName.split(PLUGIN_SCHEMA_SEPARATOR);
|
||||
const md5Hash = parts[1].replace(PLUGIN_SCHEMA_API_MD5_PREFIX, '');
|
||||
|
||||
const toolCalls: MessageToolCall[] = [
|
||||
{
|
||||
id: 'tool1',
|
||||
@@ -1069,7 +1078,7 @@ describe('ChatPluginAction', () => {
|
||||
identifier: 'plugin1',
|
||||
api: [
|
||||
{
|
||||
name: apiName,
|
||||
name: longApiName,
|
||||
parameters: { type: 'object', properties: {} },
|
||||
description: 'abc',
|
||||
},
|
||||
@@ -1085,7 +1094,7 @@ describe('ChatPluginAction', () => {
|
||||
|
||||
const transformed = result.current.internal_transformToolCalls(toolCalls);
|
||||
|
||||
expect(transformed[0].apiName).toBe(apiName);
|
||||
expect(transformed[0].apiName).toBe(longApiName);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
/* eslint-disable sort-keys-fix/sort-keys-fix, typescript-sort-keys/interface */
|
||||
import { ToolNameResolver } from '@lobechat/context-engine';
|
||||
import { ChatErrorType } from '@lobechat/types';
|
||||
import { PluginErrorType } from '@lobehub/chat-plugin-sdk';
|
||||
import { LobeChatPluginManifest, PluginErrorType } from '@lobehub/chat-plugin-sdk';
|
||||
import isEqual from 'fast-deep-equal';
|
||||
import { t } from 'i18next';
|
||||
import { StateCreator } from 'zustand/vanilla';
|
||||
|
||||
import { LOADING_FLAT } from '@/const/message';
|
||||
import { PLUGIN_SCHEMA_API_MD5_PREFIX, PLUGIN_SCHEMA_SEPARATOR } from '@/const/plugin';
|
||||
import { chatService } from '@/services/chat';
|
||||
import { mcpService } from '@/services/mcp';
|
||||
import { messageService } from '@/services/message';
|
||||
@@ -25,7 +25,6 @@ import {
|
||||
import { merge } from '@/utils/merge';
|
||||
import { safeParseJSON } from '@/utils/safeParseJSON';
|
||||
import { setNamespace } from '@/utils/storeDebug';
|
||||
import { genToolCallShortMD5Hash } from '@/utils/toolCall';
|
||||
|
||||
import { chatSelectors } from '../message/selectors';
|
||||
import { threadSelectors } from '../thread/selectors';
|
||||
@@ -504,36 +503,28 @@ export const chatPlugin: StateCreator<
|
||||
},
|
||||
|
||||
internal_transformToolCalls: (toolCalls) => {
|
||||
return toolCalls
|
||||
.map((toolCall): ChatToolPayload | null => {
|
||||
let payload: ChatToolPayload;
|
||||
const toolNameResolver = new ToolNameResolver();
|
||||
|
||||
const [identifier, apiName, type] = toolCall.function.name.split(PLUGIN_SCHEMA_SEPARATOR);
|
||||
// Build manifests map from tool store
|
||||
const toolStoreState = useToolStore.getState();
|
||||
const manifests: Record<string, LobeChatPluginManifest> = {};
|
||||
|
||||
if (!apiName) return null;
|
||||
// Get all installed plugins
|
||||
const installedPlugins = pluginSelectors.installedPlugins(toolStoreState);
|
||||
for (const plugin of installedPlugins) {
|
||||
if (plugin.manifest) {
|
||||
manifests[plugin.identifier] = plugin.manifest as LobeChatPluginManifest;
|
||||
}
|
||||
}
|
||||
|
||||
payload = {
|
||||
apiName,
|
||||
arguments: toolCall.function.arguments,
|
||||
id: toolCall.id,
|
||||
identifier,
|
||||
type: (type ?? 'default') as any,
|
||||
};
|
||||
// Get all builtin tools
|
||||
for (const tool of builtinTools) {
|
||||
if (tool.manifest) {
|
||||
manifests[tool.identifier] = tool.manifest as LobeChatPluginManifest;
|
||||
}
|
||||
}
|
||||
|
||||
// if the apiName is md5, try to find the correct apiName in the plugins
|
||||
if (apiName.startsWith(PLUGIN_SCHEMA_API_MD5_PREFIX)) {
|
||||
const md5 = apiName.replace(PLUGIN_SCHEMA_API_MD5_PREFIX, '');
|
||||
const manifest = pluginSelectors.getToolManifestById(identifier)(useToolStore.getState());
|
||||
|
||||
const api = manifest?.api.find((api) => genToolCallShortMD5Hash(api.name) === md5);
|
||||
if (api) {
|
||||
payload.apiName = api.name;
|
||||
}
|
||||
}
|
||||
|
||||
return payload;
|
||||
})
|
||||
.filter(Boolean) as ChatToolPayload[];
|
||||
return toolNameResolver.resolve(toolCalls, manifests);
|
||||
},
|
||||
internal_updatePluginError: async (id, error) => {
|
||||
const { refreshMessages } = get();
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { ToolNameResolver } from '@lobechat/context-engine';
|
||||
import { pluginPrompts } from '@lobechat/prompts';
|
||||
import { LobeChatPluginManifest } from '@lobehub/chat-plugin-sdk';
|
||||
|
||||
@@ -5,13 +6,14 @@ import { MetaData } from '@/types/meta';
|
||||
import { LobeToolMeta } from '@/types/tool/tool';
|
||||
import { globalAgentContextManager } from '@/utils/client/GlobalAgentContextManager';
|
||||
import { hydrationPrompt } from '@/utils/promptTemplate';
|
||||
import { genToolCallingName } from '@/utils/toolCall';
|
||||
|
||||
import { pluginHelpers } from '../helpers';
|
||||
import { ToolStoreState } from '../initialState';
|
||||
import { builtinToolSelectors } from '../slices/builtin/selectors';
|
||||
import { pluginSelectors } from '../slices/plugin/selectors';
|
||||
|
||||
const toolNameResolver = new ToolNameResolver();
|
||||
|
||||
const enabledSystemRoles =
|
||||
(tools: string[] = []) =>
|
||||
(s: ToolStoreState) => {
|
||||
@@ -36,7 +38,7 @@ const enabledSystemRoles =
|
||||
return {
|
||||
apis: manifest.api.map((m) => ({
|
||||
desc: m.description,
|
||||
name: genToolCallingName(manifest.identifier, m.name, manifest.type),
|
||||
name: toolNameResolver.generate(manifest.identifier, m.name, manifest.type),
|
||||
})),
|
||||
identifier: manifest.identifier,
|
||||
name: title,
|
||||
|
||||
Reference in New Issue
Block a user