refactor(core): 统一模板引擎到 src/template/ 目录
- 将 agent/prompt-template/ 目录合并到 src/template/
- 新增通用模板函数 renderTemplate、render
- 新增 Agent 特定函数 renderPromptTemplate、renderPrompt
- 新增 createToolDescriptionContext 支持工具描述模板变量
- 支持 ${GREP_TOOL_NAME} 等 Claude Code 风格变量
- 更新所有相关导入路径
This commit is contained in:
@@ -17,7 +17,7 @@ import type {
|
|||||||
} from './types.js';
|
} from './types.js';
|
||||||
import { checkBashPermission, isPathInAllowedWritePaths } from './permission-merger.js';
|
import { checkBashPermission, isPathInAllowedWritePaths } from './permission-merger.js';
|
||||||
import { getProviderRegistry } from '../provider/index.js';
|
import { getProviderRegistry } from '../provider/index.js';
|
||||||
import { renderTemplate, createPlanContext } from './prompt-template/index.js';
|
import { renderPromptTemplate, createPlanContext } from '../template/index.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Agent 执行器
|
* Agent 执行器
|
||||||
@@ -298,7 +298,7 @@ export class AgentExecutor {
|
|||||||
workdir: process.cwd(),
|
workdir: process.cwd(),
|
||||||
isSubagent: this.agentInfo.mode === 'subagent',
|
isSubagent: this.agentInfo.mode === 'subagent',
|
||||||
});
|
});
|
||||||
return renderTemplate(this.agentInfo.prompt, context);
|
return renderPromptTemplate(this.agentInfo.prompt, context);
|
||||||
}
|
}
|
||||||
return this.agentInfo.prompt;
|
return this.agentInfo.prompt;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -61,15 +61,18 @@ export {
|
|||||||
// System Prompt
|
// System Prompt
|
||||||
export { SystemPrompt } from './system-prompt.js';
|
export { SystemPrompt } from './system-prompt.js';
|
||||||
|
|
||||||
// Prompt Template
|
// Prompt Template (re-export from ../template/)
|
||||||
export {
|
export {
|
||||||
renderTemplate,
|
renderTemplate,
|
||||||
render,
|
render,
|
||||||
|
renderPromptTemplate,
|
||||||
|
renderPrompt,
|
||||||
createDefaultContext,
|
createDefaultContext,
|
||||||
createPlanContext,
|
createPlanContext,
|
||||||
|
createToolDescriptionContext,
|
||||||
checkPlanFileExists,
|
checkPlanFileExists,
|
||||||
DEFAULT_TOOL_NAMES,
|
DEFAULT_TOOL_NAMES,
|
||||||
} from './prompt-template/index.js';
|
} from '../template/index.js';
|
||||||
|
|
||||||
export type {
|
export type {
|
||||||
PromptContext,
|
PromptContext,
|
||||||
@@ -79,4 +82,4 @@ export type {
|
|||||||
PlanModeContext,
|
PlanModeContext,
|
||||||
EnvContext,
|
EnvContext,
|
||||||
AgentContext,
|
AgentContext,
|
||||||
} from './prompt-template/index.js';
|
} from '../template/index.js';
|
||||||
|
|||||||
@@ -21,8 +21,10 @@ import {
|
|||||||
agentRegistry,
|
agentRegistry,
|
||||||
AgentExecutor,
|
AgentExecutor,
|
||||||
checkBashPermission,
|
checkBashPermission,
|
||||||
renderTemplate,
|
renderPromptTemplate,
|
||||||
createPlanContext,
|
createPlanContext,
|
||||||
|
createToolDescriptionContext,
|
||||||
|
type PromptContext,
|
||||||
} from '../agent/index.js';
|
} from '../agent/index.js';
|
||||||
import { loadVisionConfig } from '../utils/config.js';
|
import { loadVisionConfig } from '../utils/config.js';
|
||||||
import { getProviderRegistry, resolveApiKey } from '../provider/index.js';
|
import { getProviderRegistry, resolveApiKey } from '../provider/index.js';
|
||||||
@@ -104,6 +106,9 @@ export class Agent {
|
|||||||
// 原始 system prompt(用于切换回 default 时恢复)
|
// 原始 system prompt(用于切换回 default 时恢复)
|
||||||
private originalSystemPrompt: string;
|
private originalSystemPrompt: string;
|
||||||
|
|
||||||
|
// 工具描述渲染上下文缓存
|
||||||
|
private toolDescriptionContext: PromptContext | null = null;
|
||||||
|
|
||||||
constructor(config: AgentConfig, compressionConfig?: Partial<CompressionConfig>) {
|
constructor(config: AgentConfig, compressionConfig?: Partial<CompressionConfig>) {
|
||||||
this.config = config;
|
this.config = config;
|
||||||
this.originalSystemPrompt = config.systemPrompt;
|
this.originalSystemPrompt = config.systemPrompt;
|
||||||
@@ -244,6 +249,33 @@ export class Agent {
|
|||||||
return filteredTools;
|
return filteredTools;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取或创建工具描述渲染上下文
|
||||||
|
*/
|
||||||
|
private getToolDescriptionContext(): PromptContext {
|
||||||
|
if (!this.toolDescriptionContext) {
|
||||||
|
this.toolDescriptionContext = createToolDescriptionContext({
|
||||||
|
agent: {
|
||||||
|
name: this.currentAgentMode?.name ?? 'default',
|
||||||
|
mode: this.currentAgentMode?.mode ?? 'primary',
|
||||||
|
isSubagent: this.currentAgentMode?.mode === 'subagent',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return this.toolDescriptionContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 渲染工具描述中的模板变量
|
||||||
|
*/
|
||||||
|
private renderToolDescription(description: string): string {
|
||||||
|
const context = this.getToolDescriptionContext();
|
||||||
|
return renderPromptTemplate(description, context, {
|
||||||
|
throwOnUndefined: false,
|
||||||
|
undefinedValue: '',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 将工具转换为 Vercel AI SDK 的工具格式
|
* 将工具转换为 Vercel AI SDK 的工具格式
|
||||||
*/
|
*/
|
||||||
@@ -254,9 +286,10 @@ export class Agent {
|
|||||||
|
|
||||||
for (const tool of availableTools) {
|
for (const tool of availableTools) {
|
||||||
const schema = buildZodSchema(tool.parameters);
|
const schema = buildZodSchema(tool.parameters);
|
||||||
|
const renderedDescription = this.renderToolDescription(tool.description);
|
||||||
|
|
||||||
vercelTools[tool.name] = {
|
vercelTools[tool.name] = {
|
||||||
description: tool.description,
|
description: renderedDescription,
|
||||||
inputSchema: schema,
|
inputSchema: schema,
|
||||||
execute: async (params) => {
|
execute: async (params) => {
|
||||||
const args = params as Record<string, unknown>;
|
const args = params as Record<string, unknown>;
|
||||||
@@ -755,6 +788,9 @@ export class Agent {
|
|||||||
* @param agent AgentInfo 对象或模式字符串 ('build'/'plan')
|
* @param agent AgentInfo 对象或模式字符串 ('build'/'plan')
|
||||||
*/
|
*/
|
||||||
setAgentMode(agent: AgentInfo | 'build' | 'plan' | null): void {
|
setAgentMode(agent: AgentInfo | 'build' | 'plan' | null): void {
|
||||||
|
// 清除工具描述上下文缓存,下次使用时重新创建
|
||||||
|
this.toolDescriptionContext = null;
|
||||||
|
|
||||||
// 如果是字符串模式,从 registry 获取预设
|
// 如果是字符串模式,从 registry 获取预设
|
||||||
if (typeof agent === 'string') {
|
if (typeof agent === 'string') {
|
||||||
const presetAgent = agentRegistry.get(agent);
|
const presetAgent = agentRegistry.get(agent);
|
||||||
@@ -809,7 +845,7 @@ export class Agent {
|
|||||||
workdir: process.cwd(),
|
workdir: process.cwd(),
|
||||||
isSubagent: agent.mode === 'subagent',
|
isSubagent: agent.mode === 'subagent',
|
||||||
});
|
});
|
||||||
return renderTemplate(agent.prompt, context);
|
return renderPromptTemplate(agent.prompt, context);
|
||||||
}
|
}
|
||||||
|
|
||||||
return agent.prompt;
|
return agent.prompt;
|
||||||
|
|||||||
@@ -259,3 +259,31 @@ export {
|
|||||||
getCacheDir,
|
getCacheDir,
|
||||||
getLogsDir,
|
getLogsDir,
|
||||||
} from './constants/index.js';
|
} from './constants/index.js';
|
||||||
|
|
||||||
|
// Template - 通用模板引擎
|
||||||
|
export {
|
||||||
|
// 通用模板渲染
|
||||||
|
renderTemplate,
|
||||||
|
render,
|
||||||
|
// Agent 特定函数
|
||||||
|
renderPromptTemplate,
|
||||||
|
renderPrompt,
|
||||||
|
createDefaultContext,
|
||||||
|
createPlanContext,
|
||||||
|
createToolDescriptionContext,
|
||||||
|
checkPlanFileExists,
|
||||||
|
DEFAULT_TOOL_NAMES,
|
||||||
|
} from './template/index.js';
|
||||||
|
|
||||||
|
export type {
|
||||||
|
TemplateContext,
|
||||||
|
Template,
|
||||||
|
RenderOptions,
|
||||||
|
// Agent 特定类型
|
||||||
|
PromptContext,
|
||||||
|
PromptTemplate,
|
||||||
|
ToolNameMapping,
|
||||||
|
PlanModeContext,
|
||||||
|
EnvContext,
|
||||||
|
AgentContext,
|
||||||
|
} from './template/index.js';
|
||||||
|
|||||||
+15
-6
@@ -1,25 +1,29 @@
|
|||||||
/**
|
/**
|
||||||
* 提示词模板系统
|
* 通用模板引擎
|
||||||
*
|
*
|
||||||
* 提供动态提示词生成能力,支持:
|
* 提供动态模板渲染能力,支持:
|
||||||
* - ${variable} 变量替换
|
* - ${variable} 变量替换
|
||||||
* - ${obj.prop} 嵌套属性访问
|
* - ${obj.prop} 嵌套属性访问
|
||||||
* - ${condition ? "trueValue" : "falseValue"} 条件表达式
|
* - ${condition ? "trueValue" : "falseValue"} 条件表达式
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* ```typescript
|
* ```typescript
|
||||||
* import { renderTemplate, createPlanContext } from './prompt-template';
|
* import { renderTemplate } from '@ai-assistant/core/template';
|
||||||
*
|
*
|
||||||
* const context = createPlanContext({ workdir: '/path/to/project' });
|
* const context = { name: 'World', count: 42 };
|
||||||
* const prompt = renderTemplate(template, context);
|
* const result = renderTemplate('Hello ${name}, count is ${count}', context);
|
||||||
|
* // => "Hello World, count is 42"
|
||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// 类型导出
|
// 类型导出
|
||||||
export type {
|
export type {
|
||||||
|
TemplateContext,
|
||||||
|
Template,
|
||||||
|
RenderOptions,
|
||||||
|
// Agent 特定类型
|
||||||
PromptContext,
|
PromptContext,
|
||||||
PromptTemplate,
|
PromptTemplate,
|
||||||
RenderOptions,
|
|
||||||
ToolNameMapping,
|
ToolNameMapping,
|
||||||
PlanModeContext,
|
PlanModeContext,
|
||||||
EnvContext,
|
EnvContext,
|
||||||
@@ -28,10 +32,15 @@ export type {
|
|||||||
|
|
||||||
// 渲染器导出
|
// 渲染器导出
|
||||||
export {
|
export {
|
||||||
|
// 通用模板渲染
|
||||||
renderTemplate,
|
renderTemplate,
|
||||||
render,
|
render,
|
||||||
|
// Agent 特定函数
|
||||||
|
renderPromptTemplate,
|
||||||
|
renderPrompt,
|
||||||
createDefaultContext,
|
createDefaultContext,
|
||||||
createPlanContext,
|
createPlanContext,
|
||||||
|
createToolDescriptionContext,
|
||||||
checkPlanFileExists,
|
checkPlanFileExists,
|
||||||
DEFAULT_TOOL_NAMES,
|
DEFAULT_TOOL_NAMES,
|
||||||
} from './renderer.js';
|
} from './renderer.js';
|
||||||
+151
-70
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* 提示词模板渲染器
|
* 通用模板渲染器
|
||||||
*
|
*
|
||||||
* 支持的语法:
|
* 支持的语法:
|
||||||
* - ${variable} - 简单变量替换
|
* - ${variable} - 简单变量替换
|
||||||
@@ -7,7 +7,14 @@
|
|||||||
* - ${condition ? "trueValue" : "falseValue"} - 条件表达式
|
* - ${condition ? "trueValue" : "falseValue"} - 条件表达式
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { PromptContext, PromptTemplate, RenderOptions, ToolNameMapping } from './types.js';
|
import type {
|
||||||
|
TemplateContext,
|
||||||
|
Template,
|
||||||
|
RenderOptions,
|
||||||
|
PromptContext,
|
||||||
|
PromptTemplate,
|
||||||
|
ToolNameMapping,
|
||||||
|
} from './types.js';
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
|
|
||||||
@@ -32,68 +39,6 @@ function getNestedValue(obj: Record<string, unknown>, pathStr: string): unknown
|
|||||||
return current;
|
return current;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 安全的表达式求值器
|
|
||||||
* 支持简单的属性访问和三元运算符
|
|
||||||
*/
|
|
||||||
function evaluateExpression(
|
|
||||||
expr: string,
|
|
||||||
context: PromptContext,
|
|
||||||
options: RenderOptions = {}
|
|
||||||
): string {
|
|
||||||
// 移除首尾空格
|
|
||||||
expr = expr.trim();
|
|
||||||
|
|
||||||
// 处理三元运算符: condition ? trueValue : falseValue
|
|
||||||
// 需要处理嵌套的情况,使用更精确的匹配
|
|
||||||
const ternaryMatch = findTernaryOperator(expr);
|
|
||||||
if (ternaryMatch) {
|
|
||||||
const { condition, trueValue, falseValue } = ternaryMatch;
|
|
||||||
const conditionResult = evaluateExpression(condition, context, options);
|
|
||||||
// 将结果转换为布尔值
|
|
||||||
const isTruthy =
|
|
||||||
conditionResult !== '' &&
|
|
||||||
conditionResult !== 'false' &&
|
|
||||||
conditionResult !== 'undefined' &&
|
|
||||||
conditionResult !== 'null' &&
|
|
||||||
conditionResult !== '0';
|
|
||||||
return isTruthy
|
|
||||||
? evaluateExpression(trueValue, context, options)
|
|
||||||
: evaluateExpression(falseValue, context, options);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理字符串字面量: "string" 或 'string' 或 `string`
|
|
||||||
const stringMatch = expr.match(/^["'`]([\s\S]*)["'`]$/);
|
|
||||||
if (stringMatch) {
|
|
||||||
// 递归处理字符串中的变量插值
|
|
||||||
return renderTemplate(stringMatch[1], context, options);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理布尔字面量
|
|
||||||
if (expr === 'true') return 'true';
|
|
||||||
if (expr === 'false') return '';
|
|
||||||
|
|
||||||
// 处理属性访问: obj.prop.subprop
|
|
||||||
const value = getNestedValue(context as unknown as Record<string, unknown>, expr);
|
|
||||||
|
|
||||||
if (value === undefined) {
|
|
||||||
if (options.throwOnUndefined) {
|
|
||||||
throw new Error(`Undefined variable: ${expr}`);
|
|
||||||
}
|
|
||||||
if (options.debug) {
|
|
||||||
console.warn(`[PromptTemplate] Undefined variable: ${expr}`);
|
|
||||||
}
|
|
||||||
return options.undefinedValue ?? '';
|
|
||||||
}
|
|
||||||
|
|
||||||
// 布尔值转换
|
|
||||||
if (typeof value === 'boolean') {
|
|
||||||
return value ? 'true' : '';
|
|
||||||
}
|
|
||||||
|
|
||||||
return String(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 查找三元运算符的各个部分
|
* 查找三元运算符的各个部分
|
||||||
* 处理嵌套引号的情况
|
* 处理嵌套引号的情况
|
||||||
@@ -152,7 +97,74 @@ function findTernaryOperator(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 渲染提示词模板
|
* 安全的表达式求值器
|
||||||
|
* 支持简单的属性访问和三元运算符
|
||||||
|
*/
|
||||||
|
function evaluateExpression(
|
||||||
|
expr: string,
|
||||||
|
context: TemplateContext,
|
||||||
|
options: RenderOptions = {}
|
||||||
|
): string {
|
||||||
|
// 移除首尾空格
|
||||||
|
expr = expr.trim();
|
||||||
|
|
||||||
|
// 处理三元运算符: condition ? trueValue : falseValue
|
||||||
|
const ternaryMatch = findTernaryOperator(expr);
|
||||||
|
if (ternaryMatch) {
|
||||||
|
const { condition, trueValue, falseValue } = ternaryMatch;
|
||||||
|
const conditionResult = evaluateExpression(condition, context, options);
|
||||||
|
// 将结果转换为布尔值
|
||||||
|
const isTruthy =
|
||||||
|
conditionResult !== '' &&
|
||||||
|
conditionResult !== 'false' &&
|
||||||
|
conditionResult !== 'undefined' &&
|
||||||
|
conditionResult !== 'null' &&
|
||||||
|
conditionResult !== '0';
|
||||||
|
return isTruthy
|
||||||
|
? evaluateExpression(trueValue, context, options)
|
||||||
|
: evaluateExpression(falseValue, context, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理字符串字面量: "string" 或 'string' 或 `string`
|
||||||
|
const stringMatch = expr.match(/^["'`]([\s\S]*)["'`]$/);
|
||||||
|
if (stringMatch) {
|
||||||
|
// 递归处理字符串中的变量插值
|
||||||
|
return renderTemplate(stringMatch[1], context, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理布尔字面量
|
||||||
|
if (expr === 'true') return 'true';
|
||||||
|
if (expr === 'false') return '';
|
||||||
|
|
||||||
|
// 处理属性访问: obj.prop.subprop
|
||||||
|
let value = getNestedValue(context, expr);
|
||||||
|
|
||||||
|
// 如果在主上下文中找不到,且是单层变量名,尝试直接访问
|
||||||
|
// 这支持在 context 根级别定义的变量(如 GREP_TOOL_NAME)
|
||||||
|
if (value === undefined && !expr.includes('.')) {
|
||||||
|
value = context[expr];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value === undefined) {
|
||||||
|
if (options.throwOnUndefined) {
|
||||||
|
throw new Error(`Undefined variable: ${expr}`);
|
||||||
|
}
|
||||||
|
if (options.debug) {
|
||||||
|
console.warn(`[Template] Undefined variable: ${expr}`);
|
||||||
|
}
|
||||||
|
return options.undefinedValue ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 布尔值转换
|
||||||
|
if (typeof value === 'boolean') {
|
||||||
|
return value ? 'true' : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return String(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 渲染模板
|
||||||
*
|
*
|
||||||
* @param template 模板字符串
|
* @param template 模板字符串
|
||||||
* @param context 上下文变量
|
* @param context 上下文变量
|
||||||
@@ -161,7 +173,7 @@ function findTernaryOperator(
|
|||||||
*/
|
*/
|
||||||
export function renderTemplate(
|
export function renderTemplate(
|
||||||
template: string,
|
template: string,
|
||||||
context: PromptContext,
|
context: TemplateContext,
|
||||||
options: RenderOptions = {}
|
options: RenderOptions = {}
|
||||||
): string {
|
): string {
|
||||||
// 匹配 ${...} 模式,支持嵌套括号和字符串
|
// 匹配 ${...} 模式,支持嵌套括号和字符串
|
||||||
@@ -214,7 +226,7 @@ export function renderTemplate(
|
|||||||
result.push(value);
|
result.push(value);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (options.debug) {
|
if (options.debug) {
|
||||||
console.warn(`[PromptTemplate] Expression evaluation failed: ${expression}`, error);
|
console.warn(`[Template] Expression evaluation failed: ${expression}`, error);
|
||||||
}
|
}
|
||||||
result.push(`\${${expression}}`); // 保留原始文本
|
result.push(`\${${expression}}`); // 保留原始文本
|
||||||
}
|
}
|
||||||
@@ -227,14 +239,47 @@ export function renderTemplate(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 渲染 PromptTemplate 对象
|
* 渲染 Template 对象
|
||||||
*/
|
*/
|
||||||
export function render(
|
export function render(templateObj: Template, context: TemplateContext, options?: RenderOptions): string {
|
||||||
|
return renderTemplate(templateObj.template, context, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// Agent 特定函数(Prompt Template)
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 渲染提示词模板(Agent 特定版本)
|
||||||
|
* 将 custom 中的变量合并到顶层,以支持直接引用(如 ${GREP_TOOL_NAME})
|
||||||
|
*/
|
||||||
|
export function renderPromptTemplate(
|
||||||
|
template: string,
|
||||||
|
context: PromptContext,
|
||||||
|
options: RenderOptions = {}
|
||||||
|
): string {
|
||||||
|
// 将 custom 中的变量合并到顶层上下文,这样 ${GREP_TOOL_NAME} 可以直接访问
|
||||||
|
const flattenedContext: Record<string, unknown> = {
|
||||||
|
...context,
|
||||||
|
...context.custom,
|
||||||
|
};
|
||||||
|
return renderTemplate(template, flattenedContext, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 渲染提示词模板对象(Agent 特定版本)
|
||||||
|
*/
|
||||||
|
export function renderPrompt(
|
||||||
promptTemplate: PromptTemplate,
|
promptTemplate: PromptTemplate,
|
||||||
context: PromptContext,
|
context: PromptContext,
|
||||||
options?: RenderOptions
|
options?: RenderOptions
|
||||||
): string {
|
): string {
|
||||||
return renderTemplate(promptTemplate.template, context, options);
|
// 将 custom 中的变量合并到顶层上下文
|
||||||
|
const flattenedContext: Record<string, unknown> = {
|
||||||
|
...context,
|
||||||
|
...context.custom,
|
||||||
|
};
|
||||||
|
return render({ ...promptTemplate }, flattenedContext, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -345,3 +390,39 @@ export function createPlanContext(options: {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将驼峰命名转换为大写下划线命名
|
||||||
|
* 例如: askUserQuestion -> ASK_USER_QUESTION
|
||||||
|
*/
|
||||||
|
function camelToUpperSnake(str: string): string {
|
||||||
|
return str.replace(/[A-Z]/g, (c) => `_${c}`).toUpperCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建工具描述渲染上下文
|
||||||
|
*
|
||||||
|
* 除了标准的 PromptContext 变量外,还会在 custom 中添加大写形式的工具名变量,
|
||||||
|
* 以兼容 Claude Code 官方提示词格式(如 ${GREP_TOOL_NAME})。
|
||||||
|
*
|
||||||
|
* 支持的变量格式:
|
||||||
|
* - ${tools.grep} - 小写驼峰形式(项目内部风格)
|
||||||
|
* - ${GREP_TOOL_NAME} - 大写下划线形式(Claude Code 官方风格)
|
||||||
|
*/
|
||||||
|
export function createToolDescriptionContext(overrides?: Partial<PromptContext>): PromptContext {
|
||||||
|
const base = createDefaultContext(overrides);
|
||||||
|
|
||||||
|
// 将 tools 映射转为大写变量放入 custom
|
||||||
|
const toolVars: Record<string, string> = {};
|
||||||
|
for (const [key, value] of Object.entries(base.tools)) {
|
||||||
|
// glob -> GLOB_TOOL_NAME
|
||||||
|
// askUserQuestion -> ASK_USER_QUESTION_TOOL_NAME
|
||||||
|
const upperKey = camelToUpperSnake(key) + '_TOOL_NAME';
|
||||||
|
toolVars[upperKey] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...base,
|
||||||
|
custom: { ...base.custom, ...toolVars },
|
||||||
|
};
|
||||||
|
}
|
||||||
+41
-25
@@ -1,9 +1,46 @@
|
|||||||
/**
|
/**
|
||||||
* 提示词模板系统 - 类型定义
|
* 通用模板引擎 - 类型定义
|
||||||
*
|
*
|
||||||
* 支持动态变量替换和条件表达式的提示词模板系统
|
* 支持动态变量替换和条件表达式的模板系统
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 模板上下文 - 运行时可用的变量
|
||||||
|
*
|
||||||
|
* 支持任意嵌套的键值对结构
|
||||||
|
*/
|
||||||
|
export type TemplateContext = Record<string, unknown>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 模板定义
|
||||||
|
*/
|
||||||
|
export interface Template {
|
||||||
|
/** 模板名称 */
|
||||||
|
name: string;
|
||||||
|
/** 模板描述 */
|
||||||
|
description?: string;
|
||||||
|
/** 模板内容(支持变量插值) */
|
||||||
|
template: string;
|
||||||
|
/** 所需变量列表(用于验证) */
|
||||||
|
requiredVariables?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 模板渲染选项
|
||||||
|
*/
|
||||||
|
export interface RenderOptions {
|
||||||
|
/** 遇到未定义变量时是否抛出错误 */
|
||||||
|
throwOnUndefined?: boolean;
|
||||||
|
/** 未定义变量的默认值 */
|
||||||
|
undefinedValue?: string;
|
||||||
|
/** 是否启用调试日志 */
|
||||||
|
debug?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// Agent 特定类型(Prompt Template)
|
||||||
|
// ============================================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 工具名称映射
|
* 工具名称映射
|
||||||
* 用于在模板中引用工具名称,便于重命名
|
* 用于在模板中引用工具名称,便于重命名
|
||||||
@@ -93,27 +130,6 @@ export interface PromptContext {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 提示词模板定义
|
* 提示词模板定义(继承自通用 Template)
|
||||||
*/
|
*/
|
||||||
export interface PromptTemplate {
|
export interface PromptTemplate extends Template {}
|
||||||
/** 模板名称 */
|
|
||||||
name: string;
|
|
||||||
/** 模板描述 */
|
|
||||||
description?: string;
|
|
||||||
/** 模板内容(支持变量插值) */
|
|
||||||
template: string;
|
|
||||||
/** 所需变量列表(用于验证) */
|
|
||||||
requiredVariables?: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 模板渲染选项
|
|
||||||
*/
|
|
||||||
export interface RenderOptions {
|
|
||||||
/** 遇到未定义变量时是否抛出错误 */
|
|
||||||
throwOnUndefined?: boolean;
|
|
||||||
/** 未定义变量的默认值 */
|
|
||||||
undefinedValue?: string;
|
|
||||||
/** 是否启用调试日志 */
|
|
||||||
debug?: boolean;
|
|
||||||
}
|
|
||||||
@@ -1,11 +1,15 @@
|
|||||||
import { describe, it, expect, beforeEach } from 'vitest';
|
import { describe, it, expect, beforeEach } from 'vitest';
|
||||||
import {
|
import {
|
||||||
renderTemplate,
|
renderPromptTemplate,
|
||||||
createDefaultContext,
|
createDefaultContext,
|
||||||
createPlanContext,
|
createPlanContext,
|
||||||
|
createToolDescriptionContext,
|
||||||
DEFAULT_TOOL_NAMES,
|
DEFAULT_TOOL_NAMES,
|
||||||
} from '../../../src/agent/prompt-template/index.js';
|
} from '../../../src/template/index.js';
|
||||||
import type { PromptContext } from '../../../src/agent/prompt-template/types.js';
|
import type { PromptContext } from '../../../src/template/types.js';
|
||||||
|
|
||||||
|
// Alias for backward compatibility in tests
|
||||||
|
const renderTemplate = renderPromptTemplate;
|
||||||
|
|
||||||
describe('Prompt Template System', () => {
|
describe('Prompt Template System', () => {
|
||||||
let context: PromptContext;
|
let context: PromptContext;
|
||||||
@@ -178,4 +182,74 @@ describe('Prompt Template System', () => {
|
|||||||
expect(result).toBe('primary');
|
expect(result).toBe('primary');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('createToolDescriptionContext', () => {
|
||||||
|
it('should create context with uppercase tool name variables', () => {
|
||||||
|
const ctx = createToolDescriptionContext();
|
||||||
|
// Check that uppercase tool names are available in custom
|
||||||
|
expect(ctx.custom).toBeDefined();
|
||||||
|
expect(ctx.custom!['GLOB_TOOL_NAME']).toBe('glob');
|
||||||
|
expect(ctx.custom!['GREP_TOOL_NAME']).toBe('grep');
|
||||||
|
expect(ctx.custom!['BASH_TOOL_NAME']).toBe('bash');
|
||||||
|
expect(ctx.custom!['READ_TOOL_NAME']).toBe('read_file');
|
||||||
|
expect(ctx.custom!['WRITE_TOOL_NAME']).toBe('write_file');
|
||||||
|
expect(ctx.custom!['EDIT_TOOL_NAME']).toBe('edit_file');
|
||||||
|
expect(ctx.custom!['TASK_TOOL_NAME']).toBe('task');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should convert camelCase to UPPER_SNAKE_CASE correctly', () => {
|
||||||
|
const ctx = createToolDescriptionContext();
|
||||||
|
// askUserQuestion -> ASK_USER_QUESTION_TOOL_NAME
|
||||||
|
expect(ctx.custom!['ASK_USER_QUESTION_TOOL_NAME']).toBe('ask_user_question');
|
||||||
|
// exitPlanMode -> EXIT_PLAN_MODE_TOOL_NAME
|
||||||
|
expect(ctx.custom!['EXIT_PLAN_MODE_TOOL_NAME']).toBe('exit_plan_mode');
|
||||||
|
// webSearch -> WEB_SEARCH_TOOL_NAME
|
||||||
|
expect(ctx.custom!['WEB_SEARCH_TOOL_NAME']).toBe('web_search');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should support rendering with uppercase tool name variables', () => {
|
||||||
|
const ctx = createToolDescriptionContext();
|
||||||
|
const template = 'Use ${GREP_TOOL_NAME} for search. Never use ${BASH_TOOL_NAME} grep.';
|
||||||
|
const result = renderTemplate(template, ctx);
|
||||||
|
expect(result).toBe('Use grep for search. Never use bash grep.');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should support both lowercase and uppercase variable styles', () => {
|
||||||
|
const ctx = createToolDescriptionContext();
|
||||||
|
const template = 'Tools: ${tools.grep} or ${GREP_TOOL_NAME}';
|
||||||
|
const result = renderTemplate(template, ctx);
|
||||||
|
expect(result).toBe('Tools: grep or grep');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should preserve overrides', () => {
|
||||||
|
const ctx = createToolDescriptionContext({
|
||||||
|
agent: {
|
||||||
|
name: 'custom-agent',
|
||||||
|
mode: 'subagent',
|
||||||
|
isSubagent: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(ctx.agent.name).toBe('custom-agent');
|
||||||
|
expect(ctx.agent.mode).toBe('subagent');
|
||||||
|
expect(ctx.agent.isSubagent).toBe(true);
|
||||||
|
// Uppercase tool names should still be available
|
||||||
|
expect(ctx.custom!['GREP_TOOL_NAME']).toBe('grep');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render Claude Code style tool description template', () => {
|
||||||
|
const ctx = createToolDescriptionContext();
|
||||||
|
// Example from Claude Code official prompt
|
||||||
|
const template = `A powerful search tool built on ripgrep
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
- ALWAYS use \${GREP_TOOL_NAME} for search tasks. NEVER invoke \`grep\` or \`rg\` as a \${BASH_TOOL_NAME} command.
|
||||||
|
- Use \${TASK_TOOL_NAME} tool for open-ended searches requiring multiple rounds`;
|
||||||
|
|
||||||
|
const result = renderTemplate(template, ctx);
|
||||||
|
|
||||||
|
expect(result).toContain('ALWAYS use grep for search tasks');
|
||||||
|
expect(result).toContain('NEVER invoke `grep` or `rg` as a bash command');
|
||||||
|
expect(result).toContain('Use task tool for open-ended searches');
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user