* reactSteps.js — ReAct 两个推理步骤的封装
*
* 职责:
* - Reason #1 analyzeTask:调用 LLM 拆解任务,产出 TaskAnalysis(喂给模糊匹配)
* - Reason #2 rankSkills :调用 LLM 对 top5 候选 SKILL 评分排序,产出 RankResult
*
* 设计原则:
* - 高内聚:两个推理步骤的 prompt 构建 + JSON 解析集中于此
* - 低耦合:只依赖 llmClient + prompts,不依赖 Agent 编排
* - 纯函数式:每个步骤都是 (llm, input) -> output,无副作用
*/
import { TASK_ANALYSIS_PROMPT, SKILL_RANKING_PROMPT, renderTemplate } from './prompts.js';
* Reason #1:任务拆解
* @param {object} llm - LLMClient 实例(需提供 chatJSON)
* @param {string} task - 具体事项
* @returns {Promise<object>} TaskAnalysis: { intent, keywords[], constraints[], techStack[], whenToUse }
*/
export async function analyzeTask(llm, task) {
if (!task) throw new Error('analyzeTask: task 不能为空');
const prompt = renderTemplate(TASK_ANALYSIS_PROMPT, { TASK: task });
const result = await llm.chatJSON(prompt);
return {
intent: typeof result.intent === 'string' ? result.intent : '',
keywords: _toArray(result.keywords),
constraints: _toArray(result.constraints),
techStack: _toArray(result.techStack),
whenToUse: typeof result.whenToUse === 'string' ? result.whenToUse : '',
};
}
* Reason #2:SKILL 排序
* @param {object} llm - LLMClient 实例
* @param {string} task - 具体事项
* @param {object} analysis - analyzeTask 产出的 TaskAnalysis
* @param {Array} candidates - 模糊匹配召回的候选 [{ skill, score }] 或 SkillMeta[]
* @returns {Promise<object>} RankResult: { bestId, ranked: [{ id, score, reason }] }
*/
export async function rankSkills(llm, task, analysis, candidates) {
if (!candidates || candidates.length === 0) {
throw new Error('rankSkills: 候选 SKILL 列表为空');
}
const normalized = candidates.map((c, i) => {
const skill = c.skill || c;
return {
skill,
score: typeof c.score === 'number' ? c.score : 0,
index: i,
};
});
const candidatesText = normalized
.map(({ skill, index }) => {
const fields = [
`id: ${skill.dirName}`,
`name: ${skill.name || ''}`,
`description: ${skill.description || ''}`,
`useWhen: ${skill.useWhen || '(无)'}`,
`category: ${skill.category || ''}`,
`language: ${skill.language || ''}`,
].filter(Boolean);
return `[#${index}] ${fields.join(' | ')}`;
})
.join('\n');
const analysisText = JSON.stringify(analysis, null, 2);
const prompt = renderTemplate(SKILL_RANKING_PROMPT, {
TASK: task,
ANALYSIS: analysisText,
WHEN_TO_USE: analysis?.whenToUse || '(未拆解出触发场景)',
CANDIDATES: candidatesText,
});
const result = await llm.chatJSON(prompt);
const rawRanked = result.ranked || result.results || result.list || result.rankings;
const ranked = Array.isArray(rawRanked)
? rawRanked
.filter((r) => r && typeof r === 'object')
.map((r) => {
const rawDims = (r.dimensions && typeof r.dimensions === 'object') ? r.dimensions : {};
const dimensions = {
intent: _numOr0(rawDims.intent),
techStack: _numOr0(rawDims.techStack),
constraint: _numOr0(rawDims.constraint),
keyword: _numOr0(rawDims.keyword),
};
let score = typeof r.score === 'number' ? r.score
: (dimensions.intent + dimensions.techStack + dimensions.constraint + dimensions.keyword);
if (typeof score !== 'number') score = 0;
return {
id: typeof r.id === 'string'
? r.id
: (typeof r.id === 'number' ? String(r.id)
: (typeof r.dirName === 'string' ? r.dirName : '')),
score,
dimensions,
reason: typeof r.reason === 'string' ? r.reason : '',
};
})
.filter((r) => r.id)
: [];
const bestId = result.bestId || result.best || result.best_id || result.id
|| (ranked[0] && ranked[0].id);
return { bestId, ranked };
}
function _toArray(v) {
if (Array.isArray(v)) return v.map((x) => String(x));
if (v == null) return [];
return [String(v)];
}
function _numOr0(v) {
if (typeof v === 'number') return Number.isFinite(v) ? v : 0;
if (typeof v === 'string') {
const n = Number(v);
return Number.isFinite(n) ? n : 0;
}
return 0;
}
export default { analyzeTask, rankSkills };