chore: cli retrieve 时新增 constraints 内容 (#79)

* chore: cli retrieve 时新增 constraints 内容

* chore: readme 更新
This commit is contained in:
Joel Alan
2026-04-28 17:40:37 +08:00
committed by GitHub
parent 25a18d6083
commit 3258b74c09
10 changed files with 207 additions and 143 deletions
+8 -5
View File
@@ -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 `<!-- CONSTRAINTS:END -->`).
- `antv retrieve <query>` - BM25 full-text search over skills
The CLI (`antv` command) provides four commands:
- `antv retrieve <query>` - BM25 full-text search over skills; `--content` returns reference doc markdown and auto-prepends the core constraints block as the first result
- `antv get <id>` - Get a single skill by exact ID
- `antv list` - List/filter available skills
- `antv info <library>` - Show library metadata
- `antv info <library>` - 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
+46 -14
View File
@@ -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 <lib> Filter by library (e.g. g2, g6)
--topk <n> 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 <format> Output format: json | text (default: "text")
```
> Note: `--content` always prepends the library's core constraints (Section 1 & 2 of SKILL.md, up to the `<!-- CONSTRAINTS:END -->` 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 `<!-- CONSTRAINTS:END -->` — 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 <!-- CONSTRAINTS:END --> marker; injected by retrieve when includeInfo: true
}
```
| Parameter | Type | Default | Description |
+4 -7
View File
@@ -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)
};
}
+4 -3
View File
@@ -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 ?? '',
};
}
});
}
}
+14 -15
View File
@@ -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 || '',
}));
}
});
}
+60 -90
View File
@@ -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] } } });
```
<!-- CONSTRAINTS:END -->
---
## 3. Basic Structure / 基础结构
+22 -6
View File
@@ -7,7 +7,7 @@ export function registerRetrieveCommand(program: Command): void {
.description('Search for skills matching a query')
.option('--library <lib>', 'Filter by library (g2 or g6)')
.option('--topk <n>', '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 <format>', '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(
+26 -2
View File
@@ -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;
+14
View File
@@ -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 `<!-- CONSTRAINTS:END -->` 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 {
+9 -1
View File
@@ -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<string, any>;
const fullContent = parsed.content;
const marker = '<!-- CONSTRAINTS:END -->';
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,
};
}