196 lines
6.5 KiB
TypeScript
196 lines
6.5 KiB
TypeScript
|
|
import {
|
|||
|
|
getModelGuidance,
|
|||
|
|
getToolBehaviorGuidance,
|
|||
|
|
} from '../src/modules/netaclaw/runtime/prompt_guidance.js';
|
|||
|
|
import {
|
|||
|
|
collectAvailableToolNames,
|
|||
|
|
buildSystemPrompt,
|
|||
|
|
BuildSystemPromptParams,
|
|||
|
|
buildLLMMessages,
|
|||
|
|
} from '../src/modules/netaclaw/runtime/prompt_builder.js';
|
|||
|
|
|
|||
|
|
// ─── Mock Helper ───
|
|||
|
|
|
|||
|
|
function createMockSkillLoader(returnValue: string) {
|
|||
|
|
return { buildSkillsPrompt: jest.fn().mockReturnValue(returnValue) } as any;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ─── collectAvailableToolNames ───
|
|||
|
|
|
|||
|
|
describe('collectAvailableToolNames', () => {
|
|||
|
|
it('基础工具始终包含', () => {
|
|||
|
|
const names = collectAvailableToolNames({});
|
|||
|
|
expect(names).toEqual(expect.arrayContaining(['bash', 'read_file', 'write_file', 'list_dir', 'todo']));
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('memoryEnabled 追加记忆工具', () => {
|
|||
|
|
const names = collectAvailableToolNames({ memoryEnabled: true });
|
|||
|
|
expect(names).toContain('memory_save');
|
|||
|
|
expect(names).toContain('memory_recall');
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('hasSkills 追加技能工具', () => {
|
|||
|
|
const names = collectAvailableToolNames({ hasSkills: true });
|
|||
|
|
expect(names).toContain('read_skill');
|
|||
|
|
expect(names).toContain('read_skill_file');
|
|||
|
|
expect(names).toContain('skill_manage');
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('crewRole=master 追加委派工具', () => {
|
|||
|
|
const names = collectAvailableToolNames({ crewRole: 'master' });
|
|||
|
|
expect(names).toContain('delegate_task');
|
|||
|
|
expect(names).toContain('delegate_parallel');
|
|||
|
|
expect(names).toContain('escalate');
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('crewRole=sub 不追加委派工具', () => {
|
|||
|
|
const names = collectAvailableToolNames({ crewRole: 'sub' });
|
|||
|
|
expect(names).not.toContain('delegate_task');
|
|||
|
|
expect(names).not.toContain('delegate_parallel');
|
|||
|
|
expect(names).not.toContain('escalate');
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// ─── getModelGuidance ───
|
|||
|
|
|
|||
|
|
describe('getModelGuidance', () => {
|
|||
|
|
it('Claude 返回空字符串', () => {
|
|||
|
|
expect(getModelGuidance('claude-3-opus')).toBe('');
|
|||
|
|
expect(getModelGuidance('Claude-Sonnet')).toBe('');
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('MiniMax 匹配 minimax 规则', () => {
|
|||
|
|
const g = getModelGuidance('minimax-abab6');
|
|||
|
|
expect(g).toContain('一个工具');
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('GPT 匹配 gpt 规则', () => {
|
|||
|
|
const g = getModelGuidance('gpt-4o');
|
|||
|
|
expect(g).toContain('tool_persistence');
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('DeepSeek 匹配 deepseek 规则', () => {
|
|||
|
|
const g = getModelGuidance('deepseek-chat');
|
|||
|
|
expect(g).toContain('并行');
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('doubao 匹配 doubao 规则', () => {
|
|||
|
|
const g = getModelGuidance('doubao-pro-32k');
|
|||
|
|
expect(g).toContain('non_interactive');
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('未知模型返回默认指导', () => {
|
|||
|
|
const g = getModelGuidance('some-unknown-model-xyz');
|
|||
|
|
expect(g).toContain('通用操作规范');
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// ─── getToolBehaviorGuidance ───
|
|||
|
|
|
|||
|
|
describe('getToolBehaviorGuidance', () => {
|
|||
|
|
it('包含 todo 时注入任务规划策略', () => {
|
|||
|
|
const g = getToolBehaviorGuidance(['todo', 'bash']);
|
|||
|
|
expect(g).toContain('任务规划策略');
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('包含 memory_save 时注入记忆策略', () => {
|
|||
|
|
const g = getToolBehaviorGuidance(['memory_save']);
|
|||
|
|
expect(g).toContain('记忆使用策略');
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('无匹配工具时返回空字符串', () => {
|
|||
|
|
const g = getToolBehaviorGuidance(['bash', 'read_file']);
|
|||
|
|
expect(g).toContain('命令执行策略');
|
|||
|
|
expect(g).toContain('文件读取策略');
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// ─── buildSystemPrompt ───
|
|||
|
|
|
|||
|
|
describe('buildSystemPrompt', () => {
|
|||
|
|
const baseParams: BuildSystemPromptParams = {
|
|||
|
|
agentSystemPrompt: '你是一个测试助手',
|
|||
|
|
modelId: 'gpt-4o',
|
|||
|
|
availableToolNames: ['bash', 'todo'],
|
|||
|
|
skills: [],
|
|||
|
|
skillLoader: createMockSkillLoader(''),
|
|||
|
|
memoryEnabled: false,
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
it('基础场景:包含 Agent 身份、工具纪律、模型指导、工具行为策略、元信息', () => {
|
|||
|
|
const { layers } = buildSystemPrompt(baseParams);
|
|||
|
|
const names = layers.map(l => l.name);
|
|||
|
|
expect(names).toContain('Agent 身份');
|
|||
|
|
expect(names).toContain('工具使用纪律');
|
|||
|
|
expect(names).toContain('模型特定指导');
|
|||
|
|
expect(names).toContain('工具行为策略');
|
|||
|
|
expect(names).toContain('元信息');
|
|||
|
|
expect(names).not.toContain('Skill 索引');
|
|||
|
|
expect(names).not.toContain('记忆系统');
|
|||
|
|
expect(names).not.toContain('Crew 编排上下文');
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('完整场景:skill + 记忆 + memoryContext → 包含所有层', () => {
|
|||
|
|
const loader = createMockSkillLoader('技能索引内容');
|
|||
|
|
const { layers } = buildSystemPrompt({
|
|||
|
|
...baseParams,
|
|||
|
|
skills: ['skill_a'],
|
|||
|
|
skillLoader: loader,
|
|||
|
|
memoryEnabled: true,
|
|||
|
|
memoryContext: '用户偏好数据',
|
|||
|
|
});
|
|||
|
|
const names = layers.map(l => l.name);
|
|||
|
|
expect(names).toContain('Skill 索引');
|
|||
|
|
expect(names).toContain('记忆行为');
|
|||
|
|
expect(loader.buildSkillsPrompt).toHaveBeenCalled();
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('Crew 场景:注入 crewContext → 包含 Crew 编排上下文层', () => {
|
|||
|
|
const { layers } = buildSystemPrompt({
|
|||
|
|
...baseParams,
|
|||
|
|
crewContext: '你是主Agent,负责分配任务',
|
|||
|
|
});
|
|||
|
|
const names = layers.map(l => l.name);
|
|||
|
|
expect(names).toContain('Crew 编排上下文');
|
|||
|
|
const crewLayer = layers.find(l => l.name === 'Crew 编排上下文');
|
|||
|
|
expect(crewLayer!.content).toContain('你是主Agent');
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('空 agentSystemPrompt 不生成 Agent 身份层', () => {
|
|||
|
|
const { layers } = buildSystemPrompt({ ...baseParams, agentSystemPrompt: '' });
|
|||
|
|
const names = layers.map(l => l.name);
|
|||
|
|
expect(names).not.toContain('Agent 身份');
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('Claude 模型不生成模型特定指导层', () => {
|
|||
|
|
const { layers } = buildSystemPrompt({ ...baseParams, modelId: 'claude-3-opus' });
|
|||
|
|
const names = layers.map(l => l.name);
|
|||
|
|
expect(names).not.toContain('模型特定指导');
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
describe('buildLLMMessages', () => {
|
|||
|
|
it('视频附件有旧伤检测 skill 时提示使用 execute_skill 而不是 image_recognize', () => {
|
|||
|
|
const messages = buildLLMMessages(
|
|||
|
|
'<skill name="vehicle-damage-inspection" type="compute-entry">汽车环车视频旧伤检测</skill>',
|
|||
|
|
[],
|
|||
|
|
{
|
|||
|
|
content: '请分析',
|
|||
|
|
metadata: {
|
|||
|
|
attachments: [{
|
|||
|
|
type: 'video',
|
|||
|
|
name: 'car.mp4',
|
|||
|
|
url: '/upload/20260507/car.mp4',
|
|||
|
|
}],
|
|||
|
|
},
|
|||
|
|
},
|
|||
|
|
['execute_skill', 'image_recognize'],
|
|||
|
|
);
|
|||
|
|
const hint = messages[messages.length - 1].content;
|
|||
|
|
expect(hint).toContain('execute_skill');
|
|||
|
|
expect(hint).toContain('vehicle-damage-inspection');
|
|||
|
|
expect(hint).toContain('/upload/20260507/car.mp4');
|
|||
|
|
expect(hint).toContain('不要用它直接分析视频文件');
|
|||
|
|
});
|
|||
|
|
});
|