From 3258b74c091632e30648924419db9b396f2633e5 Mon Sep 17 00:00:00 2001 From: Joel Alan <31396322+lxfu1@users.noreply.github.com> Date: Tue, 28 Apr 2026 17:40:37 +0800 Subject: [PATCH] =?UTF-8?q?chore:=20cli=20retrieve=20=E6=97=B6=E6=96=B0?= =?UTF-8?q?=E5=A2=9E=20constraints=20=E5=86=85=E5=AE=B9=20(#79)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore: cli retrieve 时新增 constraints 内容 * chore: readme 更新 --- AGENTS.md | 13 +- README.md | 60 +++++++-- playground/src/libs/cli-mode.ts | 11 +- playground/src/libs/tools/info-tool.ts | 7 +- playground/src/libs/tools/retrieve-tool.ts | 29 ++-- skills/antv-g2-chart/SKILL.md | 150 +++++++++------------ src/commands/retrieve.ts | 28 +++- src/core/retriever.ts | 28 +++- src/core/types.ts | 14 ++ src/scripts/build.ts | 10 +- 10 files changed, 207 insertions(+), 143 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index f34278d..72e40e1 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -55,11 +55,14 @@ Skills are the **single source of truth** for chart generation knowledge. ### 2. CLI Tool (`src/`) -The build script (`src/scripts/build.ts`) parses all skill markdown files and generates JSON index files (`src/index/*.index.json`). The CLI (`antv` command) provides three commands: +The build script (`src/scripts/build.ts`) parses all skill markdown files and generates JSON index files (`src/index/*.index.json`). Each index stores two things: the `skills[]` array (reference docs) and `info` (SKILL.md metadata including `constraintsContent` — the core constraints section up to ``). -- `antv retrieve ` - BM25 full-text search over skills +The CLI (`antv` command) provides four commands: + +- `antv retrieve ` - BM25 full-text search over skills; `--content` returns reference doc markdown and auto-prepends the core constraints block as the first result +- `antv get ` - Get a single skill by exact ID - `antv list` - List/filter available skills -- `antv info ` - Show library metadata +- `antv info ` - Show library core constraints (SKILL.md Section 1-2) The retrieval engine (`src/core/bm25.ts`) implements BM25 with Chinese/English tokenization, synonym expansion, and chart-type boosting. @@ -100,8 +103,8 @@ Iterates until MAX_PASSES consecutive clean passes are achieved. Next.js web app for interactive chart generation. Dual-panel UI with chat interface, code editor (Monaco), and real-time chart preview. Two retrieval modes: -- **Skill mode** - Agent calls `load_skill` / `read_file` tools -- **CLI mode** - Pre-injected BM25 results in system prompt +- **Skill mode** - Agent calls `load_skill` / `read_file` tools to load SKILL.md and reference docs on demand +- **CLI mode** - Agent calls `info` (first turn, gets core constraints) and `retrieve` tools (each turn, gets BM25-matched reference docs with constraints auto-prepended) ## Project Structure diff --git a/README.md b/README.md index b57c361..2b5236f 100644 --- a/README.md +++ b/README.md @@ -93,19 +93,25 @@ npm install -g @antv/chart-visualization-skills **Retrieve or list skills by query**: ```bash -# Retrieve skills by query -antv retrieve "bar chart" --library g2 --topk 10 --content +# Retrieve skills by query (metadata only) +antv retrieve "bar chart" --library g2 --topk 10 + +# Retrieve skills with full markdown content (core constraints auto-prepended) +antv retrieve "bar chart" --library g2 --content # Retrieve skills and output as JSON antv retrieve "bar chart" --library g2 --output json +# Get a skill by its exact ID +antv get g2-mark-interval-basic --library g2 + # List all available skills antv list --library g2 --category core # List skills and output as JSON antv list --output json -# Show skill info +# Show skill info (core constraints from SKILL.md) antv info --library g2 # Show skill info as JSON @@ -130,43 +136,69 @@ Commands: info [options] Show skill info from SKILL.md help [command] display help for command -Options shared by all commands: +Options for retrieve: + --library Filter by library (e.g. g2, g6) + --topk Number of results to return (default: 7) + --content Include markdown content body in results; core constraints (SKILL.md Section 1-2) are always prepended as the first result --output Output format: json | text (default: "text") ``` +> Note: `--content` always prepends the library's core constraints (Section 1 & 2 of SKILL.md, up to the `` marker) as the first result, ensuring the model receives essential rules alongside the reference documents. + ### API Usage ```typescript import { retrieve } from '@antv/chart-visualization-skills'; -const skills = retrieve('bar chart', 'g2', 5); -// with content body: retrieve('bar chart', 'g2', 5, true) +// Metadata only (no content) +const skills = retrieve('bar chart', { library: 'g2', topK: 5 }); + +// With full markdown content (core constraints auto-prepended as first result) +const skills = retrieve('bar chart', { library: 'g2', topK: 5, content: true }); + +// With content but without core constraints +const skills = retrieve('bar chart', { library: 'g2', topK: 5, content: true, includeInfo: false }); ``` ```typescript -retrieve(query: string, library?: string, topk?: number, content?: boolean) +retrieve(query: string, options?: RetrieveOptions): Skill[] + +interface RetrieveOptions { + library?: string; // Library filter, e.g. 'g2' or 'g6' + topK?: number; // Number of results (default: 7) + content?: boolean; // Include markdown content body (default: false) + includeInfo?: boolean; // Prepend SKILL.md core constraints (default: same as content) +} ``` -| Parameter | Type | Default | Description | +| Option | Type | Default | Description | | :--- | :--- | :--- | :--- | -| `query` | `string` | — | Search query | -| `library` | `string` | `'g2'` | Library filter (`g2` or `g6`) | -| `topk` | `number` | `7` | Number of results | -| `content` | `boolean` | `false` | Whether to include markdown content | +| `library` | `string` | all | Library filter (`g2` or `g6`) | +| `topK` | `number` | `7` | Number of results | +| `content` | `boolean` | `false` | Include markdown content body | +| `includeInfo` | `boolean` | same as `content` | Prepend SKILL.md core constraints (Section 1-2) as first result | > Notes: > - Default retrieval returns lightweight result objects without the `content` field. -> - `content = true` returns markdown content body (frontmatter metadata is excluded). +> - `content: true` returns markdown content body (frontmatter metadata is excluded). +> - When `includeInfo` is true (the default when `content: true`), the core constraints block — SKILL.md up to `` — is injected as the first element (id prefixed with `__info__`), ensuring the model always sees the essential rules. ```typescript import { info } from '@antv/chart-visualization-skills'; const skillInfo = info('g2'); -// => { name: 'antv-g2-chart', description: '...', content: '...' } +// => { name: 'antv-g2-chart', description: '...', content: '...', constraintsContent: '...' } ``` ```typescript info(library?: string): SkillInfo | undefined + +interface SkillInfo { + name: string; + description: string; + content: string; // Full SKILL.md body (after frontmatter) + constraintsContent: string; // SKILL.md body up to marker; injected by retrieve when includeInfo: true +} ``` | Parameter | Type | Default | Description | diff --git a/playground/src/libs/cli-mode.ts b/playground/src/libs/cli-mode.ts index 75993b7..9a7794b 100644 --- a/playground/src/libs/cli-mode.ts +++ b/playground/src/libs/cli-mode.ts @@ -1,18 +1,16 @@ import { getLibraryDisplayName } from './util'; -import { createInfoTool } from './tools/info-tool'; import { createRetrieveTool } from './tools/retrieve-tool'; export function buildCliSystemPrompt(library: string): string { const libraryName = getLibraryDisplayName(library); return `你是 AntV ${libraryName} v5 专家。你可以使用以下工具获取技术文档内容,帮你完成任务: - - 调用 \`info\`,获取当前图表库相关信息与要求文档; - - 调用 \`retrieve\`,通过用户需求或检索关键词,召回最相关的参考文档,支持设置召回文档数量; + - 调用 \`retrieve\`,通过用户需求或检索关键词,召回最相关的参考文档;结果首位自动包含核心约束(使用规则、禁止写法、常见错误),无需单独获取; ## 工作流程 -首轮一定要先调用 info 获取可视化库的基础信息,然后每轮都调用 retrieve 召回相关文档,再基于召回内容生成可运行的完整图表代码,遵从召回文档中的注意事项。 +每轮调用 \`retrieve\` 召回与当前需求最相关的参考文档,再基于召回内容生成可运行的完整图表代码,遵从文档中的注意事项。 -**重要**:每次用户提出新需求或修改请求时,你都必须重新调用 \`retrieve\` 召回与当前需求最相关的参考文档,不要依赖之前轮次的召回结果。不同的需求需要不同的参考文档。 +**重要**:每次用户提出新需求或修改请求时,必须重新调用 \`retrieve\`,不要依赖之前轮次的召回结果。 ## Output Format @@ -21,12 +19,11 @@ export function buildCliSystemPrompt(library: string): string { 3. \`container\` 必须为 'container' 4. 代码末尾必须有 \`chart.render();\` 5. 禁止返回 HTML 代码 -6. 关键配置处可加简短注释,但不要过度注释;`; +6. 关键配置处可加简短注释,但不要过度注释`; } export function createCliModeTools(library: string) { return { - info: createInfoTool(library), retrieve: createRetrieveTool(library) }; } diff --git a/playground/src/libs/tools/info-tool.ts b/playground/src/libs/tools/info-tool.ts index 4d2d9bb..4dd78f7 100644 --- a/playground/src/libs/tools/info-tool.ts +++ b/playground/src/libs/tools/info-tool.ts @@ -4,7 +4,7 @@ import { z } from 'zod'; export function createInfoTool(defaultLibrary: string) { return tool({ - description: '调用 info API,基于 library 获取该图表库相关信息与要求文档。', + description: '获取图表库的核心约束文档(使用规则、禁止写法、常见错误)。首轮必须调用。', inputSchema: z.object({ library: z .enum(['g2', 'g6']) @@ -13,10 +13,11 @@ export function createInfoTool(defaultLibrary: string) { }), execute: async ({ library = defaultLibrary }) => { console.log(`Loaded info for library: ${library}`); + const skillInfo = info(library); return { library, - content: info(library) + content: skillInfo?.constraintsContent ?? skillInfo?.content ?? '', }; } }); -} \ No newline at end of file +} diff --git a/playground/src/libs/tools/retrieve-tool.ts b/playground/src/libs/tools/retrieve-tool.ts index 125c179..74df2b1 100644 --- a/playground/src/libs/tools/retrieve-tool.ts +++ b/playground/src/libs/tools/retrieve-tool.ts @@ -8,19 +8,18 @@ interface RetrieveToolResult { id: string; title: string; description: string; - path: string; content: string; } export function createRetrieveTool(defaultLibrary: string) { return tool({ - description: '通过 retrieve 召回最相关参考文档。', + description: '通过 retrieve 召回最相关参考文档。结果首位自动包含核心约束(使用规则、禁止写法、常见错误)。', inputSchema: z.object({ query: z.string().describe('用户需求或检索关键词'), library: z .enum(['g2', 'g6']) .optional() - .describe('图表库名称,例如 g2、g6。').optional(), + .describe('图表库名称,例如 g2、g6。'), topk: z .number() .int() @@ -30,20 +29,20 @@ export function createRetrieveTool(defaultLibrary: string) { .describe('召回文档数量,默认 5') }), execute: async ({ query, library, topk }) => { - const retrievedSkills = retrieve(query, library ?? defaultLibrary, topk ?? 5, true); + const retrievedSkills = retrieve(query, { + library: library ?? defaultLibrary, + topK: topk ?? 5, + content: true, + includeInfo: true, + }); console.log(`Retrieved ${retrievedSkills.length} skills for query: "${query}"`); - const results: RetrieveToolResult[] = []; - for (const skill of retrievedSkills) { - results.push({ - id: skill.id, - title: skill.title, - description: skill.description || '', - path: skill.path, - content: skill.content || '', - }); - } - return results; + return retrievedSkills.map((skill) => ({ + id: skill.id, + title: skill.title, + description: skill.description || '', + content: skill.content || '', + })); } }); } diff --git a/skills/antv-g2-chart/SKILL.md b/skills/antv-g2-chart/SKILL.md index cd4a329..ef6e578 100644 --- a/skills/antv-g2-chart/SKILL.md +++ b/skills/antv-g2-chart/SKILL.md @@ -156,7 +156,62 @@ chart.options({ ## 2. Common Mistakes / 常见错误 -代码示例: +### ⚠️ 最高频错误:禁止多次调用 `chart.options()` + +`chart.options()` 是**全量替换**,不是合并。多次调用时**只有最后一次生效**,前面的配置全部丢失。**每个图表只能调用一次 `chart.options()`。** + +```javascript +// ❌ Wrong: 多次调用 chart.options() —— 每次完整替换前一次,只有最后一次生效 +chart.options({ type: 'interval', data, encode: { x: 'x', y: 'y' } }); // ❌ 被覆盖,不渲染 +chart.options({ type: 'line', data, encode: { x: 'x', y: 'y' } }); // ❌ 被覆盖,不渲染 +chart.options({ type: 'text', data, encode: { x: 'x', y: 'y', text: 'label' } }); // 只有这个生效 + +// ✅ Correct: 多 mark 叠加必须用 type: 'view' + children,一次 chart.options() 搞定 +chart.options({ + type: 'view', + data, + children: [ + { type: 'interval', encode: { x: 'x', y: 'y' } }, + { type: 'line', encode: { x: 'x', y: 'y' } }, + { type: 'text', encode: { x: 'x', y: 'y', text: 'label' } }, + ], +}); + +// ✅ 子 mark 需要不同数据时,在 children 里单独指定 data +chart.options({ + type: 'view', + data: mainData, + children: [ + { type: 'interval', encode: { x: 'x', y: 'y' } }, + { type: 'text', data: labelData, encode: { x: 'x', text: 'label' } }, + ], +}); +``` + +多 mark 组合规则: +- 只能使用 `children`,禁止 `marks`、`layers` 等属性 +- `children` 不能嵌套(`children` 内不能再有 `type: 'view'` + `children`) +- 复杂多坐标系组合用 `spaceLayer`/`spaceFlex` + +```javascript +// ❌ Wrong: 使用 marks/layers(禁止) +chart.options({ type: 'view', data, marks: [...] }); // ❌ +chart.options({ type: 'view', data, Layers: [...] }); // ❌ + +// ❌ Wrong: children 嵌套(禁止) +chart.options({ type: 'view', children: [{ type: 'view', children: [...] }] }); // ❌ + +// ✅ Correct: 复杂多坐标系组合用 spaceLayer +chart.options({ + type: 'spaceLayer', + children: [ + { type: 'view', children: [...] }, + { type: 'line', encode: { x: 'x', y: 'y' } }, + ], +}); +``` + +### 其他常见错误 ```javascript // ❌ Wrong: padding 数组形式(CSS 简写),G2 v5 不支持,会被忽略 @@ -168,9 +223,6 @@ const chart = new Chart({ container: 'container', padding: 40 }); // ✅ Correct: 分方向控制 const chart = new Chart({ container: 'container', paddingTop: 40, paddingLeft: 60 }); -// ✅ Correct: 默认 'auto' 即可(大多数场景无需配置) -const chart = new Chart({ container: 'container', autoFit: true, height: 400 }); - // ❌ Wrong: missing container const chart = new Chart({ width: 640, height: 480 }); @@ -189,97 +241,15 @@ chart.options({ label: { text: 'value' } }); // ✅ Correct: labels (plural) chart.options({ labels: [{ text: 'value' }] }); -// ❌ Wrong: 多次调用 chart.options() —— 每次调用完整替换前一次,只有最后一次生效 -chart.options({ type: 'interval', data, encode: { x: 'x', y: 'y' } }); // ❌ 被覆盖,不渲染 -chart.options({ type: 'line', data, encode: { x: 'x', y: 'y' } }); // ❌ 被覆盖,不渲染 -chart.options({ type: 'text', data, encode: { x: 'x', y: 'y', text: 'label' } }); // 只有这个生效 - -// ✅ Correct: 多 mark 叠加必须用 type: 'view' + children -chart.options({ - type: 'view', - data, // 共享数据(子 mark 可以覆盖) - children: [ - { type: 'interval', encode: { x: 'x', y: 'y' } }, - { type: 'line', encode: { x: 'x', y: 'y' } }, - { type: 'text', encode: { x: 'x', y: 'y', text: 'label' } }, - ], -}); - -// ✅ 子 mark 需要不同数据时,在 children 里单独指定 data -chart.options({ - type: 'view', - data: mainData, - children: [ - { type: 'interval', encode: { x: 'x', y: 'y' } }, // 用父级 mainData - { type: 'text', data: labelData, encode: { x: 'x', text: 'label' } }, // 用独立数据 - ], -}); - -// ⚠️ 多 mark 组合规则: -// 1. 只能使用 children,禁止使用 marks、layers 等配置 -// 2. children 不能嵌套(children 内不能再有 children) -// 3. 复杂组合使用 spaceLayer/spaceFlex - -// ❌ Wrong: 使用 marks(禁止) -chart.options({ - type: 'view', - data, - marks: [...], // ❌ 禁止! -}); - -// ❌ Wrong: 使用 layers(禁止) -chart.options({ - type: 'view', - data, - Layers: [...], // ❌ 禁止! -}); - -// ✅ Correct: 使用 children -chart.options({ - type: 'view', - data, - children: [ // ✅ 正确 - { type: 'line', encode: { x: 'year', y: 'value' } }, - { type: 'point', encode: { x: 'year', y: 'value' } }, - ], -}); - -// ❌ Wrong: children 嵌套(禁止) -chart.options({ - type: 'view', - children: [ - { - type: 'view', - children: [...], // ❌ 禁止!children 不能嵌套 - }, - ], -}); - -// ✅ Correct: 使用 spaceLayer/spaceFlex 处理复杂组合 -chart.options({ - type: 'spaceLayer', - children: [ - { type: 'view', children: [...] }, // ✅ spaceLayer 下可以有 view + children - { type: 'line', ... }, - ], -}); - // ❌ Wrong: unnecessary scale type specification -chart.options({ - scale: { - x: { type: 'linear' }, // ❌ 不需要,默认就是 linear - y: { type: 'linear' }, // ❌ 不需要 - }, -}); +chart.options({ scale: { x: { type: 'linear' }, y: { type: 'linear' } } }); // ✅ Correct: let G2 infer scale type automatically -chart.options({ - scale: { - y: { domain: [0, 100] }, // ✅ 只配置需要的属性 - }, -}); +chart.options({ scale: { y: { domain: [0, 100] } } }); ``` + + --- ## 3. Basic Structure / 基础结构 diff --git a/src/commands/retrieve.ts b/src/commands/retrieve.ts index c3770d4..80510b3 100644 --- a/src/commands/retrieve.ts +++ b/src/commands/retrieve.ts @@ -7,7 +7,7 @@ export function registerRetrieveCommand(program: Command): void { .description('Search for skills matching a query') .option('--library ', 'Filter by library (g2 or g6)') .option('--topk ', 'Number of results to return', '7') - .option('--content', 'Include markdown content body') + .option('--content', 'Include markdown content of matched reference docs (SKILL.md constraints are always prepended)') .option('--output ', 'Output format: json | text', 'text') .action( ( @@ -15,10 +15,13 @@ export function registerRetrieveCommand(program: Command): void { opts: { library?: string; topk: string; content?: true; output: string } ) => { const topK = parseInt(opts.topk, 10) || 7; + const withContent = !!opts.content; + const skills = retrieve(query, { library: opts.library, topK, - content: !!opts.content + content: withContent, + includeInfo: withContent, }); if (opts.output === 'json') { @@ -26,13 +29,26 @@ export function registerRetrieveCommand(program: Command): void { return; } - if (skills.length === 0) { - console.log('No skills found.'); + const refSkills = skills.filter((s) => !s.id.startsWith('__info__')); + const infoSkills = skills.filter((s) => s.id.startsWith('__info__')); + + if (infoSkills.length > 0) { + for (const infoSkill of infoSkills) { + console.log(`${'═'.repeat(60)}`); + console.log(` SKILL CONSTRAINTS: ${infoSkill.title}`); + console.log(`${'═'.repeat(60)}`); + if (infoSkill.content) console.log(infoSkill.content); + console.log(); + } + } + + if (refSkills.length === 0) { + console.log('No reference documents found.'); return; } - console.log(`Total ${skills.length} documents found:`); - for (const [i, skill] of skills.entries()) { + console.log(`Total ${refSkills.length} documents found:`); + for (const [i, skill] of refSkills.entries()) { console.log(`\n${'─'.repeat(50)}`); console.log(`[${i + 1}] ${skill.title} (${skill.id})`); console.log( diff --git a/src/core/retriever.ts b/src/core/retriever.ts index b00a4f0..b99dfb5 100644 --- a/src/core/retriever.ts +++ b/src/core/retriever.ts @@ -69,7 +69,7 @@ export function retrieve( query: string, options: RetrieveOptions = {} ): Skill[] { - const { library, topK = 7, content = false } = options; + const { library, topK = 7, content = false, includeInfo = content } = options; let skills: Skill[]; if (library) { @@ -85,7 +85,31 @@ export function retrieve( } if (!content) { - return skills.map(({ content, ...skill }) => skill); + skills = skills.map(({ content, ...skill }) => skill); + } + + if (includeInfo) { + const libs = library ? [library] : [...new Set(skills.map((s) => s.library))]; + const infoSkills: Skill[] = libs.flatMap((lib) => { + const skillInfo = getSkillInfo(lib); + if (!skillInfo) return []; + return [{ + id: `__info__${lib}`, + title: skillInfo.name, + description: skillInfo.description, + library: lib, + version: '', + category: '__info__', + subcategory: '', + tags: [], + difficulty: '', + use_cases: [], + anti_patterns: [], + related: [], + content: skillInfo.constraintsContent, + }]; + }); + skills = [...infoSkills, ...skills]; } return skills; diff --git a/src/core/types.ts b/src/core/types.ts index e77f31d..9b55c09 100644 --- a/src/core/types.ts +++ b/src/core/types.ts @@ -27,6 +27,12 @@ export interface RetrieveOptions { library?: string; topK?: number; content?: boolean; + /** + * When true, prepend the library's SKILL.md core constraints as the first + * result. Callers should set this whenever `content` is true so the model + * always receives constraints alongside reference docs. + */ + includeInfo?: boolean; } export interface ListOptions { @@ -39,7 +45,15 @@ export interface ListOptions { export interface SkillInfo { name: string; description: string; + /** Full SKILL.md body (after frontmatter). */ content: string; + /** + * Content up to and including the `` marker. + * Used by `retrieve --content` to inject only the core constraints section + * instead of the full document, avoiding context-window bloat. + * Falls back to `content` when the marker is absent. + */ + constraintsContent: string; } export interface BM25Options { diff --git a/src/scripts/build.ts b/src/scripts/build.ts index 5a123b2..3847d58 100644 --- a/src/scripts/build.ts +++ b/src/scripts/build.ts @@ -86,10 +86,18 @@ function build(): void { if (fs.existsSync(skillMd)) { const parsed = matter(fs.readFileSync(skillMd, 'utf-8')); const meta = parsed.data as Record; + const fullContent = parsed.content; + const marker = ''; + const markerIdx = fullContent.indexOf(marker); + const constraintsContent = + markerIdx !== -1 + ? fullContent.slice(0, markerIdx + marker.length) + : fullContent; info = { name: meta.name || libPath, description: (meta.description || '').replace(/\n\s*/g, ' ').trim(), - content: parsed.content, + content: fullContent, + constraintsContent, }; }