Skip to content

Commit ed1df3f

Browse files
committed
chore: skill tool 移动到 cli 下
1 parent 390baf6 commit ed1df3f

4 files changed

Lines changed: 277 additions & 271 deletions

File tree

cli/utils/skill-tools.js

Lines changed: 270 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,270 @@
1+
/**
2+
* Shared skill tools for both eval and web modules.
3+
*
4+
* Exports:
5+
* TOOLS - LLM tool definitions (list_references, read_skills)
6+
* loadSkillFile - Load a skill markdown file (strips front matter)
7+
* loadMainSkill - Load SKILL.md for a library
8+
* extractKeySections - Extract key sections from skill markdown
9+
* toolListReferences - Tool handler: list reference docs
10+
* toolReadSkills - Tool handler: read skill doc content
11+
* buildSystemPrompt - Build tool-call system prompt with SKILL.md overview
12+
*/
13+
14+
const fs = require('fs');
15+
const path = require('path');
16+
17+
const ROOT_DIR = path.resolve(__dirname, '../..');
18+
const SKILLS_DIR = path.join(ROOT_DIR, 'skills');
19+
20+
// ── Tool definitions ──────────────────────────────────────────────────────────
21+
22+
const TOOLS = [
23+
{
24+
type: 'function',
25+
function: {
26+
name: 'list_references',
27+
description:
28+
'列出 references 目录下可用的参考文档文件。返回文件路径、标题、描述等信息。',
29+
parameters: {
30+
type: 'object',
31+
properties: {
32+
library: {
33+
type: 'string',
34+
description: '库:g2、g6',
35+
enum: ['g2', 'g6']
36+
},
37+
category: {
38+
type: 'string',
39+
description:
40+
'可选,过滤分类:marks、transforms、components、scales、coordinates、interactions、data、layouts、elements、behaviors、plugins、events、themes、patterns、recipes'
41+
}
42+
},
43+
required: ['library']
44+
}
45+
}
46+
},
47+
{
48+
type: 'function',
49+
function: {
50+
name: 'read_skills',
51+
description: '读取指定 Skill 参考文档的完整内容。一次最多读取 4 个文件。',
52+
parameters: {
53+
type: 'object',
54+
properties: {
55+
paths: {
56+
type: 'array',
57+
items: { type: 'string' },
58+
description:
59+
'Skill 文件路径列表,如 ["skills/g2/references/marks/g2-mark-interval-basic.md"]',
60+
maxItems: 4
61+
}
62+
},
63+
required: ['paths']
64+
}
65+
}
66+
}
67+
];
68+
69+
// ── File helpers ──────────────────────────────────────────────────────────────
70+
71+
/**
72+
* Load a skill markdown file and strip YAML front matter.
73+
* @param {string} skillPath - absolute or relative-to-ROOT_DIR path
74+
* @param {boolean} verbose
75+
* @returns {string|null}
76+
*/
77+
function loadSkillFile(skillPath, verbose = false) {
78+
const fullPath = skillPath.startsWith('/')
79+
? skillPath
80+
: path.join(ROOT_DIR, skillPath);
81+
if (!fs.existsSync(fullPath)) {
82+
if (verbose) console.log(` ⚠️ File not found: ${fullPath}`);
83+
return null;
84+
}
85+
return fs.readFileSync(fullPath, 'utf-8').replace(/^---[\s\S]*?---\n/, '');
86+
}
87+
88+
/**
89+
* Load the main SKILL.md for a library (strips front matter).
90+
* @param {string} library - 'g2' | 'g6'
91+
* @returns {string}
92+
*/
93+
function loadMainSkill(library) {
94+
return loadSkillFile(path.join(SKILLS_DIR, library, 'SKILL.md')) || '';
95+
}
96+
97+
// ── Section extraction ────────────────────────────────────────────────────────
98+
99+
const TARGET_SECTIONS = [
100+
'最小可运行示例',
101+
'基本用法',
102+
'核心概念',
103+
'API 速查',
104+
'完整配置',
105+
'常见错误',
106+
'变体用法',
107+
'完整 Spec',
108+
'常见变体'
109+
];
110+
111+
/**
112+
* Extract key sections from skill markdown content.
113+
* Sub-headings (###) are collected into their parent section rather than
114+
* terminating it — only a same-level or higher heading ends the section.
115+
*
116+
* @param {string} content - raw markdown (front matter already stripped)
117+
* @param {number} [maxChars=5000]
118+
* @returns {string}
119+
*/
120+
function extractKeySections(content, maxChars = 5000) {
121+
const lines = content.split('\n');
122+
const sections = [];
123+
let inSection = false;
124+
let sectionLevel = 0;
125+
let currentLines = [];
126+
127+
for (const line of lines) {
128+
const headingMatch = line.match(/^(#{1,3})\s+(.+)/);
129+
if (headingMatch) {
130+
const level = headingMatch[1].length;
131+
const title = headingMatch[2];
132+
if (TARGET_SECTIONS.some((t) => title.includes(t))) {
133+
if (currentLines.length > 0 && inSection)
134+
sections.push(currentLines.join('\n'));
135+
inSection = true;
136+
sectionLevel = level;
137+
currentLines = [line];
138+
} else if (inSection && level <= sectionLevel) {
139+
sections.push(currentLines.join('\n'));
140+
inSection = false;
141+
sectionLevel = 0;
142+
currentLines = [];
143+
} else if (inSection) {
144+
currentLines.push(line);
145+
}
146+
} else if (inSection) {
147+
currentLines.push(line);
148+
}
149+
}
150+
if (currentLines.length > 0 && inSection)
151+
sections.push(currentLines.join('\n'));
152+
153+
const B = '```';
154+
const withCode = sections.filter((s) => s.includes(B));
155+
const withoutCode = sections.filter((s) => !s.includes(B));
156+
return [...withCode, ...withoutCode]
157+
.slice(0, 4)
158+
.join('\n\n')
159+
.slice(0, maxChars);
160+
}
161+
162+
// ── Tool handlers ─────────────────────────────────────────────────────────────
163+
164+
/**
165+
* list_references tool handler.
166+
* @param {{ library: string, category?: string }} args
167+
* @param {boolean} verbose
168+
*/
169+
function toolListReferences(args, verbose = false) {
170+
const { library, category } = args;
171+
const referencesDir = path.join(SKILLS_DIR, library, 'references');
172+
if (!fs.existsSync(referencesDir)) return [];
173+
174+
const results = [];
175+
const categories = category ? [category] : fs.readdirSync(referencesDir);
176+
177+
for (const cat of categories) {
178+
const catDir = path.join(referencesDir, cat);
179+
if (!fs.existsSync(catDir) || !fs.statSync(catDir).isDirectory()) continue;
180+
181+
for (const file of fs
182+
.readdirSync(catDir)
183+
.filter((f) => f.endsWith('.md'))) {
184+
const raw = fs.readFileSync(path.join(catDir, file), 'utf-8');
185+
const yamlMatch = raw.match(/^---\n([\s\S]*?)\n---/);
186+
let meta = {};
187+
if (yamlMatch) {
188+
const yaml = yamlMatch[1];
189+
const idMatch = yaml.match(/^id:\s*["']?([^'"\n]+)["']?/m);
190+
const titleMatch = yaml.match(/^title:\s*["']?([^'"\n]+)["']?/m);
191+
const descMatch = yaml.match(
192+
/^description:\s*\|?\s*([\s\S]*?)(?=^[a-z]|\s*$)/m
193+
);
194+
meta = {
195+
id: idMatch ? idMatch[1].trim() : file.replace('.md', ''),
196+
title: titleMatch ? titleMatch[1].trim() : file,
197+
description: descMatch ? descMatch[1].trim().slice(0, 100) : ''
198+
};
199+
}
200+
results.push({
201+
...meta,
202+
category: cat,
203+
path: `skills/${library}/references/${cat}/${file}`
204+
});
205+
}
206+
}
207+
208+
if (verbose) console.log(` 📋 列出 ${results.length} 个参考文档`);
209+
return results;
210+
}
211+
212+
/**
213+
* read_skills tool handler.
214+
* @param {{ paths: string[] }} args
215+
* @param {boolean} verbose
216+
*/
217+
function toolReadSkills(args, verbose = false) {
218+
return args.paths.slice(0, 4).map((skillPath) => {
219+
const content = loadSkillFile(skillPath, verbose);
220+
const fileName = path.basename(skillPath, '.md');
221+
if (!content) return { path: skillPath, error: 'File not found' };
222+
const extracted = extractKeySections(content).slice(0, 10000);
223+
if (verbose)
224+
console.log(` 📖 加载: ${fileName} (${extracted.length} 字符)`);
225+
return { id: fileName, path: skillPath, content: extracted };
226+
});
227+
}
228+
229+
// ── System prompt ─────────────────────────────────────────────────────────────
230+
231+
/**
232+
* Build the tool-call system prompt for a given library.
233+
* Injects the library's SKILL.md as an overview.
234+
* @param {string} library - 'g2' | 'g6'
235+
* @returns {string}
236+
*/
237+
function buildSystemPrompt(library) {
238+
const skillContent = loadMainSkill(library);
239+
return `你是 AntV ${library.toUpperCase()} v5 代码生成专家。根据用户描述生成准确、可运行的代码。
240+
241+
## 工具使用(必须遵循)
242+
243+
你有两个工具可以查阅详细参考文档:
244+
245+
1. **list_references(library, category?)** - 列出参考文档目录,返回文件路径和标题
246+
2. **read_skills(paths)** - 读取参考文档完整内容(最多 4 个文件)
247+
248+
**工作流程**:
249+
1. 分析用户需求,确定涉及的图表类型、transform、coordinate、交互等
250+
2. 下方知识库概览只包含 API 速查表和链接,**不包含完整代码示例**
251+
3. **必须先调用 read_skills 读取相关的详细参考文档**,获取完整代码示例和配置细节后再生成代码
252+
4. 参考文档路径格式:\`skills/${library}/references/{category}/{filename}.md\`,路径已在知识库概览中列出
253+
5. 生成代码时严格参考文档中的示例写法
254+
255+
--- 知识库概览 ---
256+
257+
${skillContent}`;
258+
}
259+
260+
// ── Exports ───────────────────────────────────────────────────────────────────
261+
262+
module.exports = {
263+
TOOLS,
264+
loadSkillFile,
265+
loadMainSkill,
266+
extractKeySections,
267+
toolListReferences,
268+
toolReadSkills,
269+
buildSystemPrompt
270+
};

eval/utils/eval-manager.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ const {
1515
toolListReferences,
1616
toolReadSkills,
1717
buildSystemPrompt
18-
} = require('../utils/skill-tools');
18+
} = require('../../cli/utils/skill-tools');
1919
const ParallelExecutor = require('./parallel-executor');
2020

2121
const ROOT_DIR = path.resolve(__dirname, '..', '..');
@@ -115,7 +115,7 @@ function formatContext7Docs(data, maxResults = 6) {
115115
// ── BM25 ──────────────────────────────────────────────────────────────────────
116116

117117
function loadRetriever() {
118-
const { SkillRetriever } = require(path.join(ROOT_DIR, 'lib/retriever'));
118+
const { SkillRetriever } = require('../../cli/utils/retriever');
119119
return new SkillRetriever();
120120
}
121121

0 commit comments

Comments
 (0)