chore: import upstream snapshot with attribution
Build and push docs image / build-image (push) Waiting to run
Update draft releases / main (push) Waiting to run
Test Python / test-python (macos-latest, 3.10) (push) Waiting to run
Test Python / test-python (macos-latest, 3.11) (push) Waiting to run
Test Python / test-python (ubuntu-latest, 3.10) (push) Waiting to run
Test Python / test-python (ubuntu-latest, 3.11) (push) Waiting to run
Build Web Application / build-web (macos-latest) (push) Waiting to run
Build Web Application / build-web (ubuntu-latest) (push) Waiting to run
Python Code Quality Checks / build (push) Waiting to run

This commit is contained in:
wehub-resource-sync
2026-07-13 13:27:08 +08:00
commit d13100ebf3
4413 changed files with 764874 additions and 0 deletions
+296
View File
@@ -0,0 +1,296 @@
# SKILL 机制集成指南
本文档说明如何将 SKILL 机制集成到现有的 DB-GPT agent 中。
## 集成步骤
### 1. 导入 SKILL 模块
在需要使用 SKILL 的文件中添加导入:
```python
from dbgpt.agent.skill import (
Skill,
SkillBuilder,
SkillLoader,
SkillManager,
SkillType,
get_skill_manager,
initialize_skill,
)
```
### 2. 修改 Agent 类以支持 Skill
在你的 Agent 类中添加 Skill 支持:
```python
from dbgpt.agent.expand.tool_assistant_agent import ToolAssistantAgent
from dbgpt.agent.skill import Skill
class SkillEnabledAgent(ToolAssistantAgent):
def __init__(self, skill: Optional[Skill] = None, **kwargs):
super().__init__(**kwargs)
self._skill = skill
if self._skill:
self._apply_skill_to_profile()
@property
def skill(self) -> Optional[Skill]:
return self._skill
def _apply_skill_to_profile(self):
"""应用 Skill 配置到 Agent profile。"""
if self.skill.prompt_template:
self.bind_prompt = self.skill.prompt_template
if self.profile:
self.profile.goal = self.skill.metadata.description
async def load_resource(self, question: str, is_retry_chat: bool = False):
"""加载 Skill 所需的资源。"""
if self.skill:
await self._load_skill_resources()
return await super().load_resource(question, is_retry_chat)
async def _load_skill_resources(self):
"""加载 Skill 所需的工具和知识。"""
if not self.resource:
return
# 检查必需的工具
if self.skill.required_tools:
available_tools = self.resource.get_resource_by_type("tool")
available_tool_names = [t.name for t in available_tools]
for required_tool in self.skill.required_tools:
if required_tool not in available_tool_names:
raise ValueError(
f"Required tool '{required_tool}' not found. "
f"Available tools: {available_tool_names}"
)
# 检查必需的知识库
if self.skill.required_knowledge:
available_knowledge = self.resource.get_resource_by_type("knowledge")
available_knowledge_names = [k.name for k in available_knowledge]
for required_knowledge in self.skill.required_knowledge:
if required_knowledge not in available_knowledge_names:
raise ValueError(
f"Required knowledge '{required_knowledge}' not found. "
f"Available knowledge: {available_knowledge_names}"
)
```
### 3. 初始化 Skill Manager
在应用启动时初始化 Skill Manager
```python
from dbgpt.component import SystemApp
def initialize_app():
system_app = SystemApp()
initialize_skill(system_app)
return system_app
```
### 4. 注册 Skill
```python
from dbgpt.agent.skill import get_skill_manager
def register_my_skills(system_app):
skill_manager = get_skill_manager(system_app)
# 创建并注册 Skill
skill = (
SkillBuilder(name="my_skill", description="My skill description")
.with_skill_type(SkillType.Chat)
.with_prompt_template("You are a helpful assistant.")
.build()
)
skill_manager.register_skill(
skill_instance=skill,
name="my_skill",
)
```
### 5. 使用 Skill 创建 Agent
```python
from dbgpt.agent import AgentContext, LLMConfig, AgentMemory
async def create_agent_with_skill():
# 获取 Skill
skill_manager = get_skill_manager()
skill = skill_manager.get_skill(name="my_skill")
# 创建 Agent
agent = SkillEnabledAgent(skill=skill)
# 绑定配置
context = AgentContext(conv_id="test_conv")
llm_config = LLMConfig()
memory = AgentMemory()
await agent.bind(context).bind(llm_config).bind(memory).build()
return agent
```
## 修改现有 Agent 示例
### 示例:修改 IntentRecognitionAgent
原始文件:`packages/dbgpt-serve/src/dbgpt_serve/agent/agents/expand/intent_recognition_agent.py`
```python
import logging
from dbgpt.agent import ConversableAgent, get_agent_manager
from dbgpt.agent.core.profile import DynConfig, ProfileConfig
from dbgpt.agent.skill import Skill
from dbgpt_serve.agent.agents.expand.actions.intent_recognition_action import (
IntentRecognitionAction,
)
class IntentRecognitionAgent(ConversableAgent):
profile: ProfileConfig = ProfileConfig(...)
def __init__(self, skill: Optional[Skill] = None, **kwargs):
super().__init__(**kwargs)
self._skill = skill
self._init_actions([IntentRecognitionAction])
if self._skill:
self._apply_skill_to_profile()
@property
def skill(self) -> Optional[Skill]:
return self._skill
def _apply_skill_to_profile(self):
if self.skill and self.skill.prompt_template:
self.bind_prompt = self.skill.prompt_template
agent_manage = get_agent_manager()
agent_manage.register_agent(IntentRecognitionAgent)
```
## SKILL 文件格式
### JSON 格式
```json
{
"metadata": {
"name": "intent_recognition",
"description": "Intent recognition skill for user queries",
"version": "1.0.0",
"author": "DB-GPT Team",
"skill_type": "custom",
"tags": ["intent", "recognition", "nlp"]
},
"prompt_template": "You are an intent recognition expert. Analyze user queries and identify their intents.",
"required_tools": [],
"required_knowledge": [],
"config": {
"max_intents": 10,
"enable_slot_filling": true
}
}
```
### Python 格式
```python
from dbgpt.agent.skill import Skill, SkillMetadata, SkillType
from dbgpt.core import PromptTemplate
class IntentRecognitionSkill(Skill):
def __init__(self):
metadata = SkillMetadata(
name="intent_recognition",
description="Intent recognition skill",
version="1.0.0",
skill_type=SkillType.Custom,
tags=["intent", "recognition"],
)
prompt = PromptTemplate.from_template(
"You are an intent recognition expert."
)
super().__init__(
metadata=metadata,
prompt_template=prompt,
config={"max_intents": 10},
)
```
## 测试 SKILL 集成
```python
import pytest
from dbgpt.agent.skill import SkillBuilder, SkillType
def test_skill_integration():
# 创建 Skill
skill = (
SkillBuilder(name="test_skill", description="Test skill")
.build()
)
# 创建 Agent
agent = SkillEnabledAgent(skill=skill)
# 验证
assert agent.skill is not None
assert agent.skill.metadata.name == "test_skill"
```
## 最佳实践
1. **分离关注点**:Skill 应该专注于特定领域的能力
2. **版本管理**:为 Skill 使用语义化版本号
3. **依赖声明**:清晰声明 Skill 所需的工具和知识
4. **文档完善**:为 Skill 编写详细的文档和示例
5. **测试覆盖**:为每个 Skill 编写单元测试
## 常见问题
### Q: 如何动态切换 Skill
A: 在 Agent 中添加 `switch_skill` 方法:
```python
def switch_skill(self, skill: Skill):
self._skill = skill
self._apply_skill_to_profile()
```
### Q: Skill 可以包含多个工具吗?
A: 可以,使用 `with_required_tool` 多次添加:
```python
skill = (
SkillBuilder(name="multi_tool", description="Multi tool skill")
.with_required_tool("tool1")
.with_required_tool("tool2")
.with_required_tool("tool3")
.build()
)
```
### Q: 如何从文件加载 Skill
A: 使用 `SkillLoader`
```python
from dbgpt.agent.skill import SkillLoader
loader = SkillLoader()
skill = loader.load_skill_from_file("path/to/skill.json")
```
+287
View File
@@ -0,0 +1,287 @@
# SKILL 机制 - DB-GPT Agent 技能加载系统
## 概述
SKILL 机制是 DB-GPT Agent 框架的高级特性,允许 Agent 加载和管理预定义的技能包,实现 Agent 能力的模块化和可复用性。
## 核心文件
```
packages/dbgpt-core/src/dbgpt/agent/skill/
├── __init__.py # 模块入口,导出主要类
├── base.py # Skill 基础类定义
├── parameters.py # Skill 参数类
├── manage.py # Skill 管理器
└── loader.py # Skill 加载器和构建器
```
## 主要特性
### 1. Skill 定义
Skill 包含以下组件:
- **Metadata**:技能元信息(名称、描述、版本、类型、标签)
- **Prompt Template**:系统提示词模板
- **Required Tools**:所需的工具列表
- **Required Knowledge**:所需的知识库列表
- **Actions**:可执行的动作
- **Config**:特定配置参数
### 2. Skill 类型
| 类型 | 说明 |
|------|------|
| `Coding` | 编程技能 |
| `DataAnalysis` | 数据分析技能 |
| `WebSearch` | 网络搜索技能 |
| `KnowledgeQA` | 知识问答技能 |
| `Chat` | 对话技能 |
| `Custom` | 自定义技能 |
## 快速开始
### 1. 创建 Skill
```python
from dbgpt.agent.skill import SkillBuilder, SkillType
skill = (
SkillBuilder(name="my_skill", description="My awesome skill")
.with_version("1.0.0")
.with_author("Your Name")
.with_skill_type(SkillType.Coding)
.with_tags(["coding", "python"])
.with_prompt_template(
"You are a coding assistant. Help users write clean, efficient code."
)
.with_required_tool("python_interpreter")
.build()
)
```
### 2. 注册 Skill
```python
from dbgpt.agent.skill import get_skill_manager, initialize_skill
from dbgpt.component import SystemApp
system_app = SystemApp()
initialize_skill(system_app)
skill_manager = get_skill_manager(system_app)
skill_manager.register_skill(
skill_instance=skill,
name="my_awesome_skill",
)
```
### 3. 创建 Skill-based Agent
```python
from dbgpt.agent import ConversableAgent
from dbgpt.agent.skill import Skill
class SkillBasedAgent(ConversableAgent):
def __init__(self, skill: Skill, **kwargs):
super().__init__(**kwargs)
self._skill = skill
self._apply_skill_to_profile()
@property
def skill(self) -> Skill:
return self._skill
```
### 4. 使用 Agent
```python
agent = SkillBasedAgent(skill=skill)
await agent.bind(context).bind(llm_config).bind(memory).build()
```
## API 参考
### SkillBuilder
| 方法 | 参数 | 说明 |
|------|------|------|
| `with_version(version)` | version: str | 设置版本 |
| `with_author(author)` | author: str | 设置作者 |
| `with_skill_type(type)` | type: SkillType | 设置技能类型 |
| `with_tags(tags)` | tags: List[str] | 设置标签 |
| `with_prompt_template(template)` | template: str | 设置提示词模板 |
| `with_required_tool(name)` | name: str | 添加必需工具 |
| `with_required_knowledge(name)` | name: str | 添加必需知识库 |
| `with_action(action)` | action: Any | 添加动作 |
| `with_config(config)` | config: Dict | 设置配置 |
| `build()` | - | 构建 Skill |
### SkillManager
| 方法 | 参数 | 返回值 | 说明 |
|------|------|--------|------|
| `register_skill()` | skill_cls, skill_instance, name, metadata | None | 注册技能 |
| `get_skill()` | name, skill_type, version | SkillBase | 获取技能 |
| `get_skills_by_type()` | skill_type | List[SkillBase] | 按类型获取技能 |
| `list_skills()` | - | List[Dict] | 列出所有技能 |
### SkillLoader
| 方法 | 参数 | 返回值 | 说明 |
|------|------|--------|------|
| `load_skill_from_file()` | file_path | Optional[SkillBase] | 从文件加载技能 |
| `load_skill_from_module()` | module_path | Optional[SkillBase] | 从模块加载技能 |
| `load_skills_from_directory()` | directory, recursive | List[SkillBase] | 从目录加载所有技能 |
## 文件格式
### JSON 格式
```json
{
"metadata": {
"name": "web_search_assistant",
"description": "Web search assistant",
"version": "1.0.0",
"author": "DB-GPT Team",
"skill_type": "web_search",
"tags": ["web", "search"]
},
"prompt_template": "You are a web search assistant.",
"required_tools": ["google_search"],
"required_knowledge": [],
"config": {}
}
```
### Python 格式
```python
from dbgpt.agent.skill import Skill, SkillMetadata, SkillType
from dbgpt.core import PromptTemplate
class CustomSkill(Skill):
def __init__(self):
metadata = SkillMetadata(
name="custom_skill",
description="A custom skill",
version="1.0.0",
skill_type=SkillType.Custom,
)
prompt = PromptTemplate.from_template("You are a custom assistant.")
super().__init__(
metadata=metadata,
prompt_template=prompt,
)
```
## 示例
### 完整示例
查看 `examples/agents/skill_agent_example.py` 获取完整的使用示例。
### 技能文件
- `skills/web_search_skill.json` - 网络搜索技能示例
- `skills/data_analysis_skill.json` - 数据分析技能示例
### 实现指南
- `skills/skill_implementation_guide.py` - 详细的实现指南
- `skills/INTEGRATION_GUIDE.md` - 集成到现有 Agent 的指南
## 集成步骤
1. **导入 SKILL 模块**
```python
from dbgpt.agent.skill import Skill, SkillBuilder, get_skill_manager
```
2. **修改 Agent 类**
```python
class MyAgent(ConversableAgent):
def __init__(self, skill: Optional[Skill] = None, **kwargs):
super().__init__(**kwargs)
self._skill = skill
if self._skill:
self._apply_skill_to_profile()
```
3. **初始化 Skill Manager**
```python
from dbgpt.component import SystemApp
system_app = SystemApp()
initialize_skill(system_app)
```
4. **注册并使用 Skill**
```python
skill_manager = get_skill_manager(system_app)
skill_manager.register_skill(skill_instance=skill)
agent = MyAgent(skill=skill)
```
## 高级用法
### 动态 Skill 切换
```python
class DynamicSkillAgent(ConversableAgent):
def switch_skill(self, skill_name: str):
self._skill = self._skills[skill_name]
self._apply_skill_to_profile()
```
### 多 Skill 组合
```python
class CompositeSkillAgent(ConversableAgent):
def __init__(self, skills: List[Skill], **kwargs):
super().__init__(**kwargs)
self._skills = skills
def get_all_tools(self) -> List[str]:
all_tools = []
for skill in self._skills:
all_tools.extend(skill.required_tools)
return list(set(all_tools))
```
## 最佳实践
1. **模块化设计**:每个 Skill 专注于单一领域
2. **版本管理**:使用语义化版本号(如 1.0.0)
3. **依赖声明**:清晰声明所需的工具和知识库
4. **文档完善**:为 Skill 编写详细的文档
5. **测试覆盖**:为每个 Skill 编写单元测试
## 故障排除
### 常见问题
**Q: Skill 加载失败?**
A: 检查文件路径、JSON 格式是否正确
**Q: 找不到必需的工具?**
A: 确保在绑定 Agent 时提供了所有必需的工具
**Q: 提示词模板不生效?**
A: 确保在 `_apply_skill_to_profile` 中正确设置了 `bind_prompt`
## 贡献指南
欢迎贡献新的 Skill!请遵循以下步骤:
1. Fork 项目
2. 创建新的 Skill 文件
3. 编写测试
4. 提交 Pull Request
## 许可证
MIT License
## 联系方式
如有问题或建议,请提交 Issue 或 Pull Request。
+206
View File
@@ -0,0 +1,206 @@
---
name: agent-browser
description: Headless browser automation CLI optimized for AI agents with accessibility tree snapshots and ref-based element selection
metadata: {"clawdbot":{"emoji":"🌐","requires":{"commands":["agent-browser"]},"homepage":"https://github.com/vercel-labs/agent-browser"}}
---
# Agent Browser Skill
Fast browser automation using accessibility tree snapshots with refs for deterministic element selection.
## Why Use This Over Built-in Browser Tool
**Use agent-browser when:**
- Automating multi-step workflows
- Need deterministic element selection
- Performance is critical
- Working with complex SPAs
- Need session isolation
**Use built-in browser tool when:**
- Need screenshots/PDFs for analysis
- Visual inspection required
- Browser extension integration needed
## Core Workflow
```bash
# 1. Navigate and snapshot
agent-browser open https://example.com
agent-browser snapshot -i --json
# 2. Parse refs from JSON, then interact
agent-browser click @e2
agent-browser fill @e3 "text"
# 3. Re-snapshot after page changes
agent-browser snapshot -i --json
```
## Key Commands
### Navigation
```bash
agent-browser open <url>
agent-browser back | forward | reload | close
```
### Snapshot (Always use -i --json)
```bash
agent-browser snapshot -i --json # Interactive elements, JSON output
agent-browser snapshot -i -c -d 5 --json # + compact, depth limit
agent-browser snapshot -s "#main" -i # Scope to selector
```
### Interactions (Ref-based)
```bash
agent-browser click @e2
agent-browser fill @e3 "text"
agent-browser type @e3 "text"
agent-browser hover @e4
agent-browser check @e5 | uncheck @e5
agent-browser select @e6 "value"
agent-browser press "Enter"
agent-browser scroll down 500
agent-browser drag @e7 @e8
```
### Get Information
```bash
agent-browser get text @e1 --json
agent-browser get html @e2 --json
agent-browser get value @e3 --json
agent-browser get attr @e4 "href" --json
agent-browser get title --json
agent-browser get url --json
agent-browser get count ".item" --json
```
### Check State
```bash
agent-browser is visible @e2 --json
agent-browser is enabled @e3 --json
agent-browser is checked @e4 --json
```
### Wait
```bash
agent-browser wait @e2 # Wait for element
agent-browser wait 1000 # Wait ms
agent-browser wait --text "Welcome" # Wait for text
agent-browser wait --url "**/dashboard" # Wait for URL
agent-browser wait --load networkidle # Wait for network
agent-browser wait --fn "window.ready === true"
```
### Sessions (Isolated Browsers)
```bash
agent-browser --session admin open site.com
agent-browser --session user open site.com
agent-browser session list
# Or via env: AGENT_BROWSER_SESSION=admin agent-browser ...
```
### State Persistence
```bash
agent-browser state save auth.json # Save cookies/storage
agent-browser state load auth.json # Load (skip login)
```
### Screenshots & PDFs
```bash
agent-browser screenshot page.png
agent-browser screenshot --full page.png
agent-browser pdf page.pdf
```
### Network Control
```bash
agent-browser network route "**/ads/*" --abort # Block
agent-browser network route "**/api/*" --body '{"x":1}' # Mock
agent-browser network requests --filter api # View
```
### Cookies & Storage
```bash
agent-browser cookies # Get all
agent-browser cookies set name value
agent-browser storage local key # Get localStorage
agent-browser storage local set key val
```
### Tabs & Frames
```bash
agent-browser tab new https://example.com
agent-browser tab 2 # Switch to tab
agent-browser frame @e5 # Switch to iframe
agent-browser frame main # Back to main
```
## Snapshot Output Format
```json
{
"success": true,
"data": {
"snapshot": "...",
"refs": {
"e1": {"role": "heading", "name": "Example Domain"},
"e2": {"role": "button", "name": "Submit"},
"e3": {"role": "textbox", "name": "Email"}
}
}
}
```
## Best Practices
1. **Always use `-i` flag** - Focus on interactive elements
2. **Always use `--json`** - Easier to parse
3. **Wait for stability** - `agent-browser wait --load networkidle`
4. **Save auth state** - Skip login flows with `state save/load`
5. **Use sessions** - Isolate different browser contexts
6. **Use `--headed` for debugging** - See what's happening
## Example: Search and Extract
```bash
agent-browser open https://www.google.com
agent-browser snapshot -i --json
# AI identifies search box @e1
agent-browser fill @e1 "AI agents"
agent-browser press Enter
agent-browser wait --load networkidle
agent-browser snapshot -i --json
# AI identifies result refs
agent-browser get text @e3 --json
agent-browser get attr @e4 "href" --json
```
## Example: Multi-Session Testing
```bash
# Admin session
agent-browser --session admin open app.com
agent-browser --session admin state load admin-auth.json
agent-browser --session admin snapshot -i --json
# User session (simultaneous)
agent-browser --session user open app.com
agent-browser --session user state load user-auth.json
agent-browser --session user snapshot -i --json
```
## Installation
```bash
npm install -g agent-browser
agent-browser install # Download Chromium
agent-browser install --with-deps # Linux: + system deps
```
## Credits
Skill created by Yossi Elkrief ([@MaTriXy](https://github.com/MaTriXy))
agent-browser CLI by [Vercel Labs](https://github.com/vercel-labs/agent-browser)
+112
View File
@@ -0,0 +1,112 @@
---
name: csv-data-analysis
description: This skill should be used when users need to analyze CSV or Excel files, understand data patterns, generate statistical summaries, or create data visualizations. Trigger keywords include "analyze CSV", "analyze Excel", "data analysis", "CSV analysis", "Excel analysis", "data statistics", "generate charts", "data visualization", "分析CSV", "分析Excel", "数据分析", "CSV分析", "Excel分析", "数据统计", "生成图表", "数据可视化".
---
# Intelligent Deep Data Analysis Tool
The Data Analysis Tool is an AI-powered deep automated data exploration tool built on frontend visualization technologies (ECharts + Tailwind CSS). It rapidly extracts statistical features, data quality metrics, numerical distributions, outlier detection, categorical information, correlations, rankings, and time series trends. The latter half of the report supplements these with anomaly overviews, attribution clues, and summary recommendations, producing highly polished and interactive web-based analysis reports. Supported formats include CSV, Excel (.xlsx/.xls), and TSV.
The report follows a structure of "foundational data analysis in the first half, anomaly detection and attribution enhancement in the second half." Core sections include: Executive Summary, Data Overview & Quality Check, Numerical Distribution Features, Feature Analysis & Structural Analysis, Relationship Analysis & Anomaly Identification, Data Anomaly Overview, Attribution Analysis Module, Analysis Results & Statistical Details, Root Cause Inference / Conclusions / Recommendations.
## Core Workflow (Required Reading for LLMs)
As an AI assistant, when a user uploads a CSV or Excel file and requests analysis, you must strictly follow these two steps:
### Step 1: Extract Data Features (Execute Script)
Use the `execute_skill_script_file` tool to run `csv_analyzer.py`, passing in the data file path (supports .csv, .xlsx, .xls, .tsv formats).
**Tool call parameter example:**
```json
{
"skill_name": "csv-data-analysis",
"script_file_name": "csv_analyzer.py",
"args": {"input_file": "/path/to/data.csv or /path/to/data.xlsx"}
}
```
**Script return explanation:**
The script returns a large block of `text` content containing two parts:
1. **[Statistical Summary]**: For you to read and understand the dataset's basic characteristics, distributions, correlations, and categorical composition.
2. **[Marker-wrapped data blocks]**: The script output contains marker data blocks in the format `###KEY_START###...###KEY_END###`. The backend automatically captures and injects these into the template — **you do not need to handle or pass this content**.
### Step 2: Generate Insights & Display Report (Inject into Template)
Read the "Statistical Summary" obtained in Step 1, and reason about the business significance or patterns behind the data. Then use the `html_interpreter` tool to load the template and inject data.
**Critical Rules (Must Follow):**
1. **You must set `template_path`** to `csv-data-analysis/templates/report_template.html`. The template has built-in complete ECharts rendering JavaScript code and all section titles and footer text. You only need to fill in 9 content placeholders via the `data` parameter. **Never write or modify any JavaScript chart rendering code yourself.**
2. **Marker data blocks are automatically injected by the backend** — you must not pass them in `data`. The backend automatically extracts content from `###KEY_START###...###KEY_END###` markers in the script output and injects it into the template; in this skill, this is primarily `CHART_DATA_JSON`.
3. **`*_INSIGHTS`, `EXEC_SUMMARY`, and `CONCLUSIONS`** must use HTML formatting (e.g., `<p>`, `<ul>`, `<li>`, `<strong>`, `<ol>`) to ensure proper layout. These are deep business insights you write based on the statistical summary.
4. **The output language must match the user's input language.** You must also pass the `LANG` placeholder (`"en"` or `"zh"`) so that the template's hardcoded section titles, labels, and footer text are displayed in the matching language. Detect language from the user's query: if the user writes in English, set `LANG` to `"en"`; if the user writes in Chinese, set `LANG` to `"zh"`. Default to `"zh"` when uncertain.
5. **Pass exactly 9 placeholders — no more, no less.** Auto-injected marker fields like `CHART_DATA_JSON` are handled by the backend and should not be passed by you. The template already hardcodes all section titles (Distribution Analysis, Correlation Analysis, etc.), insight box titles ("Insights"), and footer text — you do not need to pass these (the template will automatically translate them based on the `LANG` placeholder).
6. **Insight content must be substantive.** Each insight module should cover 4 layers of information: `observation`, `possible causes`, `business impact`, and `action recommendations`. Do not merely restate statistical values or write only a few vague conclusions.
7. **Foundational analysis first, attribution as an enhancement module.** The first half of the report must focus on analyzing the data features of the CSV itself, including numerical distributions, categorical structures, outliers, correlations, ranking patterns, etc., and should incorporate chart interpretations wherever possible. "Data Anomaly Overview," "Attribution Analysis," and "Root Cause Inference" should appear in the second half as enhancement modules — the entire report must not consist solely of attribution content.
**`html_interpreter` call example:**
```json
{
"template_path": "csv-data-analysis/templates/report_template.html",
"data": {
"LANG": "en",
"REPORT_TITLE": "Sales Dataset Deep Analysis Report",
"REPORT_SUBTITLE": "Multi-dimensional Data Feature & Business Insight Mining",
"EXEC_SUMMARY": "<p>This dataset contains 1,000 rows and 5 columns with good data completeness. Key findings include:</p><ul><li><strong>Audience Distribution:</strong> Primarily concentrated in the 25-35 age group...</li></ul>",
"DISTRIBUTION_INSIGHTS": "<p>The numerical distribution chart reveals that Metric A exhibits a pronounced right-skewed distribution, suggesting...</p>",
"CORRELATION_INSIGHTS": "<p>The heatmap between variables reveals strong positive correlations, particularly between..., which implies...</p>",
"CATEGORICAL_INSIGHTS": "<p>Category proportions show that Beijing and Shanghai account for over 50% of the 'City' field.</p>",
"TIME_SERIES_INSIGHTS": "<p>The time series trend indicates a significant seasonal uptick toward year-end.</p>",
"CONCLUSIONS": "<p>Based on the comprehensive multi-dimensional analysis, the data exhibits clear structural features and patterns.</p><h3>Recommendations</h3><ul><li>Regularly monitor missing value ratios...</li><li>Focus on high-growth market segments...</li></ul>"
}
}
```
> **Strictly Prohibited:**
> - Do NOT pass `CHART_DATA_JSON` or any auto-injected marker fields in `data` (handled automatically by the backend)
> - Do NOT add any JavaScript code in `data`
> - Do NOT omit the `template_path` parameter (omitting template_path will prevent charts from rendering!)
> - Do NOT return static PNG images — this tool has been fully upgraded to ECharts dynamic frontend rendering
> - Do NOT pass non-existent placeholders (the template only has the following 9 text placeholders + 1 auto-injected CHART_DATA_JSON; other names will be ignored)
## Placeholder Reference (9 total, passed by LLM via data)
The placeholders you need to fill in the template are as follows:
| Placeholder | Type | Required | Description |
|---|---|---|---|
| `LANG` | Text | Yes | Report language: `"en"` for English, `"zh"` for Chinese. Determines all section titles, labels, and footer text language. Detect from user's input language; default `"zh"` |
| `REPORT_TITLE` | Text | Yes | Report title, e.g., "Sales Dataset Deep Analysis Report" |
| `REPORT_SUBTITLE` | Text | Yes | Report subtitle, e.g., "Multi-dimensional Data Feature & Business Insight Mining" |
| `EXEC_SUMMARY` | HTML | Yes | Executive summary: overview of data scale, key findings, and conclusion preview |
| `DISTRIBUTION_INSIGHTS` | HTML | Yes | Numerical distribution feature interpretation: skewness, volatility, quantile ranges, dispersion |
| `CORRELATION_INSIGHTS` | HTML | Yes | Relationship analysis & anomaly identification interpretation: correlations, linkages, outliers, structural relationships |
| `CATEGORICAL_INSIGHTS` | HTML | Yes | Feature analysis & structural analysis interpretation: categorical structure, concentration, rankings, and group characteristics |
| `TIME_SERIES_INSIGHTS` | HTML | Yes | Supplementary interpretation for the data anomaly overview section: discuss trends if time columns exist; discuss stratification differences and anomaly patterns if no time columns |
| `CONCLUSIONS` | HTML | Yes | Root cause inference, conclusions & recommendations body; must distinguish between "data evidence" and "reasonable speculation" |
> **Note:** `csv_analyzer.py` includes `###CHART_DATA_JSON_START###...###CHART_DATA_JSON_END###` marker data blocks in its output. The backend automatically extracts and injects these into the template — they should not be passed in `data`. All section titles in the template (e.g., "Distribution Analysis", "Correlation Analysis", "Conclusions & Recommendations"), insight box titles ("Insights"), and footer text are hardcoded in the HTML and are automatically translated based on the `LANG` placeholder — they do not need to be passed via placeholders.
## Why Choose This Tool?
1. **Fast & Lightweight**: No more slow Python plotting and bulk PNG generation — only core JSON data is transmitted.
2. **Modern Interactive Layout**: Fully integrated with Tailwind CSS responsive layouts and Apache ECharts smooth animated interactions.
3. **Deep Business Insights**: By separating machine-driven data extraction from LLM-driven logical reasoning, this tool produces highly valuable data analysis reports.
## File Structure
```
csv-data-analysis/
├── SKILL.md # The skill guide you are currently reading
├── scripts/
│ └── csv_analyzer.py # Python analysis engine (supports CSV/Excel/TSV, lightweight, no graphics dependencies)
└── templates/
└── report_template.html # Responsive ECharts report template (with built-in rendering logic and hardcoded titles)
```
@@ -0,0 +1,15 @@
# Reference Documentation
This file contains detailed reference material for the skill.
## API Reference
[TODO: Add detailed API documentation here]
## Schema Definitions
[TODO: Add schema definitions if applicable]
## Detailed Workflows
[TODO: Add detailed workflow descriptions]
@@ -0,0 +1,990 @@
import pandas as pd
import numpy as np
import os
import json
import warnings
import sys
warnings.filterwarnings("ignore")
def log(*args, **kwargs):
"""将日志输出到 stderr,避免污染 stdout 的 JSON 输出"""
print(*args, file=sys.stderr, **kwargs)
def safe_div(numerator, denominator):
if denominator in (0, None):
return 0.0
return float(numerator) / float(denominator)
def classify_skewness(value):
abs_val = abs(value)
if abs_val >= 1:
return "明显偏态"
if abs_val >= 0.5:
return "中度偏态"
return "近似对称"
def classify_cv(value):
if value >= 100:
return "极高波动"
if value >= 50:
return "高波动"
if value >= 20:
return "中等波动"
return "低波动"
def select_primary_metric(df, numeric_cols):
if not numeric_cols:
return None
preferred_keywords = [
"score",
"value",
"amount",
"sales",
"revenue",
"profit",
"price",
"rate",
"index",
"metric",
"total",
"count",
"分数",
"得分",
"金额",
"销售",
"收入",
"利润",
"价格",
"指标",
"总量",
"数量",
"评分",
]
skip_keywords = ["rank", "ranking", "id", "index_id", "序号", "排名", "编号"]
candidates = []
for col in numeric_cols:
col_lower = str(col).lower()
unique_cnt = int(df[col].nunique())
score = 0
if unique_cnt > 5:
score += 2
if not any(kw in col_lower for kw in skip_keywords):
score += 2
if any(kw in col_lower for kw in preferred_keywords):
score += 4
series = df[col].dropna()
if len(series) > 0:
score += 1
if float(series.std()) > 0:
score += 1
candidates.append((score, col))
candidates.sort(key=lambda x: x[0], reverse=True)
return candidates[0][1] if candidates else numeric_cols[0]
def select_label_col(df):
for col in df.columns:
if df[col].dtype == "object" and df[col].nunique() > 1:
return col
return None
def analyze_csv(file_path):
"""
分析CSV文件,提取用于 ECharts 渲染的数据结构和用于 LLM 分析的统计摘要。
输出包含: overview, data_quality, distributions, correlations, categories,
time_series, scatter, stats_table, box_plots, outliers, top_bottom
"""
try:
log(f"正在读取文件: {file_path}")
ext = os.path.splitext(file_path)[1].lower()
if ext in (".xls", ".xlsx"):
df = pd.read_excel(file_path)
elif ext == ".tsv":
df = pd.read_csv(file_path, sep="\t")
else:
df = pd.read_csv(file_path)
# ==========================================
# 1. 基础概览数据
# ==========================================
total_cells = int(df.shape[0] * df.shape[1])
missing_cells = int(df.isnull().sum().sum())
missing_pct = (
round((missing_cells / total_cells) * 100, 2) if total_cells > 0 else 0
)
duplicate_rows = int(df.duplicated().sum())
overview = {
"rows": int(df.shape[0]),
"cols": int(df.shape[1]),
"missing_cells": missing_cells,
"missing_pct": missing_pct,
"duplicate_rows": duplicate_rows,
"memory_kb": round(df.memory_usage(deep=True).sum() / 1024, 1),
}
# ==========================================
# 1b. 数据质量分析 (每列缺失率 + 数据类型)
# ==========================================
data_quality = {
"columns": [],
"missing_rates": [],
"dtypes": [],
"unique_counts": [],
"dtype_summary": {},
}
for col in df.columns:
data_quality["columns"].append(str(col))
col_missing = int(df[col].isnull().sum())
rate = round((col_missing / len(df)) * 100, 1) if len(df) > 0 else 0
data_quality["missing_rates"].append(rate)
data_quality["dtypes"].append(str(df[col].dtype))
data_quality["unique_counts"].append(int(df[col].nunique()))
# dtype breakdown for overview
dtype_counts = {}
for dt in data_quality["dtypes"]:
cat = (
"numeric"
if "int" in dt or "float" in dt
else ("datetime" if "datetime" in dt else "text")
)
dtype_counts[cat] = dtype_counts.get(cat, 0) + 1
data_quality["dtype_summary"] = dtype_counts
missing_by_col = sorted(
[
(col, rate, int(df[col].isnull().sum()))
for col, rate in zip(df.columns, data_quality["missing_rates"])
],
key=lambda x: x[1],
reverse=True,
)
# ==========================================
# 2. 数值列分析 (直方图分布 & 相关性)
# ==========================================
numeric_cols = df.select_dtypes(include=[np.number]).columns.tolist()
primary_metric = select_primary_metric(df, numeric_cols)
label_col = select_label_col(df)
distributions = {}
correlations = {"cols": numeric_cols, "data": []}
numeric_summary = {}
correlation_highlights = {"positive": [], "negative": []}
if numeric_cols:
# 取最多前 8 个数值列画分布图
for col in numeric_cols[:8]:
s = df[col].dropna()
if len(s) > 0:
# 使用 numpy 计算直方图 (10个 bin)
hist, bin_edges = np.histogram(s, bins=10)
bins = [
f"{bin_edges[i]:.1f}~{bin_edges[i + 1]:.1f}"
for i in range(len(hist))
]
distributions[col] = {
"bins": bins,
"counts": [int(x) for x in hist],
}
# Skewness & Kurtosis
skew_val = float(s.skew()) if len(s) > 2 else 0.0
kurt_val = float(s.kurtosis()) if len(s) > 3 else 0.0
mean_val = float(s.mean())
std_val = float(s.std())
cv = (
round(abs(std_val / mean_val) * 100, 1)
if mean_val != 0
else 0.0
)
spread = float(s.max()) - float(s.min())
numeric_summary[col] = {
"min": float(s.min()),
"max": float(s.max()),
"mean": round(mean_val, 4),
"median": float(s.median()),
"std": round(std_val, 4),
"q25": float(s.quantile(0.25)),
"q75": float(s.quantile(0.75)),
"p5": float(s.quantile(0.05)),
"p95": float(s.quantile(0.95)),
"cv": cv,
"spread": round(spread, 4),
"skewness": round(skew_val, 3),
"kurtosis": round(kurt_val, 3),
}
# 相关性矩阵 (取全部数值列)
if len(numeric_cols) > 1:
corr_df = df[numeric_cols].corr(method="pearson").fillna(0).round(2) # type: ignore[call-overload]
corr_pairs = []
for i, col1 in enumerate(numeric_cols):
for j, col2 in enumerate(numeric_cols):
correlations["data"].append([i, j, float(corr_df.iloc[i, j])])
if i < j:
corr_pairs.append((col1, col2, float(corr_df.iloc[i, j])))
corr_pairs = sorted(corr_pairs, key=lambda x: x[2], reverse=True)
correlation_highlights["positive"] = corr_pairs[:3]
correlation_highlights["negative"] = sorted(
corr_pairs, key=lambda x: x[2]
)[:3]
# ==========================================
# 3. 分类列分析 (饼图/柱状图 + 熵/集中度)
# ==========================================
categorical_cols = df.select_dtypes(
include=["object", "category"]
).columns.tolist()
categories = {}
cat_summary = {}
segment_breakdown = []
segment_comparison = {}
if categorical_cols:
# 取最多前 6 个分类列
for col in categorical_cols[:6]:
if df[col].nunique() <= 50:
val_counts = df[col].value_counts().head(10)
if len(val_counts) > 0:
categories[col] = {
"labels": [str(x) for x in val_counts.index.tolist()],
"values": [int(x) for x in val_counts.values],
}
top1 = val_counts.index[0]
top1_count = val_counts.values[0]
n_unique = df[col].nunique()
total_non_null = int(df[col].notna().sum())
# Shannon entropy (log2)
probs = np.array(val_counts.values, dtype=float)
if total_non_null > 0:
probs = probs / float(total_non_null)
entropy = -float(np.sum(probs * np.log2(probs + 1e-12)))
# Concentration ratio: top-3 share
top3_share = (
round(val_counts.head(3).sum() / total_non_null * 100, 1)
if total_non_null > 0
else 0
)
cat_summary[col] = {
"n_unique": n_unique,
"top1": str(top1),
"top1_count": int(top1_count),
"top1_share": round(
safe_div(top1_count, total_non_null) * 100, 1
)
if total_non_null > 0
else 0,
"entropy": round(entropy, 3),
"top3_share": top3_share,
}
if primary_metric:
for col in categorical_cols[:3]:
grouped = (
df[[col, primary_metric]]
.dropna()
.groupby(col)[primary_metric]
.agg(["count", "mean", "sum"])
)
grp = (
pd.DataFrame(grouped)
.reset_index()
.sort_values("sum", ascending=False)
.head(5)
)
if not grp.empty:
segment_breakdown.append(
{
"dimension": col,
"metric": primary_metric,
"leaders": [
{
"name": str(row[col]),
"count": int(row["count"]),
"mean": round(float(row["mean"]), 2),
"sum": round(float(row["sum"]), 2),
}
for _, row in grp.iterrows()
],
}
)
if segment_breakdown:
lead_segment = segment_breakdown[0]
leaders = lead_segment["leaders"][:8]
segment_comparison = {
"dimension": lead_segment["dimension"],
"metric": lead_segment["metric"],
"labels": [item["name"] for item in leaders],
"values": [item["mean"] for item in leaders],
"counts": [item["count"] for item in leaders],
}
# ==========================================
# 4. 时间序列分析 (支持多个数值列)
# ==========================================
time_series = {"name": "", "dates": [], "values": []}
time_series_multi = [] # 额外的时序列数据
time_series_diagnostics = {}
if numeric_cols:
date_col = None
for col in df.columns:
if df[col].dtype == "object":
try:
pd.to_datetime(df[col].dropna().head(100))
date_col = col
break
except Exception:
pass
if date_col:
df_ts = df.copy()
df_ts[date_col] = pd.to_datetime(df_ts[date_col], errors="coerce")
df_ts = df_ts.dropna(subset=[date_col])
# 主时序:第一个数值列
num_col = primary_metric or numeric_cols[0]
df_ts_main = df_ts.dropna(subset=[num_col]).copy()
if not df_ts_main.empty:
df_ts_main = df_ts_main.set_index(date_col)
try:
monthly = df_ts_main[num_col].resample("M").mean().dropna()
if len(monthly) < 3:
monthly = df_ts_main[num_col].resample("D").mean().dropna()
monthly = monthly.tail(100)
time_series["name"] = num_col
time_series["dates"] = [
x.strftime("%Y-%m-%d") for x in monthly.index
]
time_series["values"] = [
round(float(x), 2) for x in monthly.values
]
if len(monthly) >= 2:
idx = np.arange(len(monthly), dtype=float)
slope = float(
np.polyfit(idx, monthly.values.astype(float), 1)[0]
)
first_val = float(monthly.iloc[0])
last_val = float(monthly.iloc[-1])
peak_idx = int(np.argmax(monthly.values))
trough_idx = int(np.argmin(monthly.values))
pct_change = (
safe_div(last_val - first_val, abs(first_val)) * 100
)
time_series_diagnostics = {
"date_col": date_col,
"metric": num_col,
"points": int(len(monthly)),
"start": round(first_val, 2),
"end": round(last_val, 2),
"change_pct": round(pct_change, 1),
"slope": round(slope, 4),
"volatility_pct": round(
safe_div(monthly.std(), abs(monthly.mean())) * 100,
1,
)
if float(monthly.mean()) != 0
else 0,
"peak_date": monthly.index[peak_idx].strftime(
"%Y-%m-%d"
),
"peak_value": round(float(monthly.iloc[peak_idx]), 2),
"trough_date": monthly.index[trough_idx].strftime(
"%Y-%m-%d"
),
"trough_value": round(
float(monthly.iloc[trough_idx]), 2
),
}
except Exception as e:
log(f"时间序列处理失败: {e}")
# 额外时序列(最多再加2个)
for extra_col in numeric_cols[1:3]:
df_ts_extra = df_ts.dropna(subset=[extra_col]).copy()
if not df_ts_extra.empty:
df_ts_extra = df_ts_extra.set_index(date_col)
try:
monthly_e = (
df_ts_extra[extra_col].resample("M").mean().dropna()
)
if len(monthly_e) < 3:
monthly_e = (
df_ts_extra[extra_col].resample("D").mean().dropna()
)
monthly_e = monthly_e.tail(100)
if len(monthly_e) >= 2:
time_series_multi.append(
{
"name": extra_col,
"dates": [
x.strftime("%Y-%m-%d")
for x in monthly_e.index
],
"values": [
round(float(x), 2) for x in monthly_e.values
],
}
)
except Exception:
pass
# ==========================================
# 5. 散点图数据 (前两个数值列)
# ==========================================
scatter = {}
if len(numeric_cols) >= 2:
if primary_metric and correlations["data"]:
corr_candidates = []
for i, j, val in correlations["data"]:
left = numeric_cols[i]
right = numeric_cols[j]
if left == right:
continue
if left == primary_metric:
corr_candidates.append((abs(val), right, primary_metric))
elif right == primary_metric:
corr_candidates.append((abs(val), left, primary_metric))
corr_candidates.sort(key=lambda x: x[0], reverse=True)
if corr_candidates:
_, partner, metric = corr_candidates[0]
col_x, col_y = partner, metric
else:
col_x, col_y = numeric_cols[0], numeric_cols[1]
else:
col_x, col_y = numeric_cols[0], numeric_cols[1]
df_scatter = df[[col_x, col_y]].dropna()
# 限制最多 500 个点,避免数据过大
if len(df_scatter) > 500:
df_scatter = df_scatter.sample(500, random_state=42)
scatter = {
"x_name": col_x,
"y_name": col_y,
"x": [round(float(v), 4) for v in df_scatter[col_x].tolist()],
"y": [round(float(v), 4) for v in df_scatter[col_y].tolist()],
}
# ==========================================
# 5b. 箱线图数据 (Box Plot) — 前 8 个数值列
# ==========================================
box_plots = {}
if numeric_cols:
for col in numeric_cols[:8]:
s = df[col].dropna()
if len(s) > 0:
q1 = float(s.quantile(0.25))
q3 = float(s.quantile(0.75))
iqr = q3 - q1
lower_fence = q1 - 1.5 * iqr
upper_fence = q3 + 1.5 * iqr
outlier_vals = s[(s < lower_fence) | (s > upper_fence)]
# Limit outlier points to 50 for rendering
outlier_list = [
round(float(v), 4)
for v in outlier_vals.head(50).tolist() # type: ignore[union-attr]
]
box_plots[col] = {
"min": round(float(s.min()), 4),
"q1": round(q1, 4),
"median": round(float(s.median()), 4),
"q3": round(q3, 4),
"max": round(float(s.max()), 4),
"lower_fence": round(max(float(s.min()), lower_fence), 4),
"upper_fence": round(min(float(s.max()), upper_fence), 4),
"outliers": outlier_list,
}
# ==========================================
# 5c. 异常值检测汇总 (IQR method)
# ==========================================
outliers = {}
if numeric_cols:
for col in numeric_cols[:8]:
s = df[col].dropna()
if len(s) > 0:
q1 = float(s.quantile(0.25))
q3 = float(s.quantile(0.75))
iqr = q3 - q1
lower = q1 - 1.5 * iqr
upper = q3 + 1.5 * iqr
n_outliers = int(((s < lower) | (s > upper)).sum())
outliers[col] = {
"count": n_outliers,
"pct": round((n_outliers / len(s)) * 100, 1)
if len(s) > 0
else 0,
"lower_bound": round(lower, 4),
"upper_bound": round(upper, 4),
}
# ==========================================
# 5d. Top/Bottom 排名 (数值列的 Top5 / Bottom5)
# ==========================================
top_bottom = {}
ranking_signal = {}
if numeric_cols and len(df) > 0:
rank_col = primary_metric or numeric_cols[0]
if rank_col:
df_sorted = df.dropna(subset=[rank_col]).sort_values(
rank_col, ascending=False
)
top5 = df_sorted.head(5)
bottom5 = df_sorted.tail(5).iloc[::-1] # reverse so worst first
def extract_ranked(subset):
labels = []
values = []
for _, row in subset.iterrows():
lbl = str(row[label_col])[:30] if label_col else str(row.name)
labels.append(lbl)
values.append(round(float(row[rank_col]), 2))
return {"labels": labels, "values": values}
top_bottom = {
"rank_col": rank_col,
"label_col": label_col or "index",
"top5": extract_ranked(top5),
"bottom5": extract_ranked(bottom5),
}
if top_bottom["top5"]["values"] and top_bottom["bottom5"]["values"]:
top_avg = float(np.mean(top_bottom["top5"]["values"]))
bottom_avg = float(np.mean(top_bottom["bottom5"]["values"]))
ranking_signal = {
"top_avg": round(top_avg, 2),
"bottom_avg": round(bottom_avg, 2),
"gap": round(top_avg - bottom_avg, 2),
}
# ==========================================
# 6b. 主指标异动概览与归因结构
# ==========================================
anomaly_overview = {}
driver_analysis = {"metric": "", "items": []}
if primary_metric and primary_metric in df.columns:
metric_series = df[primary_metric].dropna()
if len(metric_series) > 0:
q10 = float(metric_series.quantile(0.1))
q25 = float(metric_series.quantile(0.25))
q50 = float(metric_series.quantile(0.5))
q75 = float(metric_series.quantile(0.75))
q90 = float(metric_series.quantile(0.9))
band_labels = ["P0-P25", "P25-P50", "P50-P75", "P75-P100"]
band_values = [
int((metric_series <= q25).sum()),
int(((metric_series > q25) & (metric_series <= q50)).sum()),
int(((metric_series > q50) & (metric_series <= q75)).sum()),
int((metric_series > q75).sum()),
]
top_group = df[df[primary_metric] >= q90]
bottom_group = df[df[primary_metric] <= q10]
primary_outlier = outliers.get(primary_metric, {})
anomaly_overview = {
"metric": primary_metric,
"mean": round(float(metric_series.mean()), 2),
"median": round(float(metric_series.median()), 2),
"std": round(float(metric_series.std()), 2),
"q10": round(q10, 2),
"q90": round(q90, 2),
"top_group_size": int(len(top_group)),
"bottom_group_size": int(len(bottom_group)),
"top_group_mean": round(float(top_group[primary_metric].mean()), 2)
if len(top_group) > 0
else 0,
"bottom_group_mean": round(
float(bottom_group[primary_metric].mean()), 2
)
if len(bottom_group) > 0
else 0,
"gap": round(
float(top_group[primary_metric].mean())
- float(bottom_group[primary_metric].mean()),
2,
)
if len(top_group) > 0 and len(bottom_group) > 0
else 0,
"band_labels": band_labels,
"band_values": band_values,
"outlier_count": int(primary_outlier.get("count", 0)),
"outlier_pct": float(primary_outlier.get("pct", 0)),
}
driver_items = []
metric_std = float(metric_series.std()) if len(metric_series) > 1 else 0
for col in numeric_cols:
if col == primary_metric:
continue
pair = df[[primary_metric, col]].dropna()
if len(pair) < 5:
continue
metric_pair_series = pair.iloc[:, 0]
col_pair_series = pair.iloc[:, 1]
corr_val = float(metric_pair_series.corr(col_pair_series))
top_mean = (
float(top_group[col].mean())
if len(top_group) > 0 and col in top_group.columns
else 0
)
bottom_mean = (
float(bottom_group[col].mean())
if len(bottom_group) > 0 and col in bottom_group.columns
else 0
)
col_std = float(col_pair_series.std()) if len(pair) > 1 else 0
gap_ratio = safe_div(
top_mean - bottom_mean, col_std if col_std else 1
)
score = round(
min(100, abs(corr_val) * 55 + min(abs(gap_ratio), 3) * 15), 1
)
driver_items.append(
{
"name": col,
"corr": round(corr_val, 3),
"top_mean": round(top_mean, 2),
"bottom_mean": round(bottom_mean, 2),
"gap_ratio": round(gap_ratio, 2),
"score": score,
}
)
driver_items.sort(key=lambda x: x["score"], reverse=True)
driver_analysis = {"metric": primary_metric, "items": driver_items[:8]}
# ==========================================
# 6. 统计汇总表格 (含新增 P5/P95/CV 列)
# ==========================================
stats_table = {"headers": [], "rows": []}
if numeric_summary:
stats_table["headers"] = [
"变量",
"最小值",
"P5",
"Q25",
"中位数",
"均值",
"Q75",
"P95",
"最大值",
"标准差",
"CV%",
"偏度",
"峰度",
]
for col, s in numeric_summary.items():
stats_table["rows"].append(
[
col,
round(s["min"], 2),
round(s["p5"], 2),
round(s["q25"], 2),
round(s["median"], 2),
round(s["mean"], 2),
round(s["q75"], 2),
round(s["p95"], 2),
round(s["max"], 2),
round(s["std"], 2),
s["cv"],
s["skewness"],
s["kurtosis"],
]
)
# ==========================================
# 构建给 ECharts 渲染的完整 JSON 数据结构
# ==========================================
chart_data = {
"overview": overview,
"data_quality": data_quality,
"numeric_cols": numeric_cols,
"distributions": distributions,
"correlations": correlations,
"correlation_highlights": correlation_highlights,
"categories": categories,
"segment_breakdown": segment_breakdown,
"time_series": time_series,
"time_series_multi": time_series_multi,
"time_series_diagnostics": time_series_diagnostics,
"scatter": scatter,
"box_plots": box_plots,
"outliers": outliers,
"primary_metric": primary_metric,
"anomaly_overview": anomaly_overview,
"driver_analysis": driver_analysis,
"segment_comparison": segment_comparison,
"top_bottom": top_bottom,
"ranking_signal": ranking_signal,
"stats_table": stats_table,
}
chart_data_json_str = json.dumps(chart_data, ensure_ascii=False)
# ==========================================
# 构建给 LLM 深度分析阅读的文本摘要
# ==========================================
summary_lines = [
"==================================================",
"【数据概览】",
f"- 数据集尺寸: {overview['rows']}× {overview['cols']}",
f"- 缺失值情况: 共有 {overview['missing_cells']} 个单元格缺失,整体数据完整率 {100 - overview['missing_pct']}%",
f"- 重复行: {overview['duplicate_rows']}",
f"- 内存占用: {overview['memory_kb']} KB",
f"- 数值型列 ({len(numeric_cols)}): {', '.join(numeric_cols[:10])}",
f"- 分类型列 ({len(categorical_cols)}): {', '.join(categorical_cols[:10])}",
f"- 数据类型分布: {dtype_counts}",
"",
"【质量关注点】",
]
quality_focus = [
(col, rate, miss_count)
for col, rate, miss_count in missing_by_col[:5]
if rate > 0
]
if quality_focus:
for col, rate, miss_count in quality_focus:
summary_lines.append(f"- {col}: 缺失 {miss_count} 个,占比 {rate}%")
else:
summary_lines.append("- 所有字段均无缺失,数据完整性较高")
summary_lines.extend(
[
"",
"【数值型特征统计 (Top 8)】",
]
)
for col, s in numeric_summary.items():
summary_lines.append(
f"- {col}: min={s['min']:.2f}, P5={s['p5']:.2f}, Q25={s['q25']:.2f}, "
f"median={s['median']:.2f}, mean={s['mean']:.2f}, Q75={s['q75']:.2f}, "
f"P95={s['p95']:.2f}, max={s['max']:.2f}, std={s['std']:.2f}, "
f"CV={s['cv']}%({classify_cv(s['cv'])}), spread={s['spread']:.2f}, "
f"skew={s['skewness']}({classify_skewness(s['skewness'])}), kurtosis={s['kurtosis']}"
)
if numeric_summary:
volatile_cols = sorted(
numeric_summary.items(), key=lambda x: x[1]["cv"], reverse=True
)[:3]
summary_lines.append("")
summary_lines.append("【波动性与偏态重点】")
for col, s in volatile_cols:
summary_lines.append(
f"- {col}: 波动等级={classify_cv(s['cv'])}, CV={s['cv']}%, 偏态={classify_skewness(s['skewness'])}"
)
# 异常值摘要
if outliers:
summary_lines.append("")
summary_lines.append("【异常值检测 (IQR 方法)】")
for col, info in outliers.items():
if info["count"] > 0:
summary_lines.append(
f"- {col}: {info['count']} 个异常值 ({info['pct']}%), "
f"正常范围 [{info['lower_bound']}, {info['upper_bound']}]"
)
if not any(info["count"] > 0 for info in outliers.values()):
summary_lines.append("- 未检测到显著异常值")
# Top/Bottom 排名
if top_bottom:
summary_lines.append("")
summary_lines.append(
f"【Top 5 / Bottom 5 排名 (按 {top_bottom['rank_col']})】"
)
summary_lines.append(
f" Top 5: {list(zip(top_bottom['top5']['labels'], top_bottom['top5']['values']))}"
)
summary_lines.append(
f" Bottom 5: {list(zip(top_bottom['bottom5']['labels'], top_bottom['bottom5']['values']))}"
)
if ranking_signal:
summary_lines.append(
f" 排名断层: Top5均值={ranking_signal['top_avg']}, Bottom5均值={ranking_signal['bottom_avg']}, 差值={ranking_signal['gap']}"
)
if anomaly_overview:
summary_lines.append("")
summary_lines.append("【数据异动概述】")
summary_lines.append(
f"- 核心分析指标: {anomaly_overview['metric']},均值={anomaly_overview['mean']},中位数={anomaly_overview['median']}P10={anomaly_overview['q10']}P90={anomaly_overview['q90']}"
)
summary_lines.append(
f"- 高位组({anomaly_overview['top_group_size']}个样本)均值={anomaly_overview['top_group_mean']},低位组({anomaly_overview['bottom_group_size']}个样本)均值={anomaly_overview['bottom_group_mean']},差值={anomaly_overview['gap']}"
)
summary_lines.append(
f"- 主指标异常值数量={anomaly_overview['outlier_count']},占比={anomaly_overview['outlier_pct']}%,分位带样本分布={list(zip(anomaly_overview['band_labels'], anomaly_overview['band_values']))}"
)
if driver_analysis.get("items"):
summary_lines.append("")
summary_lines.append("【归因分析线索】")
for item in driver_analysis["items"][:5]:
summary_lines.append(
f"- {item['name']}: 综合驱动分={item['score']},与 {driver_analysis['metric']} 的相关系数={item['corr']},高位组均值={item['top_mean']},低位组均值={item['bottom_mean']},组间差异强度={item['gap_ratio']}"
)
summary_lines.append("")
summary_lines.append("【分类型特征摘要 (Top 6)】")
for col, stats in cat_summary.items():
summary_lines.append(
f"- {col}: 唯一值={stats['n_unique']}, 最常见={stats['top1']} "
f"(出现{stats['top1_count']}次, 占比{stats['top1_share']}%), 熵={stats['entropy']}, "
f"Top3集中度={stats['top3_share']}%"
)
if segment_breakdown:
summary_lines.append("")
summary_lines.append("【分类维度切片表现】")
for segment in segment_breakdown:
leaders = segment["leaders"][:3]
leader_text = "; ".join(
[
f"{item['name']}(样本{item['count']}, 均值{item['mean']}, 总量{item['sum']})"
for item in leaders
]
)
summary_lines.append(
f"- 维度 {segment['dimension']} 对指标 {segment['metric']} 的高贡献分组: {leader_text}"
)
summary_lines.append("")
summary_lines.append("【核心相关性】")
if correlations["data"]:
strong_corrs = []
for item in correlations["data"]:
i, j, val = item
if i < j and abs(val) >= 0.5:
strong_corrs.append(
f"{numeric_cols[i]}{numeric_cols[j]} (相关系数: {val})"
)
if strong_corrs:
summary_lines.extend([f"- {c}" for c in strong_corrs])
else:
summary_lines.append("- 没有发现强相关的数值变量组合(|r| >= 0.5)。")
if correlation_highlights["positive"]:
summary_lines.append("- 最高正相关组合:")
for c1, c2, val in correlation_highlights["positive"]:
summary_lines.append(f" * {c1} vs {c2}: {val}")
if correlation_highlights["negative"]:
summary_lines.append("- 最低相关组合:")
for c1, c2, val in correlation_highlights["negative"]:
summary_lines.append(f" * {c1} vs {c2}: {val}")
if scatter:
summary_lines.append("")
summary_lines.append(
f"【散点图】已生成 {scatter['x_name']} vs {scatter['y_name']} 的散点图数据"
)
# 时间序列摘要
if time_series["dates"]:
summary_lines.append("")
summary_lines.append(
f"【时间序列】检测到时间列,已按月/日聚合 {time_series['name']} 趋势"
)
if time_series_diagnostics:
summary_lines.append(
f"- 时间字段={time_series_diagnostics['date_col']}, 观测点={time_series_diagnostics['points']}, "
f"起点={time_series_diagnostics['start']}, 终点={time_series_diagnostics['end']}, "
f"整体变化={time_series_diagnostics['change_pct']}%, 斜率={time_series_diagnostics['slope']}, "
f"波动率={time_series_diagnostics['volatility_pct']}%"
)
summary_lines.append(
f"- 峰值出现在 {time_series_diagnostics['peak_date']} ({time_series_diagnostics['peak_value']}), "
f"谷值出现在 {time_series_diagnostics['trough_date']} ({time_series_diagnostics['trough_value']})"
)
if time_series_multi:
extra_names = [ts_m["name"] for ts_m in time_series_multi]
summary_lines.append(f" 额外趋势列: {', '.join(extra_names)}")
summary_lines.append("==================================================")
summary_lines.append(
"请作为数据分析专家,基于以上【统计摘要】为用户撰写深度的数据分析见解(Insights)。每个模块尽量覆盖现象、可能原因、业务影响、行动建议四层内容,避免只重复统计值。"
)
summary_lines.append(
"注意:marker 中包裹的 CHART_DATA_JSON 会由后端自动注入模板,你无需手动传递。"
)
summary_lines.append("###CHART_DATA_JSON_START###")
summary_lines.append(chart_data_json_str)
summary_lines.append("###CHART_DATA_JSON_END###")
final_text = "\n".join(summary_lines)
# 输出标准 chunks
print(
json.dumps(
{"chunks": [{"output_type": "text", "content": final_text}]},
ensure_ascii=False,
)
)
except Exception as e:
import traceback
err_msg = f"分析过程中出现错误: {str(e)}\n{traceback.format_exc()}"
print(
json.dumps(
{"chunks": [{"output_type": "text", "content": err_msg}]},
ensure_ascii=False,
)
)
def main():
if len(sys.argv) < 2:
result = {
"chunks": [
{
"output_type": "text",
"content": '使用方法: python csv_analyzer.py \'{"input_file": "data.csv"}\'',
}
]
}
print(json.dumps(result, ensure_ascii=False))
sys.exit(1)
try:
args = json.loads(sys.argv[1])
csv_file = (
args.get("input_file") or args.get("file_path") or args.get("csv_file", "")
)
except (ValueError, TypeError):
csv_file = sys.argv[1]
if not csv_file or not os.path.exists(csv_file):
result = {
"chunks": [{"output_type": "text", "content": f"文件不存在: {csv_file}"}]
}
print(json.dumps(result, ensure_ascii=False))
sys.exit(1)
SUPPORTED_EXTENSIONS = (".csv", ".xls", ".xlsx", ".tsv")
if not csv_file.lower().endswith(SUPPORTED_EXTENSIONS):
result = {
"chunks": [
{
"output_type": "text",
"content": (
f"不支持的文件格式: {csv_file}"
f"支持的格式: {', '.join(SUPPORTED_EXTENSIONS)}"
),
}
]
}
print(json.dumps(result, ensure_ascii=False))
sys.exit(1)
analyze_csv(csv_file)
if __name__ == "__main__":
main()
+24
View File
@@ -0,0 +1,24 @@
#!/usr/bin/env python3
"""
Example script for the skill.
This script can be called by Claude to perform deterministic operations.
Replace this with your actual skill logic.
"""
import sys
import json
def main():
"""Main entry point."""
if len(sys.argv) < 2:
print("Usage: python3 example.py <input>")
sys.exit(1)
input_data = sys.argv[1]
# TODO: Add your skill logic here
result = {"status": "success", "input": input_data}
print(json.dumps(result, indent=2))
if __name__ == "__main__":
main()
File diff suppressed because it is too large Load Diff
+108
View File
@@ -0,0 +1,108 @@
---
name: financial-report-analyzer
description: 专门用于上市公司财报(如年度报告、季度报告)的深度分析。该技能能够自动提取关键财务指标,计算核心财务比率,生成可视化图表,并结合行业背景生成专业的财务分析报告。
---
# 财报分析技能 (Financial Report Analyzer)
本技能旨在帮助 DB-GPT 系统化地分析上市公司财报,通过提取核心数据、计算财务比率、生成可视化图表并结合业务背景,产出高质量的财务分析报告。
## 核心工作流程
1. **数据提取与结构化**
- 使用 `execute_skill_script_file` 工具执行 `scripts/extract_financials.py` 脚本,传入财报文件路径(`file_path` 参数),自动提取营收、净利润、资产、负债等核心数值。
- 脚本支持 PDF 文件(通过 pdfplumber 解析)和纯文本文件,返回 JSON 格式的结构化数据。
2. **财务比率计算**
- 使用 `execute_skill_script_file` 执行 `scripts/calculate_ratios.py`,传入 Step 1 的 JSON 数据。
- 自动计算毛利率、净利率、ROE、资产负债率等关键指标,输出 30 个模板占位符键值。
- 参考 `references/financial_metrics.md` 确保指标定义的准确性。
- **系统会自动保存返回的 JSON 结果**(`react_state["ratio_data"]`),后续 html_interpreter 会自动合并。
3. **图表生成**
- 使用 `execute_skill_script_file` 执行 `scripts/generate_charts.py`,传入 Step 1 的 JSON 数据。
- 自动生成 3 张可视化图表:
- `financial_overview.png`:核心财务指标对比柱状图
- `profitability.png`:盈利能力指标横向条形图
- `asset_structure.png`:资产结构环形饼图
- **系统会自动将图片复制到静态目录并记录 URL 映射**(`react_state["image_url_map"]`),后续 html_interpreter 会自动合并。
4. **深度分析**
- 遵循 `references/analysis_framework.md` 提供的框架,从盈利质量、偿债风险、营运效率和现金流四个维度进行深度剖析。
- 结合"经营情况讨论与分析"章节,解释业绩变动的核心驱动因素。
- 撰写以下 7 段分析文本:
- `PROFITABILITY_ANALYSIS`:盈利能力分析
- `SOLVENCY_ANALYSIS`:偿债与风险分析
- `EFFICIENCY_ANALYSIS`:营运效率分析
- `CASHFLOW_ANALYSIS`:现金流与利润质量分析
- `ADVANTAGES_LIST`:核心优势列表(HTML `<li>` 格式)
- `RISKS_LIST`:主要风险列表(HTML `<li>` 格式)
- `OVERALL_ASSESSMENT`:综合评价
5. **渲染报告**
- 调用 `html_interpreter`,使用 `template_path` 模式:
```json
{
"template_path": "financial-report-analyzer/templates/report_template.html",
"data": {
"PROFITABILITY_ANALYSIS": "LLM撰写的盈利能力分析...",
"SOLVENCY_ANALYSIS": "LLM撰写的偿债分析...",
"EFFICIENCY_ANALYSIS": "LLM撰写的营运效率分析...",
"CASHFLOW_ANALYSIS": "LLM撰写的现金流分析...",
"ADVANTAGES_LIST": "<li>优势1</li><li>优势2</li>",
"RISKS_LIST": "<li>风险1</li><li>风险2</li>",
"OVERALL_ASSESSMENT": "LLM撰写的综合评价..."
},
"title": "XX公司 2023年度财报分析报告"
}
```
- **重要**`data` 字典中只需传入你撰写的 7 段分析文本!后端会自动合并:
- Step 2 的 30 个数据指标(COMPANY_NAME、REVENUE、NET_PROFIT 等)
- Step 3 的图表 URLCHART_FINANCIAL_OVERVIEW、CHART_PROFITABILITY、CHART_ASSET_STRUCTURE
- **绝对不要**在 `data` 中包含数据指标或图表路径,否则 JSON 过大会导致截断。
6. **完成**
- 调用 `terminate` 返回 1-2 句话的简短摘要。
- 报告会以卡片形式展示在左侧面板,用户点击卡片即可在右侧面板查看完整报告。
## 完整流程示例
```
Step 1: execute_skill_script_file(skill_name="financial-report-analyzer", script_file_name="extract_financials.py", args={"file_path": "/path/to/report.pdf"})
→ 返回 JSON: {"revenue": 10500000000, "net_profit": 1200000000, ...} (记为 raw_data
Step 2: execute_skill_script_file(skill_name="financial-report-analyzer", script_file_name="calculate_ratios.py", args=<raw_data>)
→ 返回 30 个模板键值,系统自动记录到 react_state["ratio_data"]
Step 3: execute_skill_script_file(skill_name="financial-report-analyzer", script_file_name="generate_charts.py", args=<raw_data>)
→ 生成图表,系统自动复制到 /images/ 并记录 URL 映射
Step 4: (LLM 自行撰写 7 段深度分析文本)
Step 5: html_interpreter(template_path="financial-report-analyzer/templates/report_template.html", data={仅包含 7 段分析文本}, title="报告标题")
→ 后端自动合并数据指标 + 图表 URL + 分析文本,渲染完整报告
Step 6: terminate(result="简短摘要")
```
## 资源使用说明
- **脚本**(均通过 `execute_skill_script_file` 执行):
- `scripts/extract_financials.py`:接收 `file_path` 参数,读取财报文件(支持 PDF 和文本格式),提取核心财务数据。
- `scripts/calculate_ratios.py`:计算财务比率,输出 30 个模板占位符键值。系统自动记录结果。
- `scripts/generate_charts.py`:生成 3 张可视化图表(matplotlib),系统自动处理图片复制。
- `scripts/fill_template.py`:(备用)接收 `ratio_data`、`chart_paths`、`analysis` 三个参数,读取 HTML 模板并替换所有占位符。正常情况下不需要使用此脚本,因为 html_interpreter 的 template_path 模式会自动完成模板填充。
- **参考**
- `references/financial_metrics.md`:包含公式定义。
- `references/analysis_framework.md`:包含分析逻辑。
- **模板**
- `templates/report_template.html`:最终交付报告的 HTML 模板(**必须严格遵循**,不得删减章节或修改表格结构)。由 html_interpreter 的 template_path 参数自动读取并填充。
- `templates/report_template.md`:Markdown 版本,仅供参考结构说明。
## 注意事项
- **必须使用 `execute_skill_script_file`** 执行脚本(不要用 shell_interpreter),因为 `execute_skill_script_file` 会自动处理图片复制和数据记录。
- 脚本提取可能受排版影响,建议在计算前人工核对提取的关键数值。
- 始终关注"非经常性损益",以评估公司核心业务的真实盈利能力。
- 对比至少三年的历史数据,以识别趋势。
- `generate_charts.py` 依赖 matplotlib,请确保环境中已安装该库。
@@ -0,0 +1,23 @@
# 财报分析标准框架
在进行财报分析时,应遵循以下逻辑步骤以确保分析的全面性和深度。
## 1. 概览与业绩摘要
- 提取年度营业收入、净利润及其同比增长率。
- 关注非经常性损益对利润的影响。
## 2. 盈利质量分析
- **毛利率趋势**:对比近三年毛利率,分析是成本下降还是售价提升。
- **费用控制**:观察销售费用、管理费用、研发费用的增速是否超过营收增速。
## 3. 资产负债表健康度
- **资产结构**:关注货币资金、应收账款、存货、固定资产的占比。
- **负债风险**:检查有息负债规模及偿债压力。
## 4. 现金流验证
- 经营活动现金流是否持续为正。
- 资本开支(投资活动现金流)是否与公司扩张战略匹配。
## 5. 业务与行业地位
- 主营业务的行业竞争力。
- 研发投入及其转化效果。
@@ -0,0 +1,14 @@
# 核心财务指标参考指南
本参考资料定义了财报分析中常用的核心指标及其计算公式。
| 指标类别 | 指标名称 | 计算公式 | 意义 |
| :--- | :--- | :--- | :--- |
| **盈利能力** | 毛利率 | (营业收入 - 营业成本) / 营业收入 | 衡量产品竞争力和盈利空间 |
| | 净利率 | 净利润 / 营业收入 | 衡量公司最终获利能力 |
| | ROE (净资产收益率) | 净利润 / 平均净资产 | 衡量股东权益的投资回报率 |
| **偿债能力** | 资产负债率 | 总负债 / 总资产 | 衡量公司财务杠杆和风险水平 |
| | 流动比率 | 流动资产 / 流动负债 | 衡量短期偿债能力 |
| **营运能力** | 应收账款周转率 | 营业收入 / 平均应收账款余额 | 衡量回款效率 |
| | 存货周转率 | 营业成本 / 平均存货余额 | 衡量库存管理效率 |
| **现金流** | 净现比 | 经营活动现金流净额 / 净利润 | 衡量利润的含金量 |
@@ -0,0 +1,213 @@
import json
import sys
from datetime import datetime
def format_amount(value):
"""Auto-scale a numeric amount to 亿元, 万元, or 元.
Returns a formatted string like "105.00亿元", "1050.50万元", or "3500.00元".
Returns "N/A" when *value* is ``None`` or not a valid number.
"""
if value is None:
return "N/A"
try:
value = float(value)
except (TypeError, ValueError):
return "N/A"
abs_val = abs(value)
if abs_val >= 1e8:
return f"{value / 1e8:.2f}亿元"
if abs_val >= 1e4:
return f"{value / 1e4:.2f}万元"
return f"{value:.2f}"
def format_growth(current, previous):
"""Calculate YoY growth and return (formatted_string, css_class).
* Returns ("+15.3%", "highlight-positive") when growth >= 0.
* Returns ("-5.2%", "highlight-negative") when growth < 0.
* Returns ("N/A", "") when either value is missing or previous is zero.
"""
if current is None or previous is None:
return "N/A", ""
try:
current = float(current)
previous = float(previous)
except (TypeError, ValueError):
return "N/A", ""
if previous == 0:
return "N/A", ""
growth = (current - previous) / abs(previous) * 100
if growth >= 0:
return f"+{growth:.1f}%", "highlight-positive"
return f"{growth:.1f}%", "highlight-negative"
def format_pct(value):
"""Format a ratio (0-1 scale or already percent-scale) as "XX.XX%".
Assumes *value* is on a 0-1 scale (e.g. 0.2857 → "28.57%").
Returns "N/A" when *value* is ``None``.
"""
if value is None:
return "N/A"
try:
value = float(value)
except (TypeError, ValueError):
return "N/A"
return f"{value * 100:.2f}%"
def format_pp_change(current, previous):
"""Calculate percentage-point change and return (formatted_string, css_class).
Both *current* and *previous* should be ratios on a 0-1 scale.
Returns ("+2.3pp", "highlight-positive") or ("-1.5pp", "highlight-negative").
Returns ("N/A", "") when either value is missing.
"""
if current is None or previous is None:
return "N/A", ""
try:
current = float(current)
previous = float(previous)
except (TypeError, ValueError):
return "N/A", ""
change = (current - previous) * 100 # convert to percentage points
if change >= 0:
return f"+{change:.1f}pp", "highlight-positive"
return f"{change:.1f}pp", "highlight-negative"
def calculate_template_data(data):
"""Turn raw financial data into a dict of ALL template placeholder values.
Parameters
----------
data : dict
Raw financial data as produced by ``extract_financials.py``.
Returns
-------
dict
Keys correspond 1-to-1 with ``{{PLACEHOLDER}}`` names in the HTML
report template. Every key listed in the spec is always present.
"""
# ------------------------------------------------------------------ helpers
def _get(key, default=None):
"""Fetch a numeric value, returning *default* for None / missing."""
v = data.get(key)
if v is None:
return default
try:
return float(v)
except (TypeError, ValueError):
return default
# ----------------------------------------------------------- raw values
revenue = _get("revenue")
net_profit = _get("net_profit")
equity = _get("equity")
cost_of_sales = _get("cost_of_sales")
non_recurring_net_profit = _get("non_recurring_net_profit")
prev_revenue = _get("prev_revenue")
prev_net_profit = _get("prev_net_profit")
prev_non_recurring = _get("prev_non_recurring_net_profit")
prev_gross_margin = _get("prev_gross_margin") # ratio 0-1
prev_roe = _get("prev_roe") # ratio 0-1
# -------------------------------------------------------- derived ratios
gross_margin = None
if revenue is not None and cost_of_sales is not None and revenue != 0:
gross_margin = (revenue - cost_of_sales) / revenue
roe = None
if net_profit is not None and equity is not None and equity != 0:
roe = net_profit / equity
# -------------------------------------------------------- formatted values
result = {}
# --- Basic info ---
result["COMPANY_NAME"] = data.get("company_name") or "未知公司"
result["YEAR"] = str(data.get("report_year") or data.get("year") or "N/A")
result["DATE"] = data.get("report_date") or datetime.now().strftime("%Y年%m月%d")
# --- Revenue ---
result["REVENUE"] = format_amount(revenue)
rev_growth, rev_cls = format_growth(revenue, prev_revenue)
result["REVENUE_GROWTH"] = rev_growth
result["REVENUE_GROWTH_CLASS"] = rev_cls
result["REVENUE_INDUSTRY_AVG"] = "N/A"
# --- Net profit ---
result["NET_PROFIT"] = format_amount(net_profit)
np_growth, np_cls = format_growth(net_profit, prev_net_profit)
result["NET_PROFIT_GROWTH"] = np_growth
result["NET_PROFIT_GROWTH_CLASS"] = np_cls
result["NET_PROFIT_INDUSTRY_AVG"] = "N/A"
# --- Non-recurring net profit ---
result["NON_RECURRING_NET_PROFIT"] = format_amount(non_recurring_net_profit)
nr_growth, nr_cls = format_growth(non_recurring_net_profit, prev_non_recurring)
result["NON_RECURRING_GROWTH"] = nr_growth
result["NON_RECURRING_GROWTH_CLASS"] = nr_cls
result["NON_RECURRING_INDUSTRY_AVG"] = "N/A"
# --- Gross margin ---
result["GROSS_MARGIN"] = format_pct(gross_margin)
gm_change, gm_cls = format_pp_change(gross_margin, prev_gross_margin)
result["GROSS_MARGIN_CHANGE"] = gm_change
result["GROSS_MARGIN_CHANGE_CLASS"] = gm_cls
result["GROSS_MARGIN_INDUSTRY_AVG"] = "N/A"
# --- ROE ---
result["ROE"] = format_pct(roe)
roe_change, roe_cls = format_pp_change(roe, prev_roe)
result["ROE_CHANGE"] = roe_change
result["ROE_CHANGE_CLASS"] = roe_cls
result["ROE_INDUSTRY_AVG"] = "N/A"
# --- Analysis placeholders (LLM fills these) ---
result["PROFITABILITY_ANALYSIS"] = "N/A"
result["SOLVENCY_ANALYSIS"] = "N/A"
result["EFFICIENCY_ANALYSIS"] = "N/A"
result["CASHFLOW_ANALYSIS"] = "N/A"
result["ADVANTAGES_LIST"] = "N/A"
result["RISKS_LIST"] = "N/A"
result["OVERALL_ASSESSMENT"] = "N/A"
return result
if __name__ == "__main__":
if len(sys.argv) > 1:
try:
arg = sys.argv[1]
parsed = json.loads(arg)
# Handle various input shapes from the LLM/adapter:
# {"financial_data": {"revenue": ...}} -> unwrap
# {"data": {"revenue": ...}} -> unwrap
# {"revenue": ..., "net_profit": ...} -> use directly
if isinstance(parsed, dict):
# If wrapped in a single key whose value is also a dict,
# unwrap it.
if len(parsed) == 1:
only_value = next(iter(parsed.values()))
if isinstance(only_value, dict):
parsed = only_value
result = calculate_template_data(parsed)
print(json.dumps(result, indent=2, ensure_ascii=False))
except Exception as e:
print(json.dumps({"error": str(e)}, ensure_ascii=False))
else:
print("Please provide JSON data as an argument.")
@@ -0,0 +1,196 @@
import re
import json
import sys
import os
def read_file_content(file_path):
"""Read content from a file, supporting both text and PDF formats."""
_, ext = os.path.splitext(file_path.lower())
if ext == ".pdf":
try:
import pdfplumber
text_parts = []
with pdfplumber.open(file_path) as pdf:
for page in pdf.pages:
page_text = page.extract_text()
if page_text:
text_parts.append(page_text)
return "\n".join(text_parts)
except ImportError:
raise RuntimeError(
"pdfplumber is required to read PDF files. "
"Install it with: pip install pdfplumber"
)
else:
with open(file_path, "r", encoding="utf-8") as f:
return f.read()
def extract_from_text(text):
"""Extract key financial data from text using regex.
Scans Chinese financial reports for common accounting line items and
returns the first numeric match found for each metric.
"""
data = {
"company_name": None,
"report_year": None,
"report_date": None,
"revenue": None,
"net_profit": None,
"total_assets": None,
"total_liabilities": None,
"equity": None,
"operating_cash_flow": None,
"cost_of_sales": None,
}
# ── Extract basic info: company name, year, date ──────────────
# Company name: look for patterns like "XX公司" or "XX股份有限公司"
company_patterns = [
r"公司名称[:\s]*([\u4e00-\u9fa5]{2,}(?:股份有限公司|有限责任公司|有限公司|集团|公司))",
r"([\u4e00-\u9fa5]{2,}(?:股份有限公司|有限责任公司|有限公司))\s*\d{4}\s*年",
r"([\u4e00-\u9fa5]{2,}(?:股份有限公司|有限责任公司|有限公司))",
]
for pat in company_patterns:
m = re.search(pat, text[:3000])
if m:
data["company_name"] = m.group(1).strip()
break
# Report year: "2023年年度报告" / "2023年度" / "2023 年"
year_patterns = [
r"(\d{4})\s*年\s*(?:年度|半年度|第[一二三四]季度)?\s*报告",
r"(\d{4})\s*年度",
r"(\d{4})\s*年",
]
for pat in year_patterns:
m = re.search(pat, text[:5000])
if m:
data["report_year"] = m.group(1)
break
# Report date: "报告期 2023-12-31" / "2023年12月31日"
date_patterns = [
r"报告期[末::\s]*(\d{4}[-/年]\d{1,2}[-/月]\d{1,2}日?)",
r"(\d{4}\d{1,2}月\d{1,2}日)",
r"(\d{4}-\d{2}-\d{2})",
]
for pat in date_patterns:
m = re.search(pat, text[:5000])
if m:
data["report_date"] = m.group(1)
break
patterns = {
"revenue": [
r"营业收入[^\d]*?([\d,]+\.?\d*)",
r"营业总收入[^\d]*?([\d,]+\.?\d*)",
],
"net_profit": [
r"归属于上市公司股东的净利润[^\d]*?([\d,]+\.?\d*)",
r"净利润[^\d]*?([\d,]+\.?\d*)",
],
"total_assets": [
r"总资产[^\d]*?([\d,]+\.?\d*)",
r"资产总[计额][^\d]*?([\d,]+\.?\d*)",
],
"total_liabilities": [
r"总负债[^\d]*?([\d,]+\.?\d*)",
r"负债[总合][计额][^\d]*?([\d,]+\.?\d*)",
],
"equity": [
r"归属于上市公司股东的净资产[^\d]*?([\d,]+\.?\d*)",
r"所有者权益合计[^\d]*?([\d,]+\.?\d*)",
r"股东权益合计[^\d]*?([\d,]+\.?\d*)",
],
"operating_cash_flow": [
r"经营活动产生的现金流量净额[^\d]*?([\d,]+\.?\d*)",
],
"cost_of_sales": [
r"营业成本[^\d]*?([\d,]+\.?\d*)",
r"营业总成本[^\d]*?([\d,]+\.?\d*)",
],
}
for key, regex_list in patterns.items():
for regex in regex_list:
match = re.search(regex, text)
if match:
val_str = match.group(1).replace(",", "")
try:
data[key] = float(val_str)
break
except ValueError:
continue
return data
def extract_financials(file_path):
"""Extract key financial data from a file.
Args:
file_path: Path to the financial report file (supports .pdf, .txt, .md, etc.)
Returns:
dict with extracted financial metrics.
"""
if not os.path.exists(file_path):
return {"error": True, "message": f"File not found: {file_path}"}
try:
text = read_file_content(file_path)
except Exception as e:
return {"error": True, "message": f"Failed to read file: {e}"}
if not text or not text.strip():
return {"error": True, "message": "File is empty or could not be parsed"}
data = extract_from_text(text)
# Add a summary of which fields were successfully extracted
extracted = [k for k, v in data.items() if v is not None]
missing = [k for k, v in data.items() if v is None]
data["_meta"] = {
"file": os.path.basename(file_path),
"text_length": len(text),
"extracted_fields": extracted,
"missing_fields": missing,
}
return data
if __name__ == "__main__":
if len(sys.argv) > 1:
arg = sys.argv[1]
# Parse the JSON argument passed by execute_skill_script_file
try:
parsed = json.loads(arg)
if isinstance(parsed, dict):
fp = parsed.get("file_path", "")
else:
fp = str(parsed)
except json.JSONDecodeError:
fp = arg
if not fp:
print(
json.dumps(
{"error": True, "message": "Missing required parameter: file_path"},
ensure_ascii=False,
)
)
sys.exit(1)
result = extract_financials(fp)
print(json.dumps(result, indent=2, ensure_ascii=False))
else:
print(
'Usage: python3 extract_financials.py \'{"file_path": "/path/to/report.pdf"}\''
)
@@ -0,0 +1,460 @@
"""Generate financial analysis charts from JSON data.
Usage:
python generate_charts.py '<json_data>'
Produces 3 PNG charts in the same directory as this script and outputs
a JSON manifest with the chart file paths.
"""
import json
import os
import sys
import matplotlib
matplotlib.use("Agg") # Non-interactive backend — must be set before pyplot import
import matplotlib.font_manager as fm # noqa: E402
import matplotlib.pyplot as plt # noqa: E402
# ---------------------------------------------------------------------------
# Color palette (matches the HTML report theme)
# ---------------------------------------------------------------------------
COLOR_NAVY = "#0f3460"
COLOR_DARK = "#16213e"
COLOR_ACCENT = "#e94560"
COLOR_PURPLE = "#533483"
COLOR_GREEN = "#27ae60"
COLOR_LIGHT_BLUE = "#3498db"
COLOR_ORANGE = "#e67e22"
CHART_DPI = 150
# ---------------------------------------------------------------------------
# Font setup for Chinese characters
# ---------------------------------------------------------------------------
def setup_chinese_font():
"""Configure matplotlib to render Chinese characters correctly.
Tries a priority-ordered list of CJK fonts commonly available on
macOS, Linux, and Windows. Falls back to DejaVu Sans.
"""
candidates = [
# macOS
"Heiti TC",
"Hiragino Sans GB",
"PingFang SC",
"PingFang HK",
"STHeiti",
"Songti SC",
"Arial Unicode MS",
# Linux
"Noto Sans CJK SC",
"Noto Sans SC",
"WenQuanYi Micro Hei",
"WenQuanYi Zen Hei",
"Droid Sans Fallback",
# Windows
"Microsoft YaHei",
"SimHei",
"SimSun",
]
available = {f.name for f in fm.fontManager.ttflist}
for font_name in candidates:
if font_name in available:
plt.rcParams["font.sans-serif"] = [font_name, "sans-serif"]
plt.rcParams["font.family"] = "sans-serif"
plt.rcParams["axes.unicode_minus"] = False
return
plt.rcParams["font.sans-serif"] = ["DejaVu Sans"]
plt.rcParams["axes.unicode_minus"] = False
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
def format_yi(value):
"""Convert a raw number to 亿元 scale."""
return value / 1e8
def safe_float(value, default=0.0):
"""Safely convert *value* to float."""
if value is None:
return default
try:
return float(value)
except (TypeError, ValueError):
return default
# ---------------------------------------------------------------------------
# Chart 1 核心财务指标对比 (financial_overview.png)
# ---------------------------------------------------------------------------
def chart_financial_overview(data, output_dir):
"""Grouped bar chart of key financial metrics (in 亿元)."""
labels = ["营业收入", "营业成本", "净利润", "总资产", "总负债", "所有者权益"]
keys = [
"revenue",
"cost_of_sales",
"net_profit",
"total_assets",
"total_liabilities",
"equity",
]
current_values = [format_yi(safe_float(data.get(k))) for k in keys]
has_prev = any(data.get(f"prev_{k}") is not None for k in keys[:3])
prev_keys = [
"prev_revenue",
"prev_cost_of_sales",
"prev_net_profit",
None,
None,
None,
]
fig, ax = plt.subplots(figsize=(10, 6))
bar_width = 0.35
x_positions = list(range(len(labels)))
if has_prev:
prev_values = []
for pk in prev_keys:
if pk is not None and data.get(pk) is not None:
prev_values.append(format_yi(safe_float(data.get(pk))))
else:
prev_values.append(0)
x_curr = [x + bar_width / 2 for x in x_positions]
x_prev = [x - bar_width / 2 for x in x_positions]
year = data.get("year", "本期")
try:
prev_year = str(int(year) - 1)
except (ValueError, TypeError):
prev_year = "上期"
bars_prev = ax.bar(
x_prev,
prev_values,
width=bar_width,
label=f"{prev_year}",
color=COLOR_LIGHT_BLUE,
edgecolor="white",
linewidth=0.5,
)
bars_curr = ax.bar(
x_curr,
current_values,
width=bar_width,
label=f"{year}",
color=COLOR_NAVY,
edgecolor="white",
linewidth=0.5,
)
# Value labels for current year
for bar in bars_curr:
height = bar.get_height()
if height != 0:
ax.text(
bar.get_x() + bar.get_width() / 2,
height,
f"{height:.1f}",
ha="center",
va="bottom",
fontsize=8,
color=COLOR_DARK,
)
# Value labels for previous year (non-zero only)
for bar in bars_prev:
height = bar.get_height()
if height != 0:
ax.text(
bar.get_x() + bar.get_width() / 2,
height,
f"{height:.1f}",
ha="center",
va="bottom",
fontsize=8,
color=COLOR_LIGHT_BLUE,
)
ax.legend(loc="upper right", fontsize=9)
else:
colors = [
COLOR_NAVY,
COLOR_DARK,
COLOR_ACCENT,
COLOR_PURPLE,
COLOR_GREEN,
COLOR_LIGHT_BLUE,
]
bars = ax.bar(
x_positions,
current_values,
width=bar_width * 1.5,
color=colors,
edgecolor="white",
linewidth=0.5,
)
for bar in bars:
height = bar.get_height()
if height != 0:
ax.text(
bar.get_x() + bar.get_width() / 2,
height,
f"{height:.1f}",
ha="center",
va="bottom",
fontsize=8,
color=COLOR_DARK,
)
ax.set_xticks(x_positions)
ax.set_xticklabels(labels, fontsize=10)
ax.set_ylabel("金额(亿元)", fontsize=11)
company = data.get("company_name", "")
year_str = data.get("year", "")
ax.set_title(
f"{company} {year_str}年 核心财务指标对比", fontsize=14, fontweight="bold"
)
ax.grid(axis="y", linestyle="--", alpha=0.4, color="#cccccc")
ax.set_axisbelow(True)
ax.spines["top"].set_visible(False)
ax.spines["right"].set_visible(False)
plt.tight_layout()
path = os.path.join(output_dir, "financial_overview.png")
fig.savefig(path, dpi=CHART_DPI, facecolor="white", bbox_inches="tight")
plt.close(fig)
return path
# ---------------------------------------------------------------------------
# Chart 2 盈利能力指标 (profitability.png)
# ---------------------------------------------------------------------------
def chart_profitability(data, output_dir):
"""Bar chart of profitability / efficiency ratios (%)."""
revenue = safe_float(data.get("revenue"), default=None)
cost_of_sales = safe_float(data.get("cost_of_sales"), default=None)
net_profit = safe_float(data.get("net_profit"), default=None)
equity = safe_float(data.get("equity"), default=None)
total_assets = safe_float(data.get("total_assets"), default=None)
total_liabilities = safe_float(data.get("total_liabilities"), default=None)
operating_cash_flow = safe_float(data.get("operating_cash_flow"), default=None)
# Calculate ratios
gross_margin = 0.0
if revenue and cost_of_sales is not None:
gross_margin = (revenue - cost_of_sales) / revenue * 100
net_margin = 0.0
if revenue and net_profit is not None:
net_margin = net_profit / revenue * 100
roe = 0.0
if equity and net_profit is not None:
roe = net_profit / equity * 100
debt_ratio = 0.0
if total_assets and total_liabilities is not None:
debt_ratio = total_liabilities / total_assets * 100
cash_ratio = 0.0
if net_profit and operating_cash_flow is not None:
cash_ratio = operating_cash_flow / net_profit * 100
# Cap at 200% for display readability
cash_ratio = min(cash_ratio, 200.0)
labels = ["毛利率", "净利率", "ROE", "资产负债率", "净现比"]
values = [gross_margin, net_margin, roe, debt_ratio, cash_ratio]
colors = [COLOR_NAVY, COLOR_LIGHT_BLUE, COLOR_GREEN, COLOR_ACCENT, COLOR_PURPLE]
fig, ax = plt.subplots(figsize=(10, 6))
bars = ax.barh(
labels, values, color=colors, edgecolor="white", linewidth=0.5, height=0.55
)
for bar, val in zip(bars, values):
ax.text(
bar.get_width() + 0.5,
bar.get_y() + bar.get_height() / 2,
f"{val:.1f}%",
ha="left",
va="center",
fontsize=10,
fontweight="bold",
color=COLOR_DARK,
)
ax.set_xlabel("百分比 (%)", fontsize=11)
company = data.get("company_name", "")
year_str = data.get("year", "")
ax.set_title(f"{company} {year_str}年 盈利能力指标", fontsize=14, fontweight="bold")
ax.grid(axis="x", linestyle="--", alpha=0.4, color="#cccccc")
ax.set_axisbelow(True)
ax.spines["top"].set_visible(False)
ax.spines["right"].set_visible(False)
# Give some right margin for the labels
max_val = max(values) if values else 100
ax.set_xlim(0, max_val * 1.2 if max_val > 0 else 100)
plt.tight_layout()
path = os.path.join(output_dir, "profitability.png")
fig.savefig(path, dpi=CHART_DPI, facecolor="white", bbox_inches="tight")
plt.close(fig)
return path
# ---------------------------------------------------------------------------
# Chart 3 资产结构分布 (asset_structure.png)
# ---------------------------------------------------------------------------
def chart_asset_structure(data, output_dir):
"""Donut chart showing liabilities vs equity composition."""
total_liabilities = safe_float(data.get("total_liabilities"))
equity = safe_float(data.get("equity"))
total = total_liabilities + equity
if total == 0:
# Avoid division by zero — create a placeholder chart
total = 1
liab_yi = format_yi(total_liabilities)
equity_yi = format_yi(equity)
labels = [
f"负债\n{liab_yi:.1f}亿元",
f"所有者权益\n{equity_yi:.1f}亿元",
]
sizes = [total_liabilities, equity]
colors = [COLOR_ACCENT, COLOR_NAVY]
explode = (0.03, 0.03)
fig, ax = plt.subplots(figsize=(8, 8))
wedges, texts, autotexts = ax.pie(
sizes,
labels=labels,
autopct="%1.1f%%",
startangle=90,
colors=colors,
explode=explode,
pctdistance=0.75,
labeldistance=1.15,
textprops={"fontsize": 12},
wedgeprops={"linewidth": 2, "edgecolor": "white"},
)
for autotext in autotexts:
autotext.set_fontsize(13)
autotext.set_fontweight("bold")
autotext.set_color("white")
# Draw a white circle in the centre for a donut effect
centre_circle = plt.Circle((0, 0), 0.55, fc="white")
ax.add_artist(centre_circle)
# Centre text
total_yi = format_yi(total)
ax.text(
0,
0.05,
"总资产",
ha="center",
va="center",
fontsize=13,
color="#666666",
)
ax.text(
0,
-0.1,
f"{total_yi:.1f}亿元",
ha="center",
va="center",
fontsize=16,
fontweight="bold",
color=COLOR_DARK,
)
company = data.get("company_name", "")
year_str = data.get("year", "")
ax.set_title(
f"{company} {year_str}年 资产结构分布",
fontsize=14,
fontweight="bold",
pad=20,
)
ax.axis("equal")
plt.tight_layout()
path = os.path.join(output_dir, "asset_structure.png")
fig.savefig(path, dpi=CHART_DPI, facecolor="white", bbox_inches="tight")
plt.close(fig)
return path
# ---------------------------------------------------------------------------
# Main orchestrator
# ---------------------------------------------------------------------------
def generate_charts(data, output_dir):
"""Generate all 3 charts and return a manifest dict."""
setup_chinese_font()
paths = {
"financial_overview": chart_financial_overview(data, output_dir),
"profitability": chart_profitability(data, output_dir),
"asset_structure": chart_asset_structure(data, output_dir),
}
return paths
# ---------------------------------------------------------------------------
# CLI entry point
# ---------------------------------------------------------------------------
if __name__ == "__main__":
if len(sys.argv) < 2:
print(
json.dumps(
{"error": "Please provide JSON data as an argument."},
ensure_ascii=False,
)
)
sys.exit(1)
try:
arg = sys.argv[1]
parsed = json.loads(arg)
# Unwrap single-key wrappers like {"financial_data": {...}} or {"data": {...}}
if isinstance(parsed, dict):
if len(parsed) == 1:
only_value = next(iter(parsed.values()))
if isinstance(only_value, dict):
parsed = only_value
# Use OUTPUT_DIR env var if set (injected by manage.py),
# otherwise fall back to the script's own directory.
out_dir = os.environ.get("OUTPUT_DIR") or os.path.dirname(
os.path.abspath(__file__)
)
chart_paths = generate_charts(parsed, out_dir)
result = {
"charts": chart_paths,
"output_dir": out_dir,
}
print(json.dumps(result, indent=2, ensure_ascii=False))
except Exception as e:
print(json.dumps({"error": str(e)}, ensure_ascii=False))
sys.exit(1)
@@ -0,0 +1,243 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<title>{{COMPANY_NAME}} {{YEAR}} 年度财报分析报告</title>
<style>
body {
font-family: "PingFang SC", "Microsoft YaHei", system-ui, sans-serif;
max-width: 1200px;
margin: 0 auto;
padding: 32px 24px;
color: #1a1a2e;
background: #f8f9fc;
line-height: 1.7;
}
h1 {
text-align: center;
font-size: 26px;
color: #16213e;
border-bottom: 3px solid #0f3460;
padding-bottom: 12px;
margin-bottom: 8px;
}
.subtitle {
text-align: center;
color: #666;
font-size: 14px;
margin-bottom: 32px;
}
h2 {
font-size: 20px;
color: #0f3460;
margin-top: 36px;
margin-bottom: 16px;
padding-left: 12px;
border-left: 4px solid #e94560;
}
h3 {
font-size: 17px;
color: #16213e;
margin-top: 24px;
margin-bottom: 12px;
}
table {
width: 100%;
border-collapse: collapse;
margin: 16px 0 24px 0;
background: #fff;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 1px 4px rgba(0,0,0,0.06);
}
thead th {
background: #0f3460;
color: #fff;
font-weight: 600;
padding: 12px 16px;
text-align: left;
font-size: 14px;
}
tbody td {
padding: 10px 16px;
border-bottom: 1px solid #eef0f5;
font-size: 14px;
}
tbody tr:hover {
background: #f0f4ff;
}
.section-card {
background: #fff;
border-radius: 10px;
padding: 20px 24px;
margin: 16px 0;
box-shadow: 0 1px 4px rgba(0,0,0,0.05);
}
.highlight-positive { color: #27ae60; font-weight: 600; }
.highlight-negative { color: #e74c3c; font-weight: 600; }
.conclusion-box {
background: linear-gradient(135deg, #f8f9fc 0%, #eef1f8 100%);
border-radius: 10px;
padding: 24px;
margin-top: 20px;
}
.conclusion-box strong { color: #0f3460; }
.tag-advantage {
display: inline-block;
background: #d4edda;
color: #155724;
padding: 2px 10px;
border-radius: 4px;
font-size: 13px;
margin-right: 6px;
}
.tag-risk {
display: inline-block;
background: #f8d7da;
color: #721c24;
padding: 2px 10px;
border-radius: 4px;
font-size: 13px;
margin-right: 6px;
}
p { margin: 8px 0; font-size: 15px; }
ul { padding-left: 20px; }
li { margin: 4px 0; font-size: 14px; }
.chart-container {
text-align: center;
margin: 20px 0;
}
.chart-container img {
max-width: 100%;
height: auto;
border-radius: 8px;
box-shadow: 0 1px 4px rgba(0,0,0,0.08);
}
.chart-container .chart-caption {
font-size: 13px;
color: #888;
margin-top: 8px;
}
</style>
</head>
<body>
<!-- ============================================================
使用说明(Agent 必读):
1. 将所有 {{占位符}} 替换为真实数据
2. 严禁删除任何章节(1-4),严禁修改表格列数
3. 核心财务摘要表格必须保留 4 列:指标 / 本期数值 / 同比增长 / 行业平均(参考)
4. 如果某项数据缺失,填写 "N/A" 而非删除该行或该列
5. 可以在各 section-card 中增加子段落、图表等补充内容,但不得删减已有结构
6. 同比增长为正时加 class="highlight-positive",为负时加 class="highlight-negative"
============================================================ -->
<h1>{{COMPANY_NAME}} {{YEAR}} 年度财报分析报告</h1>
<p class="subtitle">报告生成时间:{{DATE}} | 数据来源:公司年度报告</p>
<!-- ==================== 第1章 核心财务摘要 ==================== -->
<h2>1. 核心财务摘要</h2>
<table>
<thead>
<tr>
<th>指标</th>
<th>本期数值</th>
<th>同比增长</th>
<th>行业平均 (参考)</th>
</tr>
</thead>
<tbody>
<tr>
<td>营业收入</td>
<td>{{REVENUE}}</td>
<td class="{{REVENUE_GROWTH_CLASS}}">{{REVENUE_GROWTH}}</td>
<td>{{REVENUE_INDUSTRY_AVG}}</td>
</tr>
<tr>
<td>归母净利润</td>
<td>{{NET_PROFIT}}</td>
<td class="{{NET_PROFIT_GROWTH_CLASS}}">{{NET_PROFIT_GROWTH}}</td>
<td>{{NET_PROFIT_INDUSTRY_AVG}}</td>
</tr>
<tr>
<td>扣非净利润</td>
<td>{{NON_RECURRING_NET_PROFIT}}</td>
<td class="{{NON_RECURRING_GROWTH_CLASS}}">{{NON_RECURRING_GROWTH}}</td>
<td>{{NON_RECURRING_INDUSTRY_AVG}}</td>
</tr>
<tr>
<td>毛利率</td>
<td>{{GROSS_MARGIN}}</td>
<td class="{{GROSS_MARGIN_CHANGE_CLASS}}">{{GROSS_MARGIN_CHANGE}}</td>
<td>{{GROSS_MARGIN_INDUSTRY_AVG}}</td>
</tr>
<tr>
<td>ROE</td>
<td>{{ROE}}</td>
<td class="{{ROE_CHANGE_CLASS}}">{{ROE_CHANGE}}</td>
<td>{{ROE_INDUSTRY_AVG}}</td>
</tr>
</tbody>
</table>
<div class="chart-container">
<img src="{{CHART_FINANCIAL_OVERVIEW}}" alt="核心财务指标对比">
<p class="chart-caption">图1 核心财务指标对比(单位:亿元)</p>
</div>
<!-- ==================== 第2章 财务状况深度分析 ==================== -->
<h2>2. 财务状况深度分析</h2>
<h3>2.1 盈利能力分析</h3>
<div class="section-card">
<!-- 在此处详细描述盈利能力的变动原因,结合毛利率、净利率和费用率进行分析 -->
<p>{{PROFITABILITY_ANALYSIS}}</p>
</div>
<h3>2.2 偿债与风险分析</h3>
<div class="section-card">
<!-- 分析资产负债率、流动比率、速动比率等,评估财务安全性 -->
<p>{{SOLVENCY_ANALYSIS}}</p>
<div class="chart-container">
<img src="{{CHART_PROFITABILITY}}" alt="盈利能力指标">
<p class="chart-caption">图2 盈利能力指标分析(%</p>
</div>
<h3>2.3 营运效率分析</h3>
<div class="section-card">
<!-- 分析应收账款周转率、存货周转率、总资产周转率等,评估公司管理效率 -->
<p>{{EFFICIENCY_ANALYSIS}}</p>
</div>
<!-- ==================== 第3章 现金流与利润质量 ==================== -->
<h2>3. 现金流与利润质量</h2>
<div class="section-card">
<div class="chart-container">
<img src="{{CHART_ASSET_STRUCTURE}}" alt="资产结构分布">
<p class="chart-caption">图3 资产结构分布</p>
</div>
<!-- 对比净利润与经营现金流,评估盈利的真实性;分析投资和筹资现金流结构 -->
<p>{{CASHFLOW_ANALYSIS}}</p>
</div>
<!-- ==================== 第4章 结论与建议 ==================== -->
<h2>4. 结论与建议</h2>
<div class="conclusion-box">
<p><strong>优势:</strong></p>
<ul>
<!-- 列出 2-4 条核心优势,每条用 <li> -->
{{ADVANTAGES_LIST}}
</ul>
<p><strong>风险:</strong></p>
<ul>
<!-- 列出 2-4 条主要风险,每条用 <li> -->
{{RISKS_LIST}}
</ul>
<p><strong>综合评价:</strong></p>
<p>{{OVERALL_ASSESSMENT}}</p>
</div>
</body>
</html>
@@ -0,0 +1,30 @@
# [公司名称] [年份] 年度财报分析报告
## 1. 核心财务摘要
| 指标 | 本期数值 | 同比增长 | 行业平均 (参考) |
| :--- | :--- | :--- | :--- |
| 营业收入 | | | |
| 归母净利润 | | | |
| 扣非净利润 | | | |
| 毛利率 | | | |
| ROE | | | |
## 2. 财务状况深度分析
### 2.1 盈利能力分析
[在此处详细描述盈利能力的变动原因,结合毛利率和费用率进行分析]
### 2.2 偿债与风险分析
[分析资产负债率、流动比率等,评估财务安全性]
### 2.3 营运效率分析
[分析周转率指标,评估公司管理效率]
## 3. 现金流与利润质量
[对比净利润与经营现金流,评估盈利的真实性]
## 4. 结论与建议
- **优势**
- **风险**
- **综合评价**
+202
View File
@@ -0,0 +1,202 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
+357
View File
@@ -0,0 +1,357 @@
---
name: skill-creator
description: Guide for creating effective skills. This skill should be used when users want to create a new skill (or update an existing skill) that extends Claude's capabilities with specialized knowledge, workflows, or tool integrations.
license: Complete terms in LICENSE.txt
---
# Skill Creator
This skill provides guidance for creating effective skills.
## About Skills
Skills are modular, self-contained packages that extend Claude's capabilities by providing
specialized knowledge, workflows, and tools. Think of them as "onboarding guides" for specific
domains or tasks—they transform Claude from a general-purpose agent into a specialized agent
equipped with procedural knowledge that no model can fully possess.
### What Skills Provide
1. Specialized workflows - Multi-step procedures for specific domains
2. Tool integrations - Instructions for working with specific file formats or APIs
3. Domain expertise - Company-specific knowledge, schemas, business logic
4. Bundled resources - Scripts, references, and assets for complex and repetitive tasks
## Core Principles
### Concise is Key
The context window is a public good. Skills share the context window with everything else Claude needs: system prompt, conversation history, other Skills' metadata, and the actual user request.
**Default assumption: Claude is already very smart.** Only add context Claude doesn't already have. Challenge each piece of information: "Does Claude really need this explanation?" and "Does this paragraph justify its token cost?"
Prefer concise examples over verbose explanations.
### Set Appropriate Degrees of Freedom
Match the level of specificity to the task's fragility and variability:
**High freedom (text-based instructions)**: Use when multiple approaches are valid, decisions depend on context, or heuristics guide the approach.
**Medium freedom (pseudocode or scripts with parameters)**: Use when a preferred pattern exists, some variation is acceptable, or configuration affects behavior.
**Low freedom (specific scripts, few parameters)**: Use when operations are fragile and error-prone, consistency is critical, or a specific sequence must be followed.
Think of Claude as exploring a path: a narrow bridge with cliffs needs specific guardrails (low freedom), while an open field allows many routes (high freedom).
### Anatomy of a Skill
Every skill consists of a required SKILL.md file and optional bundled resources:
```
skill-name/
├── SKILL.md (required)
│ ├── YAML frontmatter metadata (required)
│ │ ├── name: (required)
│ │ └── description: (required)
│ └── Markdown instructions (required)
└── Bundled Resources (optional)
├── scripts/ - Executable code (Python/Bash/etc.)
├── references/ - Documentation intended to be loaded into context as needed
└── assets/ - Files used in output (templates, icons, fonts, etc.)
```
#### SKILL.md (required)
Every SKILL.md consists of:
- **Frontmatter** (YAML): Contains `name` and `description` fields. These are the only fields that Claude reads to determine when the skill gets used, thus it is very important to be clear and comprehensive in describing what the skill is, and when it should be used.
- **Body** (Markdown): Instructions and guidance for using the skill. Only loaded AFTER the skill triggers (if at all).
#### Bundled Resources (optional)
##### Scripts (`scripts/`)
Executable code (Python/Bash/etc.) for tasks that require deterministic reliability or are repeatedly rewritten.
- **When to include**: When the same code is being rewritten repeatedly or deterministic reliability is needed
- **Example**: `scripts/rotate_pdf.py` for PDF rotation tasks
- **Benefits**: Token efficient, deterministic, may be executed without loading into context
- **Note**: Scripts may still need to be read by Claude for patching or environment-specific adjustments
##### References (`references/`)
Documentation and reference material intended to be loaded as needed into context to inform Claude's process and thinking.
- **When to include**: For documentation that Claude should reference while working
- **Examples**: `references/finance.md` for financial schemas, `references/mnda.md` for company NDA template, `references/policies.md` for company policies, `references/api_docs.md` for API specifications
- **Use cases**: Database schemas, API documentation, domain knowledge, company policies, detailed workflow guides
- **Benefits**: Keeps SKILL.md lean, loaded only when Claude determines it's needed
- **Best practice**: If files are large (>10k words), include grep search patterns in SKILL.md
- **Avoid duplication**: Information should live in either SKILL.md or references files, not both. Prefer references files for detailed information unless it's truly core to the skill—this keeps SKILL.md lean while making information discoverable without hogging the context window. Keep only essential procedural instructions and workflow guidance in SKILL.md; move detailed reference material, schemas, and examples to references files.
##### Assets (`assets/`)
Files not intended to be loaded into context, but rather used within the output Claude produces.
- **When to include**: When the skill needs files that will be used in the final output
- **Examples**: `assets/logo.png` for brand assets, `assets/slides.pptx` for PowerPoint templates, `assets/frontend-template/` for HTML/React boilerplate, `assets/font.ttf` for typography
- **Use cases**: Templates, images, icons, boilerplate code, fonts, sample documents that get copied or modified
- **Benefits**: Separates output resources from documentation, enables Claude to use files without loading them into context
#### What to Not Include in a Skill
A skill should only contain essential files that directly support its functionality. Do NOT create extraneous documentation or auxiliary files, including:
- README.md
- INSTALLATION_GUIDE.md
- QUICK_REFERENCE.md
- CHANGELOG.md
- etc.
The skill should only contain the information needed for an AI agent to do the job at hand. It should not contain auxilary context about the process that went into creating it, setup and testing procedures, user-facing documentation, etc. Creating additional documentation files just adds clutter and confusion.
### Progressive Disclosure Design Principle
Skills use a three-level loading system to manage context efficiently:
1. **Metadata (name + description)** - Always in context (~100 words)
2. **SKILL.md body** - When skill triggers (<5k words)
3. **Bundled resources** - As needed by Claude (Unlimited because scripts can be executed without reading into context window)
#### Progressive Disclosure Patterns
Keep SKILL.md body to the essentials and under 500 lines to minimize context bloat. Split content into separate files when approaching this limit. When splitting out content into other files, it is very important to reference them from SKILL.md and describe clearly when to read them, to ensure the reader of the skill knows they exist and when to use them.
**Key principle:** When a skill supports multiple variations, frameworks, or options, keep only the core workflow and selection guidance in SKILL.md. Move variant-specific details (patterns, examples, configuration) into separate reference files.
**Pattern 1: High-level guide with references**
```markdown
# PDF Processing
## Quick start
Extract text with pdfplumber:
[code example]
## Advanced features
- **Form filling**: See [FORMS.md](FORMS.md) for complete guide
- **API reference**: See [REFERENCE.md](REFERENCE.md) for all methods
- **Examples**: See [EXAMPLES.md](EXAMPLES.md) for common patterns
```
Claude loads FORMS.md, REFERENCE.md, or EXAMPLES.md only when needed.
**Pattern 2: Domain-specific organization**
For Skills with multiple domains, organize content by domain to avoid loading irrelevant context:
```
bigquery-skill/
├── SKILL.md (overview and navigation)
└── reference/
├── finance.md (revenue, billing metrics)
├── sales.md (opportunities, pipeline)
├── product.md (API usage, features)
└── marketing.md (campaigns, attribution)
```
When a user asks about sales metrics, Claude only reads sales.md.
Similarly, for skills supporting multiple frameworks or variants, organize by variant:
```
cloud-deploy/
├── SKILL.md (workflow + provider selection)
└── references/
├── aws.md (AWS deployment patterns)
├── gcp.md (GCP deployment patterns)
└── azure.md (Azure deployment patterns)
```
When the user chooses AWS, Claude only reads aws.md.
**Pattern 3: Conditional details**
Show basic content, link to advanced content:
```markdown
# DOCX Processing
## Creating documents
Use docx-js for new documents. See [DOCX-JS.md](DOCX-JS.md).
## Editing documents
For simple edits, modify the XML directly.
**For tracked changes**: See [REDLINING.md](REDLINING.md)
**For OOXML details**: See [OOXML.md](OOXML.md)
```
Claude reads REDLINING.md or OOXML.md only when the user needs those features.
**Important guidelines:**
- **Avoid deeply nested references** - Keep references one level deep from SKILL.md. All reference files should link directly from SKILL.md.
- **Structure longer reference files** - For files longer than 100 lines, include a table of contents at the top so Claude can see the full scope when previewing.
## Skill Creation Process
Skill creation involves these steps:
1. Understand the skill with concrete examples
2. Plan reusable skill contents (scripts, references, assets)
3. Initialize the skill (run init_skill.py)
4. Edit the skill (implement resources and write SKILL.md)
5. Package the skill (run package_skill.py)
6. Iterate based on real usage
Follow these steps in order, skipping only if there is a clear reason why they are not applicable.
### Step 1: Understanding the Skill with Concrete Examples
**Default: Skip this step and proceed to Step 2** when the user's request already specifies a clear skill topic (e.g., "创建一个Excel分析技能", "Create a PDF processing skill", "生成一个数据可视化技能"). In such cases, you already have enough context to plan the skill — infer reasonable usage patterns yourself and move forward immediately.
**Only pause to ask questions** when the request is genuinely ambiguous and you cannot proceed without clarification — for example, "帮我创建一个技能" with no topic specified, or a domain so specialized that guessing usage patterns would likely be wrong.
When clarification IS needed, ask at most 1-2 focused questions in a single message, then proceed regardless of the detail level of the user's response. Do NOT enter a multi-turn Q&A loop. Examples of focused questions:
- "这个技能主要处理什么类型的数据/文件?"
- "能给一个典型的使用场景吗?"
Conclude this step as quickly as possible. The goal is to start building, not to achieve perfect understanding upfront — iteration (Step 6) exists for refinement.
### Step 2: Planning the Reusable Skill Contents
To turn concrete examples into an effective skill, analyze each example by:
1. Considering how to execute on the example from scratch
2. Identifying what scripts, references, and assets would be helpful when executing these workflows repeatedly
Example: When building a `pdf-editor` skill to handle queries like "Help me rotate this PDF," the analysis shows:
1. Rotating a PDF requires re-writing the same code each time
2. A `scripts/rotate_pdf.py` script would be helpful to store in the skill
Example: When designing a `frontend-webapp-builder` skill for queries like "Build me a todo app" or "Build me a dashboard to track my steps," the analysis shows:
1. Writing a frontend webapp requires the same boilerplate HTML/React each time
2. An `assets/hello-world/` template containing the boilerplate HTML/React project files would be helpful to store in the skill
Example: When building a `big-query` skill to handle queries like "How many users have logged in today?" the analysis shows:
1. Querying BigQuery requires re-discovering the table schemas and relationships each time
2. A `references/schema.md` file documenting the table schemas would be helpful to store in the skill
To establish the skill's contents, analyze each concrete example to create a list of the reusable resources to include: scripts, references, and assets.
### Step 3: Initializing the Skill
At this point, it is time to actually create the skill.
Skip this step only if the skill being developed already exists, and iteration or packaging is needed. In this case, continue to the next step.
When creating a new skill from scratch, always run the `init_skill.py` script using `shell_interpreter`. The script conveniently generates a new template skill directory that automatically includes everything a skill requires, making the skill creation process much more efficient and reliable.
**Important**: All script executions in the skill-creator workflow must use the `shell_interpreter` tool. First, use `get_skill_resource` to find the skill-creator's script path, then execute via `shell_interpreter`.
Usage (via shell_interpreter):
```
Action: shell_interpreter
Action Input: {"code": "python skills/skill-creator/scripts/init_skill.py <skill-name> --path skills/"}
```
The script:
- Creates the skill directory at the specified path
- Generates a SKILL.md template with proper frontmatter and TODO placeholders
- Creates example resource directories: `scripts/`, `references/`, and `assets/`
- Adds example files in each directory that can be customized or deleted
After initialization, customize or remove the generated SKILL.md and example files as needed.
### Step 4: Edit the Skill
When editing the (newly-generated or existing) skill, remember that the skill is being created for another instance of Claude to use. Include information that would be beneficial and non-obvious to Claude. Consider what procedural knowledge, domain-specific details, or reusable assets would help another Claude instance execute these tasks more effectively.
#### Learn Proven Design Patterns
Consult these helpful guides based on your skill's needs:
- **Multi-step processes**: See references/workflows.md for sequential workflows and conditional logic
- **Specific output formats or quality standards**: See references/output-patterns.md for template and example patterns
These files contain established best practices for effective skill design.
#### Start with Reusable Skill Contents
To begin implementation, start with the reusable resources identified above: `scripts/`, `references/`, and `assets/` files. Note that this step may require user input. For example, when implementing a `brand-guidelines` skill, the user may need to provide brand assets or templates to store in `assets/`, or documentation to store in `references/`.
Added scripts must be tested by actually running them via `shell_interpreter` to ensure there are no bugs and that the output matches what is expected. For example: `shell_interpreter({"code": "python skills/<new-skill>/scripts/my_script.py --arg value"})`. If there are many similar scripts, only a representative sample needs to be tested to ensure confidence that they all work while balancing time to completion.
Any example files and directories not needed for the skill should be deleted. The initialization script creates example files in `scripts/`, `references/`, and `assets/` to demonstrate structure, but most skills won't need all of them.
#### Update SKILL.md
**Writing Guidelines:** Always use imperative/infinitive form.
##### Frontmatter
Write the YAML frontmatter with `name` and `description`:
- `name`: The skill name
- `description`: This is the primary triggering mechanism for your skill, and helps Claude understand when to use the skill.
- Include both what the Skill does and specific triggers/contexts for when to use it.
- Include all "when to use" information here - Not in the body. The body is only loaded after triggering, so "When to Use This Skill" sections in the body are not helpful to Claude.
- Example description for a `docx` skill: "Comprehensive document creation, editing, and analysis with support for tracked changes, comments, formatting preservation, and text extraction. Use when Claude needs to work with professional documents (.docx files) for: (1) Creating new documents, (2) Modifying or editing content, (3) Working with tracked changes, (4) Adding comments, or any other document tasks"
Do not include any other fields in YAML frontmatter.
##### Body
Write instructions for using the skill and its bundled resources.
### Step 5: Packaging a Skill
Once development of the skill is complete, it must be packaged into a distributable .skill file that gets shared with the user. Use `shell_interpreter` to run the packaging script:
```
Action: shell_interpreter
Action Input: {"code": "python skills/skill-creator/scripts/package_skill.py <path/to/skill-folder>"}
```
Optional output directory specification:
```
Action: shell_interpreter
Action Input: {"code": "python skills/skill-creator/scripts/package_skill.py <path/to/skill-folder> ./dist"}
```
The packaging script will:
1. **Validate** the skill automatically, checking:
- YAML frontmatter format and required fields
- Skill naming conventions and directory structure
- Description completeness and quality
- File organization and resource references
2. **Package** the skill if validation passes, creating a .skill file named after the skill (e.g., `my-skill.skill`) that includes all files and maintains the proper directory structure for distribution. The .skill file is a zip file with a .skill extension.
If validation fails, the script will report the errors and exit without creating a package. Fix any validation errors and run the packaging command again.
### Step 6: Iterate
After testing the skill, users may request improvements. Often this happens right after using the skill, with fresh context of how the skill performed.
**Iteration workflow:**
1. Use the skill on real tasks
2. Notice struggles or inefficiencies
3. Identify how SKILL.md or bundled resources should be updated
4. Implement changes and test again
@@ -0,0 +1,28 @@
# Workflow Patterns
## Sequential Workflows
For complex tasks, break operations into clear, sequential steps. It is often helpful to give Claude an overview of the process towards the beginning of SKILL.md:
```markdown
Filling a PDF form involves these steps:
1. Analyze the form (run analyze_form.py)
2. Create field mapping (edit fields.json)
3. Validate mapping (run validate_fields.py)
4. Fill the form (run fill_form.py)
5. Verify output (run verify_output.py)
```
## Conditional Workflows
For tasks with branching logic, guide Claude through decision points:
```markdown
1. Determine the modification type:
**Creating new content?** → Follow "Creation workflow" below
**Editing existing content?** → Follow "Editing workflow" below
2. Creation workflow: [steps]
3. Editing workflow: [steps]
```
+303
View File
@@ -0,0 +1,303 @@
#!/usr/bin/env python3
"""
Skill Initializer - Creates a new skill from template
Usage:
init_skill.py <skill-name> --path <path>
Examples:
init_skill.py my-new-skill --path skills/public
init_skill.py my-api-helper --path skills/private
init_skill.py custom-skill --path /custom/location
"""
import sys
from pathlib import Path
SKILL_TEMPLATE = """---
name: {skill_name}
description: [TODO: Complete and informative explanation of what the skill does and when to use it. Include WHEN to use this skill - specific scenarios, file types, or tasks that trigger it.]
---
# {skill_title}
## Overview
[TODO: 1-2 sentences explaining what this skill enables]
## Structuring This Skill
[TODO: Choose the structure that best fits this skill's purpose. Common patterns:
**1. Workflow-Based** (best for sequential processes)
- Works well when there are clear step-by-step procedures
- Example: DOCX skill with "Workflow Decision Tree""Reading""Creating""Editing"
- Structure: ## Overview → ## Workflow Decision Tree → ## Step 1 → ## Step 2...
**2. Task-Based** (best for tool collections)
- Works well when the skill offers different operations/capabilities
- Example: PDF skill with "Quick Start""Merge PDFs""Split PDFs""Extract Text"
- Structure: ## Overview → ## Quick Start → ## Task Category 1 → ## Task Category 2...
**3. Reference/Guidelines** (best for standards or specifications)
- Works well for brand guidelines, coding standards, or requirements
- Example: Brand styling with "Brand Guidelines""Colors""Typography""Features"
- Structure: ## Overview → ## Guidelines → ## Specifications → ## Usage...
**4. Capabilities-Based** (best for integrated systems)
- Works well when the skill provides multiple interrelated features
- Example: Product Management with "Core Capabilities" → numbered capability list
- Structure: ## Overview → ## Core Capabilities → ### 1. Feature → ### 2. Feature...
Patterns can be mixed and matched as needed. Most skills combine patterns (e.g., start with task-based, add workflow for complex operations).
Delete this entire "Structuring This Skill" section when done - it's just guidance.]
## [TODO: Replace with the first main section based on chosen structure]
[TODO: Add content here. See examples in existing skills:
- Code samples for technical skills
- Decision trees for complex workflows
- Concrete examples with realistic user requests
- References to scripts/templates/references as needed]
## Resources
This skill includes example resource directories that demonstrate how to organize different types of bundled resources:
### scripts/
Executable code (Python/Bash/etc.) that can be run directly to perform specific operations.
**Examples from other skills:**
- PDF skill: `fill_fillable_fields.py`, `extract_form_field_info.py` - utilities for PDF manipulation
- DOCX skill: `document.py`, `utilities.py` - Python modules for document processing
**Appropriate for:** Python scripts, shell scripts, or any executable code that performs automation, data processing, or specific operations.
**Note:** Scripts may be executed without loading into context, but can still be read by Claude for patching or environment adjustments.
### references/
Documentation and reference material intended to be loaded into context to inform Claude's process and thinking.
**Examples from other skills:**
- Product management: `communication.md`, `context_building.md` - detailed workflow guides
- BigQuery: API reference documentation and query examples
- Finance: Schema documentation, company policies
**Appropriate for:** In-depth documentation, API references, database schemas, comprehensive guides, or any detailed information that Claude should reference while working.
### assets/
Files not intended to be loaded into context, but rather used within the output Claude produces.
**Examples from other skills:**
- Brand styling: PowerPoint template files (.pptx), logo files
- Frontend builder: HTML/React boilerplate project directories
- Typography: Font files (.ttf, .woff2)
**Appropriate for:** Templates, boilerplate code, document templates, images, icons, fonts, or any files meant to be copied or used in the final output.
---
**Any unneeded directories can be deleted.** Not every skill requires all three types of resources.
"""
EXAMPLE_SCRIPT = '''#!/usr/bin/env python3
"""
Example helper script for {skill_name}
This is a placeholder script that can be executed directly.
Replace with actual implementation or delete if not needed.
Example real scripts from other skills:
- pdf/scripts/fill_fillable_fields.py - Fills PDF form fields
- pdf/scripts/convert_pdf_to_images.py - Converts PDF pages to images
"""
def main():
print("This is an example script for {skill_name}")
# TODO: Add actual script logic here
# This could be data processing, file conversion, API calls, etc.
if __name__ == "__main__":
main()
'''
EXAMPLE_REFERENCE = """# Reference Documentation for {skill_title}
This is a placeholder for detailed reference documentation.
Replace with actual reference content or delete if not needed.
Example real reference docs from other skills:
- product-management/references/communication.md - Comprehensive guide for status updates
- product-management/references/context_building.md - Deep-dive on gathering context
- bigquery/references/ - API references and query examples
## When Reference Docs Are Useful
Reference docs are ideal for:
- Comprehensive API documentation
- Detailed workflow guides
- Complex multi-step processes
- Information too lengthy for main SKILL.md
- Content that's only needed for specific use cases
## Structure Suggestions
### API Reference Example
- Overview
- Authentication
- Endpoints with examples
- Error codes
- Rate limits
### Workflow Guide Example
- Prerequisites
- Step-by-step instructions
- Common patterns
- Troubleshooting
- Best practices
"""
EXAMPLE_ASSET = """# Example Asset File
This placeholder represents where asset files would be stored.
Replace with actual asset files (templates, images, fonts, etc.) or delete if not needed.
Asset files are NOT intended to be loaded into context, but rather used within
the output Claude produces.
Example asset files from other skills:
- Brand guidelines: logo.png, slides_template.pptx
- Frontend builder: hello-world/ directory with HTML/React boilerplate
- Typography: custom-font.ttf, font-family.woff2
- Data: sample_data.csv, test_dataset.json
## Common Asset Types
- Templates: .pptx, .docx, boilerplate directories
- Images: .png, .jpg, .svg, .gif
- Fonts: .ttf, .otf, .woff, .woff2
- Boilerplate code: Project directories, starter files
- Icons: .ico, .svg
- Data files: .csv, .json, .xml, .yaml
Note: This is a text placeholder. Actual assets can be any file type.
"""
def title_case_skill_name(skill_name):
"""Convert hyphenated skill name to Title Case for display."""
return ' '.join(word.capitalize() for word in skill_name.split('-'))
def init_skill(skill_name, path):
"""
Initialize a new skill directory with template SKILL.md.
Args:
skill_name: Name of the skill
path: Path where the skill directory should be created
Returns:
Path to created skill directory, or None if error
"""
# Determine skill directory path
skill_dir = Path(path).resolve() / skill_name
# Check if directory already exists
if skill_dir.exists():
print(f"❌ Error: Skill directory already exists: {skill_dir}")
return None
# Create skill directory
try:
skill_dir.mkdir(parents=True, exist_ok=False)
print(f"✅ Created skill directory: {skill_dir}")
except Exception as e:
print(f"❌ Error creating directory: {e}")
return None
# Create SKILL.md from template
skill_title = title_case_skill_name(skill_name)
skill_content = SKILL_TEMPLATE.format(
skill_name=skill_name,
skill_title=skill_title
)
skill_md_path = skill_dir / 'SKILL.md'
try:
skill_md_path.write_text(skill_content)
print("✅ Created SKILL.md")
except Exception as e:
print(f"❌ Error creating SKILL.md: {e}")
return None
# Create resource directories with example files
try:
# Create scripts/ directory with example script
scripts_dir = skill_dir / 'scripts'
scripts_dir.mkdir(exist_ok=True)
example_script = scripts_dir / 'example.py'
example_script.write_text(EXAMPLE_SCRIPT.format(skill_name=skill_name))
example_script.chmod(0o755)
print("✅ Created scripts/example.py")
# Create references/ directory with example reference doc
references_dir = skill_dir / 'references'
references_dir.mkdir(exist_ok=True)
example_reference = references_dir / 'api_reference.md'
example_reference.write_text(EXAMPLE_REFERENCE.format(skill_title=skill_title))
print("✅ Created references/api_reference.md")
# Create assets/ directory with example asset placeholder
assets_dir = skill_dir / 'assets'
assets_dir.mkdir(exist_ok=True)
example_asset = assets_dir / 'example_asset.txt'
example_asset.write_text(EXAMPLE_ASSET)
print("✅ Created assets/example_asset.txt")
except Exception as e:
print(f"❌ Error creating resource directories: {e}")
return None
# Print next steps
print(f"\n✅ Skill '{skill_name}' initialized successfully at {skill_dir}")
print("\nNext steps:")
print("1. Edit SKILL.md to complete the TODO items and update the description")
print("2. Customize or delete the example files in scripts/, references/, and assets/")
print("3. Run the validator when ready to check the skill structure")
return skill_dir
def main():
if len(sys.argv) < 4 or sys.argv[2] != '--path':
print("Usage: init_skill.py <skill-name> --path <path>")
print("\nSkill name requirements:")
print(" - Hyphen-case identifier (e.g., 'data-analyzer')")
print(" - Lowercase letters, digits, and hyphens only")
print(" - Max 40 characters")
print(" - Must match directory name exactly")
print("\nExamples:")
print(" init_skill.py my-new-skill --path skills/public")
print(" init_skill.py my-api-helper --path skills/private")
print(" init_skill.py custom-skill --path /custom/location")
sys.exit(1)
skill_name = sys.argv[1]
path = sys.argv[3]
print(f"🚀 Initializing skill: {skill_name}")
print(f" Location: {path}")
print()
result = init_skill(skill_name, path)
if result:
sys.exit(0)
else:
sys.exit(1)
if __name__ == "__main__":
main()
+110
View File
@@ -0,0 +1,110 @@
#!/usr/bin/env python3
"""
Skill Packager - Creates a distributable .skill file of a skill folder
Usage:
python utils/package_skill.py <path/to/skill-folder> [output-directory]
Example:
python utils/package_skill.py skills/public/my-skill
python utils/package_skill.py skills/public/my-skill ./dist
"""
import sys
import zipfile
from pathlib import Path
from quick_validate import validate_skill
def package_skill(skill_path, output_dir=None):
"""
Package a skill folder into a .skill file.
Args:
skill_path: Path to the skill folder
output_dir: Optional output directory for the .skill file (defaults to current directory)
Returns:
Path to the created .skill file, or None if error
"""
skill_path = Path(skill_path).resolve()
# Validate skill folder exists
if not skill_path.exists():
print(f"❌ Error: Skill folder not found: {skill_path}")
return None
if not skill_path.is_dir():
print(f"❌ Error: Path is not a directory: {skill_path}")
return None
# Validate SKILL.md exists
skill_md = skill_path / "SKILL.md"
if not skill_md.exists():
print(f"❌ Error: SKILL.md not found in {skill_path}")
return None
# Run validation before packaging
print("🔍 Validating skill...")
valid, message = validate_skill(skill_path)
if not valid:
print(f"❌ Validation failed: {message}")
print(" Please fix the validation errors before packaging.")
return None
print(f"{message}\n")
# Determine output location
skill_name = skill_path.name
if output_dir:
output_path = Path(output_dir).resolve()
output_path.mkdir(parents=True, exist_ok=True)
else:
output_path = Path.cwd()
skill_filename = output_path / f"{skill_name}.skill"
# Create the .skill file (zip format)
try:
with zipfile.ZipFile(skill_filename, 'w', zipfile.ZIP_DEFLATED) as zipf:
# Walk through the skill directory
for file_path in skill_path.rglob('*'):
if file_path.is_file():
# Calculate the relative path within the zip
arcname = file_path.relative_to(skill_path.parent)
zipf.write(file_path, arcname)
print(f" Added: {arcname}")
print(f"\n✅ Successfully packaged skill to: {skill_filename}")
return skill_filename
except Exception as e:
print(f"❌ Error creating .skill file: {e}")
return None
def main():
if len(sys.argv) < 2:
print("Usage: python utils/package_skill.py <path/to/skill-folder> [output-directory]")
print("\nExample:")
print(" python utils/package_skill.py skills/public/my-skill")
print(" python utils/package_skill.py skills/public/my-skill ./dist")
sys.exit(1)
skill_path = sys.argv[1]
output_dir = sys.argv[2] if len(sys.argv) > 2 else None
print(f"📦 Packaging skill: {skill_path}")
if output_dir:
print(f" Output directory: {output_dir}")
print()
result = package_skill(skill_path, output_dir)
if result:
sys.exit(0)
else:
sys.exit(1)
if __name__ == "__main__":
main()
+95
View File
@@ -0,0 +1,95 @@
#!/usr/bin/env python3
"""
Quick validation script for skills - minimal version
"""
import sys
import os
import re
import yaml
from pathlib import Path
def validate_skill(skill_path):
"""Basic validation of a skill"""
skill_path = Path(skill_path)
# Check SKILL.md exists
skill_md = skill_path / 'SKILL.md'
if not skill_md.exists():
return False, "SKILL.md not found"
# Read and validate frontmatter
content = skill_md.read_text()
if not content.startswith('---'):
return False, "No YAML frontmatter found"
# Extract frontmatter
match = re.match(r'^---\n(.*?)\n---', content, re.DOTALL)
if not match:
return False, "Invalid frontmatter format"
frontmatter_text = match.group(1)
# Parse YAML frontmatter
try:
frontmatter = yaml.safe_load(frontmatter_text)
if not isinstance(frontmatter, dict):
return False, "Frontmatter must be a YAML dictionary"
except yaml.YAMLError as e:
return False, f"Invalid YAML in frontmatter: {e}"
# Define allowed properties
ALLOWED_PROPERTIES = {'name', 'description', 'license', 'allowed-tools', 'metadata'}
# Check for unexpected properties (excluding nested keys under metadata)
unexpected_keys = set(frontmatter.keys()) - ALLOWED_PROPERTIES
if unexpected_keys:
return False, (
f"Unexpected key(s) in SKILL.md frontmatter: {', '.join(sorted(unexpected_keys))}. "
f"Allowed properties are: {', '.join(sorted(ALLOWED_PROPERTIES))}"
)
# Check required fields
if 'name' not in frontmatter:
return False, "Missing 'name' in frontmatter"
if 'description' not in frontmatter:
return False, "Missing 'description' in frontmatter"
# Extract name for validation
name = frontmatter.get('name', '')
if not isinstance(name, str):
return False, f"Name must be a string, got {type(name).__name__}"
name = name.strip()
if name:
# Check naming convention (hyphen-case: lowercase with hyphens)
if not re.match(r'^[a-z0-9-]+$', name):
return False, f"Name '{name}' should be hyphen-case (lowercase letters, digits, and hyphens only)"
if name.startswith('-') or name.endswith('-') or '--' in name:
return False, f"Name '{name}' cannot start/end with hyphen or contain consecutive hyphens"
# Check name length (max 64 characters per spec)
if len(name) > 64:
return False, f"Name is too long ({len(name)} characters). Maximum is 64 characters."
# Extract and validate description
description = frontmatter.get('description', '')
if not isinstance(description, str):
return False, f"Description must be a string, got {type(description).__name__}"
description = description.strip()
if description:
# Check for angle brackets
if '<' in description or '>' in description:
return False, "Description cannot contain angle brackets (< or >)"
# Check description length (max 1024 characters per spec)
if len(description) > 1024:
return False, f"Description is too long ({len(description)} characters). Maximum is 1024 characters."
return True, "Skill is valid!"
if __name__ == "__main__":
if len(sys.argv) != 2:
print("Usage: python quick_validate.py <skill_directory>")
sys.exit(1)
valid, message = validate_skill(sys.argv[1])
print(message)
sys.exit(0 if valid else 1)
+275
View File
@@ -0,0 +1,275 @@
"""
Skill-based Agent Implementation Guide
This guide shows how to integrate SKILL loading mechanism into DB-GPT agents.
"""
# ============================================================================
# 1. Basic Skill Definition
# ============================================================================
from dbgpt.agent.skill import (
Skill,
SkillBuilder,
SkillType,
)
from dbgpt.core import PromptTemplate
# Method 1: Define skill class
class CustomSkill(Skill):
"""Custom skill example."""
def __init__(self):
"""Initialize custom skill."""
metadata = SkillMetadata(
name="custom_skill",
description="A custom skill for specific tasks",
version="1.0.0",
skill_type=SkillType.Custom,
tags=["custom", "example"],
)
prompt = PromptTemplate.from_template(
"You are a custom assistant for specific tasks."
)
super().__init__(
metadata=metadata,
prompt_template=prompt,
required_tools=["tool1", "tool2"],
required_knowledge=["knowledge_base"],
)
# Method 2: Use SkillBuilder
custom_skill = (
SkillBuilder(name="my_skill", description="My awesome skill")
.with_version("1.0.0")
.with_author("Your Name")
.with_skill_type(SkillType.Coding)
.with_tags(["coding", "python"])
.with_prompt_template(
"You are a coding assistant. Help users write clean, efficient code."
)
.with_required_tool("python_interpreter")
.with_config({"max_lines": 1000})
.build()
)
# ============================================================================
# 2. Skill Registration
# ============================================================================
from dbgpt.agent.skill import get_skill_manager, initialize_skill
from dbgpt.component import SystemApp
def register_skills():
"""Register skills in the system."""
system_app = SystemApp()
initialize_skill(system_app)
skill_manager = get_skill_manager(system_app)
# Register skill instance
skill_manager.register_skill(
skill_instance=custom_skill,
name="my_awesome_skill",
)
# List all skills
skills = skill_manager.list_skills()
print(f"Registered skills: {skills}")
# ============================================================================
# 3. Agent with Skill Integration
# ============================================================================
from dbgpt.agent import ConversableAgent
from dbgpt.agent.skill import Skill
class SkillBasedAgent(ConversableAgent):
"""Agent that uses a skill."""
def __init__(self, skill: Skill, **kwargs):
"""Initialize agent with skill."""
super().__init__(**kwargs)
self._skill = skill
self._apply_skill_to_profile()
@property
def skill(self) -> Skill:
"""Return the skill."""
return self._skill
def _apply_skill_to_profile(self):
"""Apply skill settings to agent profile."""
if self.skill.prompt_template:
self.bind_prompt = self.skill.prompt_template
# Set profile based on skill metadata
if self.profile:
self.profile.goal = self.skill.metadata.description
async def load_resource(self, question: str, is_retry_chat: bool = False):
"""Load resources required by the skill."""
# Load required tools
if self.skill.required_tools and self.resource:
tools = self.resource.get_resource_by_type("tool")
for tool_name in self.skill.required_tools:
if tool_name not in [t.name for t in tools]:
raise ValueError(f"Required tool {tool_name} not found")
# Load required knowledge
if self.skill.required_knowledge and self.resource:
knowledge = self.resource.get_resource_by_type("knowledge")
for knowledge_name in self.skill.required_knowledge:
if knowledge_name not in [k.name for k in knowledge]:
raise ValueError(f"Required knowledge {knowledge_name} not found")
return await super().load_resource(question, is_retry_chat)
# ============================================================================
# 4. Skill Loading from Files
# ============================================================================
from dbgpt.agent.skill import SkillLoader
def load_skills_from_directory(directory: str):
"""Load all skills from a directory."""
loader = SkillLoader()
skills = loader.load_skills_from_directory(directory, recursive=True)
system_app = SystemApp()
initialize_skill(system_app)
skill_manager = get_skill_manager(system_app)
for skill in skills:
skill_manager.register_skill(skill_instance=skill, name=skill.metadata.name)
print(f"Loaded {len(skills)} skills from {directory}")
# ============================================================================
# 5. Dynamic Skill Switching
# ============================================================================
class DynamicSkillAgent(ConversableAgent):
"""Agent that can switch between different skills."""
def __init__(self, **kwargs):
"""Initialize agent with dynamic skill support."""
super().__init__(**kwargs)
self._current_skill: Optional[Skill] = None
self._available_skills: Dict[str, Skill] = {}
def register_skill(self, skill: Skill):
"""Register a skill."""
self._available_skills[skill.metadata.name] = skill
def switch_skill(self, skill_name: str):
"""Switch to a different skill."""
if skill_name not in self._available_skills:
raise ValueError(f"Skill {skill_name} not found")
self._current_skill = self._available_skills[skill_name]
if self._current_skill.prompt_template:
self.bind_prompt = self._current_skill.prompt_template
print(f"Switched to skill: {skill_name}")
@property
def current_skill(self) -> Optional[Skill]:
"""Return the current skill."""
return self._current_skill
# ============================================================================
# 6. Skill Composition (Multiple Skills)
# ============================================================================
class CompositeSkillAgent(ConversableAgent):
"""Agent that combines multiple skills."""
def __init__(self, skills: List[Skill], **kwargs):
"""Initialize agent with multiple skills."""
super().__init__(**kwargs)
self._skills = skills
def get_skill_by_type(self, skill_type: SkillType) -> Optional[Skill]:
"""Get skill by type."""
for skill in self._skills:
if skill.metadata.skill_type == skill_type:
return skill
return None
def get_all_tools(self) -> List[str]:
"""Get all required tools from all skills."""
all_tools = []
for skill in self._skills:
all_tools.extend(skill.required_tools)
return list(set(all_tools))
def combine_prompts(self) -> str:
"""Combine prompts from all skills."""
prompts = []
for skill in self._skills:
if skill.prompt_template:
prompts.append(skill.prompt_template.template)
return "\n\n".join(prompts)
# ============================================================================
# 7. Usage Example
# ============================================================================
async def example_usage():
"""Example usage of skill-based agents."""
from dbgpt.agent import AgentContext, LLMConfig, AgentMemory
from dbgpt.agent.resource import tool
@tool
def search(query: str) -> str:
"""Search for information.
Args:
query: Search query.
Returns:
Search results.
"""
return f"Search results for: {query}"
# Create skill
skill = (
SkillBuilder(name="search_skill", description="Search assistant")
.with_prompt_template("Help users search for information.")
.with_required_tool("search")
.build()
)
# Create agent with skill
agent = SkillBasedAgent(skill=skill)
# Bind necessary components
context = AgentContext(conv_id="test_conv")
llm_config = LLMConfig()
memory = AgentMemory()
await agent.bind(context).bind(llm_config).bind(memory).bind([search]).build()
print("Agent created with skill!")
print(f"Skill name: {agent.skill.metadata.name}")
print(f"Required tools: {agent.skill.required_tools}")
if __name__ == "__main__":
register_skills()
asyncio.run(example_usage())
+69
View File
@@ -0,0 +1,69 @@
---
name: walmart-sales-analyzer
description: Analyze Walmart sales data to explore trends between store sales and unemployment rates. Generate insightful visualizations and a beautiful HTML report with deep analysis. Suitable for quick insights into the relationship between sales data and macroeconomic factors.
---
# Walmart Sales Data Deep Analyzer
This skill is designed to help users conduct in-depth analysis of Walmart sales data, particularly exploring the relationship between sales and unemployment rates across different stores. It visually presents these trends by generating visualizations with detailed interpretations and professional HTML reports.
## Features
This skill provides the following analysis and visualization features:
1. **Data Correlation Heatmap**: Displays the correlation between all numerical variables in the dataset and provides a detailed interpretation.
2. **Sales vs. Unemployment Scatter Plot**: Visually demonstrates the relationship between weekly sales and the unemployment rate, accompanied by a regression line, and deeply analyzes consumption resilience under economic pressure.
3. **Time Series Trend of Sales and Unemployment for Specific Stores**: Tracks the trends of sales and unemployment rates over time for selected stores to analyze seasonal forces and macro trends.
4. **Comparison of Average Sales and Average Unemployment Across Stores**: Compares the average sales performance of different stores with local average unemployment rates to provide suggestions for regional operational strategies.
5. **HTML Deep Analysis Report Generation**: Automatically integrates all charts into a beautiful, responsive HTML report that includes detailed analysis conclusions and business recommendations.
## Usage
To use this skill, you need to provide a CSV file containing Walmart sales data. The file should contain at least the following columns: `Store` (Store ID), `Date` (Date), `Weekly_Sales` (Weekly Sales), `Unemployment` (Unemployment Rate).
## Core Workflow
1. **Check Uploaded File**: First, verify that a valid Walmart Sales CSV file was provided.
2. **Execute Analysis Script**: Use the `execute_skill_script_file` tool to run the `generate_html_report.py` script. Pass the CSV file path to the `input_file` argument in the `args` parameter.
- Example: `{"skill_name": "walmart-sales-analyzer", "script_file_name": "generate_html_report.py", "args": {"input_file": "/path/to/Walmart_Sales.csv", "output_dir": "."}}`
- *Note: This script automatically generates all required charts (`correlation_heatmap.png`, `sales_vs_unemployment_scatter.png`, etc.) and the base report.*
3. **Present Report**: To present the results to the user via the DB-GPT UI, you must use the `html_interpreter` tool. Provide the `template_path` (`walmart-sales-analyzer/templates/report_template.html`) and the necessary text data to render the report interactively. You MUST fill in ALL the placeholders dynamically based on your analysis (including ALL section titles, report titles, and analysis content, otherwise they will render as 'NA') and ensure they are translated to the user's language.
- Example `data` payload:
{
"LANG": "en",
"REPORT_TITLE": "Walmart Sales Deep Analysis Report",
"REPORT_SUBTITLE": "Based on macroeconomic indicators and store performance",
"EXEC_SUMMARY_TITLE": "Executive Summary",
"EXEC_SUMMARY_CONTENT": "<p>Your detailed summary...</p>",
"SECTION_1_TITLE": "1. Multi-dimensional Correlation Analysis",
"SECTION_1_ANALYSIS": "<h3><span class=\"tag\">Insights</span> Variable relationships</h3><ul><li>...</li></ul>",
"SECTION_2_TITLE": "2. Sales vs Unemployment Regression",
"SECTION_2_ANALYSIS": "<h3><span class=\"tag\">Deep Dive</span> Resilience under pressure</h3><p>...</p>",
"SECTION_3_TITLE": "3. Dynamic Trends Tracking",
"SECTION_3_ANALYSIS": "<h3><span class=\"tag\">Trends</span> Seasonal vs Macro</h3><p>...</p>",
"SECTION_4_TITLE": "4. Store Performance Comparison",
"SECTION_4_ANALYSIS": "<h3><span class=\"tag\">Strategy</span> Regional operations</h3><p>...</p>",
"CONCLUSION_TITLE": "Final Conclusions & Recommendations",
"CONCLUSION_CONTENT": "<ol><li>...</li></ol>",
"FOOTER_TEXT": "Deep Data-Driven Decisions"
}
```
4. **Complete Task**: Call `terminate` with a final answer summarizing your actions.
### Script List
* `scripts/generate_html_report.py`: **Recommended**, generates an HTML report containing all charts and deep analysis with one click.
* `scripts/generate_correlation_heatmap.py`: Generates a data correlation heatmap.
* `scripts/generate_sales_unemployment_scatter.py`: Generates a scatter plot of sales vs. unemployment rate.
* `scripts/generate_time_series_trend.py`: Generates a time series trend chart for a specific store.
* `scripts/generate_store_avg_comparison.py`: Generates a comparison chart of average values across stores.
### Templates
* `templates/report_template.html`: HTML style template used to generate the deep analysis report.
## Important Notes
* **Language Requirement: You MUST ensure that your output language exactly matches the language used by the user in their input/request.**
* All charts support multi-language display.
* The report template uses a responsive design suitable for viewing on different devices and provides detailed analysis interpretations and business suggestions.
@@ -0,0 +1,43 @@
import matplotlib
import matplotlib.pyplot as plt
import matplotlib.font_manager as fm
def setup_chinese_font():
"""Configure matplotlib to render Chinese characters correctly.
Tries a priority-ordered list of CJK fonts commonly available on
macOS, Linux, and Windows. Falls back to whatever the system has.
"""
candidates = [
# macOS
"Heiti TC",
"Hiragino Sans GB",
"PingFang SC",
"PingFang HK",
"STHeiti",
"Songti SC",
"Arial Unicode MS",
# Linux
"Noto Sans CJK SC",
"Noto Sans SC",
"WenQuanYi Micro Hei",
"WenQuanYi Zen Hei",
"Droid Sans Fallback",
# Windows
"Microsoft YaHei",
"SimHei",
"SimSun",
]
available = {f.name for f in fm.fontManager.ttflist}
for font_name in candidates:
if font_name in available:
plt.rcParams["font.sans-serif"] = [font_name, "sans-serif"]
plt.rcParams["font.family"] = "sans-serif"
plt.rcParams["axes.unicode_minus"] = False
return
# Last resort: let matplotlib pick, but still fix minus sign
plt.rcParams["axes.unicode_minus"] = False
@@ -0,0 +1,28 @@
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
from font_setup import setup_chinese_font
import os
def generate_correlation_heatmap(data_path, output_dir):
setup_chinese_font()
df = pd.read_csv(data_path)
plt.figure(figsize=(10, 8))
corr = df.corr(numeric_only=True)
sns.heatmap(corr, annot=True, cmap='coolwarm', fmt=".2f")
plt.title('Walmart 销售数据相关性热力图')
plt.tight_layout()
output_path = os.path.join(output_dir, 'correlation_heatmap.png')
plt.savefig(output_path, dpi=150, bbox_inches="tight")
plt.close()
if __name__ == '__main__':
import sys, json
args = json.loads(sys.argv[1]) if len(sys.argv) > 1 else {}
data_path = args.get('input_file') or args.get('file_path') or args.get('data_path', 'Walmart_Sales.csv')
out_dir = args.get('output_dir', os.environ.get('OUTPUT_DIR', '.'))
os.makedirs(out_dir, exist_ok=True)
generate_correlation_heatmap(data_path, out_dir)
print('Correlation heatmap generated.')
@@ -0,0 +1,57 @@
import os
import shutil
import json
from generate_correlation_heatmap import generate_correlation_heatmap
from generate_sales_unemployment_scatter import generate_sales_unemployment_scatter
from generate_time_series_trend import generate_time_series_trend
from generate_store_avg_comparison import generate_store_avg_comparison
def generate_html_report(data_path, output_dir):
# Create output directory if it doesn't exist
os.makedirs(output_dir, exist_ok=True)
# Generate all plots into the output directory
generate_correlation_heatmap(data_path, output_dir)
generate_sales_unemployment_scatter(data_path, output_dir)
generate_time_series_trend(data_path, output_dir)
generate_store_avg_comparison(data_path, output_dir)
images = [
"correlation_heatmap.png",
"sales_vs_unemployment_scatter.png",
"time_series_trend.png",
"store_avg_comparison.png"
]
chunks = []
for img in images:
img_path = os.path.join(output_dir, img)
if os.path.exists(img_path):
chunks.append({
"output_type": "image",
"content": os.path.abspath(img_path)
})
# Read HTML template
template_path = os.path.join(os.path.dirname(__file__), "..", "templates", "report_template.html")
with open(template_path, "r", encoding="utf-8") as f:
html_content = f.read()
# Save the final HTML report
report_output_path = os.path.join(output_dir, "walmart_sales_report.html")
with open(report_output_path, "w", encoding="utf-8") as f:
f.write(html_content)
chunks.append({
"output_type": "text",
"content": f"HTML report and {len(images)} charts generated successfully."
})
print(json.dumps({"chunks": chunks}, ensure_ascii=False))
if __name__ == "__main__":
import sys, json
args = json.loads(sys.argv[1]) if len(sys.argv) > 1 else {}
data_path = args.get('input_file') or args.get('file_path') or args.get('data_path', 'Walmart_Sales.csv')
out_dir = args.get('output_dir', os.environ.get('OUTPUT_DIR', '.'))
generate_html_report(data_path, out_dir)
@@ -0,0 +1,29 @@
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
from font_setup import setup_chinese_font
import os
def generate_sales_unemployment_scatter(data_path, output_dir):
setup_chinese_font()
df = pd.read_csv(data_path)
plt.figure(figsize=(10, 6))
sns.regplot(data=df, x='Unemployment', y='Weekly_Sales', scatter_kws={'alpha':0.3}, line_kws={'color':'red'})
plt.title('销售额与失业率的关系 (散点图 + 回归线)')
plt.xlabel('失业率 (%)')
plt.ylabel('周销售额')
plt.tight_layout()
output_path = os.path.join(output_dir, 'sales_vs_unemployment_scatter.png')
plt.savefig(output_path, dpi=150, bbox_inches="tight")
plt.close()
if __name__ == '__main__':
import sys, json
args = json.loads(sys.argv[1]) if len(sys.argv) > 1 else {}
data_path = args.get('input_file') or args.get('file_path') or args.get('data_path', 'Walmart_Sales.csv')
out_dir = args.get('output_dir', os.environ.get('OUTPUT_DIR', '.'))
os.makedirs(out_dir, exist_ok=True)
generate_sales_unemployment_scatter(data_path, out_dir)
print('Sales vs unemployment scatter plot generated.')
@@ -0,0 +1,30 @@
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
from font_setup import setup_chinese_font
import os
def generate_store_avg_comparison(data_path, output_dir):
setup_chinese_font()
df = pd.read_csv(data_path)
store_agg = df.groupby("Store").agg({"Weekly_Sales": "mean", "Unemployment": "mean"}).reset_index()
plt.figure(figsize=(10, 6))
sns.scatterplot(data=store_agg, x="Unemployment", y="Weekly_Sales", hue="Store", palette="viridis", s=100)
plt.title("各门店平均销售额与平均失业率的关系")
plt.xlabel("平均失业率 (%)")
plt.ylabel("平均周销售额")
plt.tight_layout()
output_path = os.path.join(output_dir, "store_avg_comparison.png")
plt.savefig(output_path, dpi=150, bbox_inches="tight")
plt.close()
if __name__ == "__main__":
import sys, json
args = json.loads(sys.argv[1]) if len(sys.argv) > 1 else {}
data_path = args.get('input_file') or args.get('file_path') or args.get('data_path', 'Walmart_Sales.csv')
out_dir = args.get('output_dir', os.environ.get('OUTPUT_DIR', '.'))
os.makedirs(out_dir, exist_ok=True)
generate_store_avg_comparison(data_path, out_dir)
print('Store average comparison plot generated.')
@@ -0,0 +1,40 @@
import pandas as pd
import matplotlib.pyplot as plt
from font_setup import setup_chinese_font
import os
def generate_time_series_trend(data_path, output_dir, selected_stores=[1, 4, 20]):
setup_chinese_font()
df = pd.read_csv(data_path)
df["Date"] = pd.to_datetime(df["Date"], dayfirst=True)
fig, ax1 = plt.subplots(figsize=(12, 6))
ax2 = ax1.twinx()
colors = ["blue", "green", "orange"]
for i, store in enumerate(selected_stores):
store_data = df[df["Store"] == store].sort_values("Date")
ax1.plot(store_data["Date"], store_data["Weekly_Sales"], label=f"门店 {store} 销售额", color=colors[i], alpha=0.7)
if i == 0: # Just plot unemployment for one store as it's often regional/similar
ax2.plot(store_data["Date"], store_data["Unemployment"], label="失业率", color="red", linestyle="--", linewidth=2)
ax1.set_xlabel("日期")
ax1.set_ylabel("周销售额", color="blue")
ax2.set_ylabel("失业率 (%)", color="red")
plt.title("特定门店销售额与失业率随时间的变化趋势")
ax1.legend(loc="upper left")
ax2.legend(loc="upper right")
plt.tight_layout()
output_path = os.path.join(output_dir, 'time_series_trend.png')
plt.savefig(output_path, dpi=150, bbox_inches="tight")
plt.close()
if __name__ == "__main__":
import sys, json
args = json.loads(sys.argv[1]) if len(sys.argv) > 1 else {}
data_path = args.get('input_file') or args.get('file_path') or args.get('data_path', 'Walmart_Sales.csv')
out_dir = args.get('output_dir', os.environ.get('OUTPUT_DIR', '.'))
os.makedirs(out_dir, exist_ok=True)
generate_time_series_trend(data_path, out_dir)
print('Time series trend plot generated.')
@@ -0,0 +1,213 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Walmart 销售数据深度分析报告</title>
<style>
:root {
--primary-color: #0071ce;
--secondary-color: #ffc220;
--text-color: #333;
--bg-color: #f0f2f5;
--card-bg: #ffffff;
--accent-color: #e7f3ff;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif, "Noto Sans CJK SC";
line-height: 1.8;
color: var(--text-color);
background-color: var(--bg-color);
margin: 0;
padding: 0;
}
.container {
max-width: 1100px;
margin: 0 auto;
padding: 40px 20px;
}
header {
background: linear-gradient(135deg, var(--primary-color) 0%, #004f8b 100%);
color: white;
padding: 60px 0;
text-align: center;
margin-bottom: 40px;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
}
header h1 {
margin: 0;
font-size: 2.8em;
letter-spacing: 1px;
}
header p {
margin: 15px 0 0;
font-size: 1.2em;
opacity: 0.9;
}
.section {
background: var(--card-bg);
padding: 40px;
border-radius: 12px;
box-shadow: 0 8px 24px rgba(0,0,0,0.05);
margin-bottom: 40px;
}
h2 {
color: var(--primary-color);
border-left: 5px solid var(--secondary-color);
padding-left: 15px;
margin-top: 0;
margin-bottom: 25px;
font-size: 1.8em;
}
.analysis-box {
background-color: var(--accent-color);
border-radius: 8px;
padding: 20px;
margin-top: 20px;
border-left: 4px solid var(--primary-color);
}
.analysis-box h3 {
margin-top: 0;
color: var(--primary-color);
font-size: 1.2em;
}
.chart-container {
text-align: center;
margin: 30px 0;
}
.chart-container img {
max-width: 100%;
height: auto;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
}
.insight-list {
padding-left: 20px;
}
.insight-list li {
margin-bottom: 12px;
}
.tag {
display: inline-block;
padding: 4px 12px;
border-radius: 20px;
font-size: 0.85em;
font-weight: bold;
margin-right: 10px;
background-color: var(--secondary-color);
color: #444;
}
footer {
text-align: center;
padding: 50px 0;
color: #777;
font-size: 0.95em;
}
hr {
border: 0;
height: 1px;
background: #eee;
margin: 40px 0;
}
</style>
</head>
<body>
<header>
<div class="container">
<h1>Walmart 销售数据深度分析报告</h1>
<p>基于宏观经济指标与门店业绩的关联性研究</p>
</div>
</header>
<div class="container">
<!-- Executive Summary -->
<section class="section">
<h2>执行摘要</h2>
<p>本报告旨在通过对 Walmart 历史销售数据的多维度分析,揭示<strong>失业率、CPI、油价</strong>等宏观经济因素对零售业绩的影响。通过数据可视化,我们识别了销售波动的核心驱动力,并为未来的库存管理和市场策略提供参考。</p>
</section>
<!-- 1. Correlation Analysis -->
<section class="section">
<h2>1. 多维度相关性分析</h2>
<div class="chart-container">
<img src="correlation_heatmap.png" alt="相关性热力图">
</div>
<div class="analysis-box">
<h3><span class="tag">数据解读</span> 变量间的内在联系</h3>
<ul class="insight-list">
<li><strong>销售额与失业率</strong>:呈现微弱的负相关(约 -0.11),表明失业率上升对整体销售有抑制作用,但并非主导因素。</li>
<li><strong>销售额与节假日</strong>:正相关性显著,验证了零售业强烈的季节性特征。</li>
<li><strong>CPI 与失业率</strong>:通常呈现负相关,反映了通货膨胀与就业市场之间的宏观经济博弈。</li>
</ul>
</div>
</section>
<!-- 2. Sales vs Unemployment -->
<section class="section">
<h2>2. 销售额与失业率的回归探索</h2>
<div class="chart-container">
<img src="sales_vs_unemployment_scatter.png" alt="销售额与失业率散点图">
</div>
<div class="analysis-box">
<h3><span class="tag">深度洞察</span> 经济压力下的消费韧性</h3>
<p>散点图结合回归线清晰地展示了随着失业率百分比的增加,周销售额的中位数呈现缓慢下降趋势。然而,数据点的离散程度较高,这表明:</p>
<ul class="insight-list">
<li><strong>消费刚性</strong>:作为大型零售商,Walmart 销售的许多商品属于生活必需品,受经济波动影响相对较小。</li>
<li><strong>异常值分析</strong>:在失业率较高的区域,仍有部分门店保持极高销售额,这可能与当地缺乏竞争对手或门店规模较大有关。</li>
</ul>
</div>
</section>
<!-- 3. Time Series Trends -->
<section class="section">
<h2>3. 动态趋势追踪:销售额 vs 失业率</h2>
<div class="chart-container">
<img src="time_series_trend.png" alt="时间序列趋势图">
</div>
<div class="analysis-box">
<h3><span class="tag">趋势研判</span> 季节性力量 vs 宏观趋势</h3>
<p>通过对比特定门店(Store 1, 4, 20)的长期走势,我们可以观察到:</p>
<ul class="insight-list">
<li><strong>周期性主导</strong>:每年的 Q4 季度(11-12月)销售额均会出现爆发式增长,这种季节性力量远超失业率的微小波动。</li>
<li><strong>滞后效应</strong>:失业率的变动通常是缓慢且具有趋势性的,而销售额对短期促销和节假日反应更为敏锐。</li>
<li><strong>门店一致性</strong>:尽管不同门店的销售基数不同,但其波动节奏高度一致,说明全国性的促销活动和节假日是核心驱动力。</li>
</ul>
</div>
</section>
<!-- 4. Store Comparison -->
<section class="section">
<h2>4. 门店表现横向对比</h2>
<div class="chart-container">
<img src="store_avg_comparison.png" alt="门店平均值对比图">
</div>
<div class="analysis-box">
<h3><span class="tag">策略建议</span> 区域化运营策略</h3>
<p>各门店平均销售额与平均失业率的分布图揭示了显著的地域差异:</p>
<ul class="insight-list">
<li><strong>高潜力区域</strong>:识别那些处于低失业率且高销售额的门店,作为旗舰店进行重点资源投入。</li>
<li><strong>风险预警</strong>:对于处于高失业率区域且销售额持续低迷的门店,需考虑优化商品结构,增加高性价比(Value-for-money)产品的占比。</li>
</ul>
</div>
</section>
<!-- Conclusion -->
<section class="section">
<h2>最终结论与建议</h2>
<p>综合以上分析,我们建议:</p>
<ol class="insight-list">
<li><strong>强化季节性备货</strong>:鉴于节假日对销售的决定性影响,应提前 3-6 个月完成 Q4 供应链优化。</li>
<li><strong>动态定价策略</strong>:在失业率上升较快的区域,适度增加促销频率,以维持客流量。</li>
<li><strong>关注 CPI 波动</strong>:虽然失业率影响有限,但 CPI 的变动直接关系到采购成本和利润空间,需密切监控。</li>
</ol>
</section>
</div>
<footer>
<div class="container">
<p>&copy; 2026 Walmart Sales Analysis Tool | 深度数据驱动决策</p>
<p>Generated by DB-GPT AI Professional Suite</p>
</div>
</footer>
</body>
</html>