feat(ai): 优化内置工具目录检索与参数提示

- 为内置工具目录增加关键词搜索和结果计数

- 参数提示补充类型、默认值、枚举和示例信息

- 补充目录渲染和参数摘要提取测试
This commit is contained in:
Syngnat
2026-06-11 22:29:37 +08:00
parent 6f4e80c749
commit cba8ff394c
4 changed files with 304 additions and 34 deletions

View File

@@ -0,0 +1,93 @@
import { describe, expect, it } from 'vitest';
import {
BUILTIN_TOOL_FLOWS,
describeBuiltinToolParameters,
filterBuiltinToolFlows,
filterBuiltinTools,
} from './aiBuiltinToolCatalog';
import type { AIBuiltinToolInfo } from './aiBuiltinToolInfo.types';
import { BUILTIN_AI_TOOL_INFO } from './aiToolRegistry';
describe('describeBuiltinToolParameters', () => {
it('extracts type, required, enum, default, and example hints from builtin tool schemas', () => {
const tool: AIBuiltinToolInfo = {
name: 'inspect_demo',
icon: '🧪',
desc: '测试工具',
detail: '用于测试参数提示提取。',
params: 'lineLimit?, mode?, serverName?',
tool: {
type: 'function',
function: {
name: 'inspect_demo',
description: '测试工具',
parameters: {
type: 'object',
required: ['mode'],
properties: {
lineLimit: { type: 'number', description: '可选,最多读取多少行,默认 160最大 200' },
mode: { type: 'string', enum: ['fast', 'safe'], default: 'safe', description: '运行模式' },
serverName: { type: 'string', description: '可选,例如 GitHub、Browser、DockerFetch' },
includeDisabled: { type: ['boolean', 'null'], description: '是否包含禁用项,默认 false' },
},
},
},
},
};
expect(describeBuiltinToolParameters(tool)).toEqual([
{
name: 'lineLimit',
required: false,
typeLabel: 'number',
description: '可选,最多读取多少行,默认 160最大 200',
enumValues: [],
defaultValue: '160',
exampleValue: '',
},
{
name: 'mode',
required: true,
typeLabel: 'string',
description: '运行模式',
enumValues: ['fast', 'safe'],
defaultValue: 'safe',
exampleValue: '',
},
{
name: 'serverName',
required: false,
typeLabel: 'string',
description: '可选,例如 GitHub、Browser、DockerFetch',
enumValues: [],
defaultValue: '',
exampleValue: 'GitHub、Browser、DockerFetch',
},
{
name: 'includeDisabled',
required: false,
typeLabel: 'boolean | null',
description: '是否包含禁用项,默认 false',
enumValues: [],
defaultValue: 'false',
exampleValue: '',
},
]);
});
it('filters flows and tools by parameter names and descriptions', () => {
const allowMutatingTools = filterBuiltinTools(BUILTIN_AI_TOOL_INFO, 'allowMutating')
.map((tool) => tool.name);
expect(allowMutatingTools).toContain('inspect_ai_safety');
expect(allowMutatingTools).not.toContain('inspect_mcp_runtime_failures');
const executeSqlTools = filterBuiltinTools(BUILTIN_AI_TOOL_INFO, '要执行的 SQL 语句')
.map((tool) => tool.name);
expect(executeSqlTools).toContain('execute_sql');
const mcpFlows = filterBuiltinToolFlows(BUILTIN_TOOL_FLOWS, '运行期失败日志')
.map((flow) => flow.title);
expect(mcpFlows).toContain('排查 MCP 接入状态');
});
});

View File

@@ -9,8 +9,11 @@ export interface AIBuiltinToolFlow {
export interface AIBuiltinToolParameterHint {
name: string;
required: boolean;
typeLabel: string;
description: string;
enumValues: string[];
defaultValue: string;
exampleValue: string;
}
export const BUILTIN_TOOL_FLOWS: AIBuiltinToolFlow[] = [
@@ -231,6 +234,44 @@ export const BUILTIN_TOOL_FLOWS: AIBuiltinToolFlow[] = [
},
];
const stringifyHintValue = (value: unknown): string => {
if (value === undefined) return '';
if (value === null) return 'null';
if (typeof value === 'string') return value;
if (typeof value === 'number' || typeof value === 'boolean') return String(value);
try {
return JSON.stringify(value);
} catch {
return String(value);
}
};
const readTypeLabel = (schema: Record<string, any>): string => {
if (Array.isArray(schema.type)) {
return schema.type.map((item) => String(item)).filter(Boolean).join(' | ') || 'any';
}
if (typeof schema.type === 'string' && schema.type.trim()) {
return schema.type.trim();
}
if (Array.isArray(schema.enum) && schema.enum.length > 0) {
return 'enum';
}
return 'any';
};
const readDefaultValue = (schema: Record<string, any>, description: string): string => {
if (Object.prototype.hasOwnProperty.call(schema, 'default')) {
return stringifyHintValue(schema.default);
}
const match = description.match(/\s*([^\s,;)]+)/u);
return match?.[1]?.trim() || '';
};
const readExampleValue = (description: string): string => {
const match = description.match(/(?:|?[:])\s*([^;\n]+)/u);
return match?.[1]?.trim() || '';
};
export const describeBuiltinToolParameters = (tool: AIBuiltinToolInfo): AIBuiltinToolParameterHint[] => {
const schema = tool.tool.function.parameters;
const properties = schema && typeof schema === 'object' && typeof schema.properties === 'object'
@@ -242,11 +283,57 @@ export const describeBuiltinToolParameters = (tool: AIBuiltinToolInfo): AIBuilti
return Object.entries(properties).map(([name, config]) => {
const normalized = config && typeof config === 'object' ? config as Record<string, any> : {};
const description = typeof normalized.description === 'string' ? normalized.description : '';
return {
name,
required: required.has(name),
description: typeof normalized.description === 'string' ? normalized.description : '',
typeLabel: readTypeLabel(normalized),
description,
enumValues: Array.isArray(normalized.enum) ? normalized.enum.map((item) => String(item)) : [],
defaultValue: readDefaultValue(normalized, description),
exampleValue: readExampleValue(description),
};
});
};
export const normalizeBuiltinToolCatalogSearch = (value: string): string =>
value.trim().toLowerCase();
const matchesCatalogSearch = (keyword: string, values: unknown[]): boolean =>
!keyword || values.some((value) => String(value || '').toLowerCase().includes(keyword));
export const filterBuiltinToolFlows = (
flows: AIBuiltinToolFlow[],
searchText: string,
): AIBuiltinToolFlow[] => {
const keyword = normalizeBuiltinToolCatalogSearch(searchText);
return flows.filter((flow) => matchesCatalogSearch(keyword, [
flow.title,
flow.steps,
flow.description,
]));
};
export const filterBuiltinTools = (
tools: AIBuiltinToolInfo[],
searchText: string,
): AIBuiltinToolInfo[] => {
const keyword = normalizeBuiltinToolCatalogSearch(searchText);
return tools.filter((tool) => {
const parameterDetails = describeBuiltinToolParameters(tool);
return matchesCatalogSearch(keyword, [
tool.name,
tool.desc,
tool.detail,
tool.params,
...parameterDetails.flatMap((item) => [
item.name,
item.typeLabel,
item.description,
item.defaultValue,
item.exampleValue,
item.enumValues.join(' '),
]),
]);
});
};