mirror of
https://github.com/csunny/DB-GPT.git
synced 2026-07-16 17:15:22 +00:00
feat:create skill example
This commit is contained in:
@@ -65,7 +65,7 @@ async def execute_skill_script(skill_name: str, script_name: str, args: dict) ->
|
||||
"\\n示例:"
|
||||
'\\n- 读取参考文档: {"skill_name": "my-skill", '
|
||||
'"resource_path": "references/analysis_framework.md"}'
|
||||
"\\n注意: 执行脚本请使用 execute_skill_script_file 工具"
|
||||
"\n注意: 执行脚本请使用 shell_interpreter 工具"
|
||||
)
|
||||
async def get_skill_resource(
|
||||
skill_name: str, resource_path: str, args: Optional[dict] = None
|
||||
@@ -1498,6 +1498,113 @@ print(json.dumps(summary, ensure_ascii=False))
|
||||
}
|
||||
)
|
||||
|
||||
# ── Safety-net post-processing for skill script execution ──
|
||||
# If the LLM used shell_interpreter to run a skill script despite
|
||||
# the prompt requesting execute_skill_script_file, we still capture
|
||||
# critical side-effects (ratio_data, images) into react_state.
|
||||
_code_lower = code.strip().lower()
|
||||
_is_skill_script = "skills/" in _code_lower and ".py" in _code_lower
|
||||
if _is_skill_script and output_text.strip():
|
||||
import shutil
|
||||
|
||||
from dbgpt.configs.model_config import STATIC_MESSAGE_IMG_PATH
|
||||
|
||||
# 1) Capture calculate_ratios.py output as ratio_data
|
||||
if "calculate_ratios" in _code_lower:
|
||||
try:
|
||||
ratio_data = json.loads(output_text.strip())
|
||||
if isinstance(ratio_data, dict):
|
||||
react_state["ratio_data"] = ratio_data
|
||||
logger.info(
|
||||
"shell_interpreter: captured %d ratio_data keys",
|
||||
len(ratio_data),
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# 2) Capture generate_charts.py output — look for image paths
|
||||
# and copy them to static dir, same as execute_skill_script_file
|
||||
if "generate_charts" in _code_lower:
|
||||
try:
|
||||
os.makedirs(STATIC_MESSAGE_IMG_PATH, exist_ok=True)
|
||||
IMAGE_EXTS = {".png", ".jpg", ".jpeg", ".gif", ".svg", ".webp"}
|
||||
# Try to parse JSON output for image paths
|
||||
try:
|
||||
chart_output = json.loads(output_text.strip())
|
||||
if isinstance(chart_output, dict):
|
||||
# Might be {"charts": {...}} or flat dict
|
||||
chart_map = chart_output.get("charts", chart_output)
|
||||
for name, abs_path in chart_map.items():
|
||||
if isinstance(abs_path, str) and os.path.isfile(abs_path):
|
||||
ext = os.path.splitext(abs_path)[1].lower()
|
||||
if ext in IMAGE_EXTS:
|
||||
unique_name = (
|
||||
f"{uuid.uuid4().hex[:8]}_"
|
||||
f"{os.path.basename(abs_path)}"
|
||||
)
|
||||
dest = os.path.join(
|
||||
STATIC_MESSAGE_IMG_PATH, unique_name
|
||||
)
|
||||
shutil.copy2(abs_path, dest)
|
||||
img_url = f"/images/{unique_name}"
|
||||
react_state.setdefault(
|
||||
"generated_images", []
|
||||
).append(img_url)
|
||||
orig_stem = os.path.splitext(
|
||||
os.path.basename(abs_path)
|
||||
)[0].lower()
|
||||
react_state.setdefault(
|
||||
"image_url_map", {}
|
||||
)[orig_stem] = img_url
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
pass
|
||||
# Also scan the output dir for any new .png files
|
||||
cid = react_state.get("conv_id") or "default"
|
||||
from dbgpt.configs.model_config import PILOT_PATH
|
||||
|
||||
out_dir = os.path.join(PILOT_PATH, "tmp", cid)
|
||||
if os.path.isdir(out_dir):
|
||||
for fname in os.listdir(out_dir):
|
||||
ext = os.path.splitext(fname)[1].lower()
|
||||
if ext in IMAGE_EXTS:
|
||||
abs_path = os.path.join(out_dir, fname)
|
||||
orig_stem = os.path.splitext(fname)[0].lower()
|
||||
if orig_stem not in react_state.get(
|
||||
"image_url_map", {}
|
||||
):
|
||||
unique_name = (
|
||||
f"{uuid.uuid4().hex[:8]}_{fname}"
|
||||
)
|
||||
dest = os.path.join(
|
||||
STATIC_MESSAGE_IMG_PATH, unique_name
|
||||
)
|
||||
shutil.copy2(abs_path, dest)
|
||||
img_url = f"/images/{unique_name}"
|
||||
react_state.setdefault(
|
||||
"generated_images", []
|
||||
).append(img_url)
|
||||
react_state.setdefault(
|
||||
"image_url_map", {}
|
||||
)[orig_stem] = img_url
|
||||
# Append image URL summary for LLM reference
|
||||
all_images = react_state.get("generated_images", [])
|
||||
if all_images:
|
||||
img_summary = (
|
||||
"\u5df2\u751f\u6210\u7684\u56fe\u7247URL\uff08\u5728\u751f\u6210HTML\u62a5\u544a\u65f6\u8bf7\u4f7f\u7528\u8fd9\u4e9bURL\uff09:\n"
|
||||
+ "\n".join(f" - {url}" for url in all_images)
|
||||
)
|
||||
chunks.append(
|
||||
{"output_type": "text", "content": img_summary}
|
||||
)
|
||||
logger.info(
|
||||
"shell_interpreter: captured %d images for skill script",
|
||||
len(react_state.get("image_url_map", {})),
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
"shell_interpreter: image post-processing failed: %s", e
|
||||
)
|
||||
|
||||
return json.dumps({"chunks": chunks}, ensure_ascii=False)
|
||||
|
||||
@tool(
|
||||
@@ -2051,7 +2158,7 @@ print(json.dumps(summary, ensure_ascii=False))
|
||||
1. 严格按照已加载技能的指令执行
|
||||
2. 每个步骤输出 Thought → Phase → Action → Action Input
|
||||
3. 等待系统返回 Observation 后,再决定下一步
|
||||
4. 如果任务需要生成分析报告,流程为:`execute_skill_script_file` 执行 `extract_financials.py` 提取数据 → `execute_skill_script_file` 执行 `calculate_ratios.py` 计算比率(系统自动记录结果) → `execute_skill_script_file` 执行 `generate_charts.py` 生成图表(系统自动合并图片) → 调用 `html_interpreter(template_path=..., data={{...仅包含你写的分析文本}})` 渲染报告(系统自动合并数据和图片) → `terminate` 返回摘要
|
||||
4. 如果任务需要生成分析报告,流程为:`execute_skill_script_file` 执行 extract_financials.py 提取数据 → `execute_skill_script_file` 执行 calculate_ratios.py 计算比率(系统自动记录结果) → `execute_skill_script_file` 执行 generate_charts.py 生成图表(系统自动合并图片) → 调用 `html_interpreter(template_path=..., data={{...仅包含你写的分析文本}})` 渲染报告(系统自动合并数据和图片) → `terminate` 返回摘要
|
||||
5. 如果任务不需要生成报告,直接调用 terminate 返回最终结果,Action Input 格式必须为 {{"result": "最终回答"}}
|
||||
|
||||
{skill_prompt_context}
|
||||
@@ -2059,26 +2166,25 @@ print(json.dumps(summary, ensure_ascii=False))
|
||||
|
||||
## 技能执行规范
|
||||
### 资源使用
|
||||
- **需要计算/处理数据** → 使用 `execute_skill_script_file` 执行技能 scripts 目录下的脚本
|
||||
- **需要执行技能脚本** → 使用 `execute_skill_script_file`,参数为 {{"skill_name": "技能名", "script_file_name": "脚本文件名", "args": {{参数}}}}。此工具会自动处理图片复制和数据记录。
|
||||
- **需要了解指标定义/分析框架** → 使用 `get_skill_resource` 并指定 `references/xxx.md` 路径读取参考文档
|
||||
- **遇到图片文件** → 如果模型不支持图片输入,会返回错误提示
|
||||
- **需要生成报告** → 调用 `html_interpreter`,传入 `template_path`(模板相对路径)和 `data`(仅包含你自己撰写的分析文本,由于后端会自动合并之前工具生成的30个数据指标和图片URL,请绝对不要在 `data` 里写这些数据指标,否则会导致超长截断)。**不要使用 `code_interpreter`,不要使用 `execute_skill_script_file` 生成报告,不需要先用 `get_skill_resource` 读取模板**
|
||||
- **需要生成报告** → 调用 `html_interpreter`,传入 `template_path`(模板相对路径)和 `data`(仅包含你自己撰写的分析文本,由于后端会自动合并之前工具生成的30个数据指标和图片URL,请绝对不要在 `data` 里写这些数据指标,否则会导致超长截断)。**不要使用 `code_interpreter` 生成报告,不需要先用 `get_skill_resource` 读取模板**
|
||||
|
||||
## 可用工具说明
|
||||
1. **execute_skill_script_file**(用于执行技能自身的脚本): 执行技能 scripts 目录下的脚本文件。
|
||||
参数: {{"skill_name": "技能名", "script_file_name": "脚本文件名", "args": {{"参数名": "参数值"}}}}
|
||||
- 示例: {{"skill_name": "{pre_matched_skill.metadata.name if pre_matched_skill else 'skill'}", "script_file_name": "calculate.py", "args": {{"param": "value"}}}}
|
||||
1. **execute_skill_script_file**(推荐用于执行技能脚本): 执行技能 scripts 目录下的脚本文件,自动处理图片复制到静态目录、记录计算结果等后处理。
|
||||
参数: {{"skill_name": "技能名", "script_file_name": "脚本文件名", "args": {{参数}}}}
|
||||
- 示例: {{"skill_name": "{pre_matched_skill.metadata.name if pre_matched_skill else 'skill'}", "script_file_name": "calculate_ratios.py", "args": {{"input_data": "..."}}}}
|
||||
- **执行技能脚本时,必须使用此工具**,不要使用 shell_interpreter
|
||||
2. **get_skill_resource**: 读取技能中的参考文档、配置、模板等非脚本资源文件。
|
||||
参数: {{"skill_name": "技能名", "resource_path": "资源路径"}}
|
||||
- 读取参考文档: {{"skill_name": "{pre_matched_skill.metadata.name if pre_matched_skill else 'skill'}", "resource_path": "references/analysis_framework.md"}}
|
||||
- 注意: 报告模板不需要用此工具读取,直接用 html_interpreter 的 template_path 参数
|
||||
3. **execute_skill_script**: 执行技能中定义的内联脚本。参数: {{"skill_name": "技能名", "script_name": "脚本名", "args": {{"参数名": "参数值"}}}}
|
||||
4. **shell_interpreter**: 执行 shell/bash 命令。适用于运行系统命令、执行 Python 脚本等场景。
|
||||
3. **execute_skill_script**: 执行技能中定义的内联脚本(备用)。参数: {{"skill_name": "技能名", "script_name": "脚本名", "args": {{"参数名": "参数值"}}}}
|
||||
4. **shell_interpreter**: 执行 shell/bash 命令(仅用于非技能脚本的系统命令,如 ls、cat 等)。
|
||||
参数: {{"code": "shell命令"}}
|
||||
- 示例: {{"code": "python skills/skill-creator/scripts/init_skill.py my-skill --path skills/"}}
|
||||
- 示例: {{"code": "ls -la skills/my-skill/"}}
|
||||
- 每次调用独立,不保留状态。如需多步操作,用 `&&` 或 `;` 串联命令。
|
||||
- **当技能 SKILL.md 指示使用 shell_interpreter 执行脚本时,优先使用此工具**
|
||||
- **注意:不要用此工具执行技能脚本**,因为它不会自动处理图片和数据记录
|
||||
5. **html_interpreter**: 将 HTML 模板渲染为网页报告。
|
||||
推荐用法: {{"template_path": "技能名/templates/模板文件.html", "data": {{"PLACEHOLDER_KEY": "值", ...}}, "title": "报告标题"}}
|
||||
- 后端会自动把先前的财务数据计算结果合并进模板中。你只需要在 `data` 字典中返回诸如 `PROFITABILITY_ANALYSIS` 等你手写的分析段落即可,无需包含 `COMPANY_NAME` 或 `REVENUE` 等。
|
||||
@@ -2135,7 +2241,7 @@ Action Input: 工具参数的 JSON 格式
|
||||
加载技能后,仔细阅读 SKILL.md 中的 **核心工作流程** 部分,按步骤顺序执行。如果某个步骤明确说明了跳过条件(如用户意图已明确时),则直接跳到下一步,不要强制执行每一步。优先快速产出结果,迭代优化在后续步骤完成。
|
||||
|
||||
### 2. 资源使用时机
|
||||
- **需要计算/处理数据** → 使用 `execute_skill_script_file` 执行技能 scripts 目录下的脚本
|
||||
- **需要计算/处理数据** → 使用 `execute_skill_script_file` 执行技能 scripts 目录下的脚本(此工具会自动处理图片和数据记录),参数为 {{"skill_name": "技能名", "script_file_name": "脚本.py", "args": {{参数}}}}
|
||||
- **需要了解指标定义/分析框架** → 使用 `get_skill_resource` 并指定 `references/xxx.md` 路径读取参考文档
|
||||
- **遇到图片文件** → 如果模型不支持图片输入,会返回错误提示
|
||||
|
||||
@@ -2152,20 +2258,19 @@ Action Input: {{"skill_name": "financial-report-analyzer", "resource_path": "ref
|
||||
Thought: 现在执行脚本计算比率
|
||||
Phase: 执行比率计算脚本
|
||||
Action: execute_skill_script_file
|
||||
Action Input: {{"skill_name": "financial-report-analyzer", "script_file_name": "calculate_ratios.py", "args": {{...}}}}
|
||||
Action Input: {{"skill_name": "financial-report-analyzer", "script_file_name": "calculate_ratios.py", "args": {{"input_data": "..."}}}}
|
||||
```
|
||||
|
||||
## 可用工具说明
|
||||
1. **load_skill**: 加载指定技能的详细说明,参数: {{"skill_name": "技能名", "file_path": "技能文件路径"}}
|
||||
2. **knowledge_retrieve**: 从知识库中检索相关信息,参数: {{"query": "检索问题"}}
|
||||
3. **execute_skill_script**: 执行技能中定义的内联脚本,参数: {{"skill_name": "技能名", "script_name": \
|
||||
3. **execute_skill_script**: 执行技能中定义的内联脚本(备用),参数: {{"skill_name": "技能名", "script_name": \
|
||||
"脚本名", "args": {{"参数名": "参数值"}}}}
|
||||
4. **execute_skill_script_file**(推荐用于脚本执行): 执行技能 scripts 目录下的脚本文件,参数: {{"skill_name": "技能名", \
|
||||
"script_file_name": "脚本文件名如calculate_ratios.py", "args": {{"参数名": "参数值"}}}}
|
||||
4. **execute_skill_script_file**(推荐用于执行技能脚本): 执行技能 scripts 目录下的脚本文件,自动处理图片复制和数据记录。参数: {{"skill_name": "技能名", "script_file_name": "脚本文件名", "args": {{参数}}}}
|
||||
5. **get_skill_resource**: 读取技能中的参考文档、配置等非脚本资源文件。
|
||||
参数: {{"skill_name": "技能名", "resource_path": "资源路径"}}
|
||||
- 读取参考文档: {{"skill_name": "my-skill", "resource_path": "references/analysis_framework.md"}}
|
||||
- 注意: 执行脚本请使用 execute_skill_script_file,不要用此工具执行脚本
|
||||
- 注意: 执行脚本请使用 execute_skill_script_file(不是 shell_interpreter),不要用此工具执行脚本
|
||||
- 图片文件会返回错误提示(模型不支持)
|
||||
6. **code_interpreter**: 执行 Python 代码进行数据分析和计算。支持 pandas、numpy、matplotlib 等。已预导入 \
|
||||
pandas(pd)、numpy(np)、json、os。如果用户上传了文件,FILE_PATH 变量已预设为文件路径。PLOT_DIR 变量已预设为图片保存目录。参数: {{"code": "python代码"}}
|
||||
|
||||
@@ -26,6 +26,10 @@ EXAMPLE_FILES = {
|
||||
"path": "docker/examples/fin_report/pdf/2020-01-23__浙江海翔药业股份有限公司__002099__海翔药业__2019年__年度报告.pdf",
|
||||
"name": "2020-01-23__浙江海翔药业股份有限公司__002099__海翔药业__2019年__年度报告.pdf",
|
||||
},
|
||||
"create_sql_skill": {
|
||||
"path": "docker/examples/txt/sql_skill.txt",
|
||||
"name": "sql_skill.txt",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -14,70 +14,94 @@ description: 专门用于上市公司财报(如年度报告、季度报告)
|
||||
- 脚本支持 PDF 文件(通过 pdfplumber 解析)和纯文本文件,返回 JSON 格式的结构化数据。
|
||||
|
||||
2. **财务比率计算**:
|
||||
- 将提取的 JSON 数据传递给 `scripts/calculate_ratios.py` 脚本。
|
||||
- 使用 `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. **图表生成**:
|
||||
- 将提取的 JSON 数据传递给 `scripts/generate_charts.py` 脚本。
|
||||
- 使用 `execute_skill_script_file` 执行 `scripts/generate_charts.py`,传入 Step 1 的 JSON 数据。
|
||||
- 自动生成 3 张可视化图表:
|
||||
- `financial_overview.png`:核心财务指标对比柱状图
|
||||
- `profitability.png`:盈利能力指标横向条形图
|
||||
- `asset_structure.png`:资产结构环形饼图
|
||||
- 脚本执行后,系统自动扫描生成的图片文件并返回 `/images/xxx.png` 格式的 URL,可直接用于 HTML 报告。
|
||||
- **系统会自动将图片复制到静态目录并记录 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` 和 `data` 参数:
|
||||
- `template_path`: `"financial-report-analyzer/templates/report_template.html"`
|
||||
- `data`: 包含你需要填写的占位符键值的字典,包括:
|
||||
- LLM 自行撰写的分析文本键:`PROFITABILITY_ANALYSIS`, `SOLVENCY_ANALYSIS`, `EFFICIENCY_ANALYSIS`, `CASHFLOW_ANALYSIS`, `ADVANTAGES_LIST`, `RISKS_LIST`, `OVERALL_ASSESSMENT`
|
||||
- (注:来自 `calculate_ratios.py` 的 30 个数据键会被系统自动合并到模板中,你不需要在 `data` 里再次传入它们,只需传入你写的分析文本即可)
|
||||
- 后端自动读取模板文件并替换 `{{占位符}}`,无需手动拼接 HTML。
|
||||
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 的图表 URL(CHART_FINANCIAL_OVERVIEW、CHART_PROFITABILITY、CHART_ASSET_STRUCTURE)
|
||||
- **绝对不要**在 `data` 中包含数据指标或图表路径,否则 JSON 过大会导致截断。
|
||||
|
||||
6. **完成**:
|
||||
- 调用 `terminate` 返回 1-2 句话的简短摘要。
|
||||
- **注意**:不需要使用 `code_interpreter`,不需要使用 `execute_skill_script_file` 生成报告,不需要先用 `get_skill_resource` 读取模板。
|
||||
- **严格保留**模板的完整结构:所有章节(第1-4章)不得删除,摘要表格必须保留4列。
|
||||
- 报告会以卡片形式展示在左侧面板,用户点击卡片即可在右侧面板查看完整报告。
|
||||
|
||||
## 完整流程示例
|
||||
|
||||
```
|
||||
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, ...}
|
||||
→ 返回 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=<Step1的JSON结果>)
|
||||
→ 返回 30 个模板键值: {"COMPANY_NAME": "XX公司", "REVENUE": "105.00亿元", "GROSS_MARGIN": "28.57%", ...}
|
||||
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=<Step1的JSON结果>)
|
||||
→ 生成图表,系统自动返回图片URL: /images/xxx_financial_overview.png, /images/xxx_profitability.png, /images/xxx_asset_structure.png
|
||||
Step 3: execute_skill_script_file(skill_name="financial-report-analyzer", script_file_name="generate_charts.py", args=<raw_data>)
|
||||
→ 生成图表,系统自动复制到 /images/ 并记录 URL 映射
|
||||
|
||||
Step 4: html_interpreter(template_path="financial-report-analyzer/templates/report_template.html", data={
|
||||
"PROFITABILITY_ANALYSIS": "LLM撰写的盈利能力分析...",
|
||||
"SOLVENCY_ANALYSIS": "LLM撰写的偿债分析...",
|
||||
...其他LLM分析文本
|
||||
})
|
||||
Step 4: (LLM 自行撰写 7 段深度分析文本)
|
||||
|
||||
Step 5: terminate(message="简短摘要")
|
||||
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),图表 URL 由系统自动扫描返回。
|
||||
- `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` 参数直接引用,后端自动替换 `{{占位符}}`。模板包含 3 处图表占位符(`{{CHART_FINANCIAL_OVERVIEW}}`, `{{CHART_PROFITABILITY}}`, `{{CHART_ASSET_STRUCTURE}}`),需填入 Step 3 返回的图片 URL。
|
||||
- `templates/report_template.html`:最终交付报告的 HTML 模板(**必须严格遵循**,不得删减章节或修改表格结构)。由 html_interpreter 的 template_path 参数自动读取并填充。
|
||||
- `templates/report_template.md`:Markdown 版本,仅供参考结构说明。
|
||||
|
||||
## 注意事项
|
||||
|
||||
- **必须使用 `execute_skill_script_file`** 执行脚本(不要用 shell_interpreter),因为 `execute_skill_script_file` 会自动处理图片复制和数据记录。
|
||||
- 脚本提取可能受排版影响,建议在计算前人工核对提取的关键数值。
|
||||
- 始终关注"非经常性损益",以评估公司核心业务的真实盈利能力。
|
||||
- 对比至少三年的历史数据,以识别趋势。
|
||||
|
||||
@@ -342,7 +342,7 @@ const SkillCompactCard: React.FC<{
|
||||
onDownload?: () => void;
|
||||
}> = memo(({ skillName, onClick, onDownload }) => {
|
||||
const [downloading, setDownloading] = useState(false);
|
||||
|
||||
const [isAdded, setIsAdded] = useState(false);
|
||||
return (
|
||||
<div
|
||||
onClick={onClick}
|
||||
@@ -373,14 +373,30 @@ const SkillCompactCard: React.FC<{
|
||||
</button>
|
||||
</Tooltip>
|
||||
<button
|
||||
className='flex items-center gap-1.5 bg-gray-900 dark:bg-gray-100 text-white dark:text-gray-900 rounded-lg text-xs font-medium px-4 py-2 hover:opacity-90 transition-opacity'
|
||||
className={`flex items-center gap-1.5 rounded-lg text-xs font-medium px-4 py-2 transition-all duration-200 ${
|
||||
isAdded
|
||||
? 'bg-green-50 text-green-600 border border-green-200 dark:bg-green-900/20 dark:text-green-500 dark:border-green-800'
|
||||
: 'bg-gray-900 dark:bg-gray-100 text-white dark:text-gray-900 hover:opacity-90'
|
||||
}`}
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
message.success(`技能 "${skillName}" 已添加到我的技能`);
|
||||
if (!isAdded) {
|
||||
setIsAdded(true);
|
||||
message.success(`技能 "${skillName}" 已添加到我的技能`);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<PlusOutlined className='text-[10px]' />
|
||||
添加到我的技能
|
||||
{isAdded ? (
|
||||
<>
|
||||
<CheckOutlined className='text-[10px]' />
|
||||
已添加
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<PlusOutlined className='text-[10px]' />
|
||||
添加
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
AppstoreOutlined,
|
||||
BarChartOutlined,
|
||||
CheckCircleFilled,
|
||||
CheckOutlined,
|
||||
CloseCircleFilled,
|
||||
CodeOutlined,
|
||||
ConsoleSqlOutlined,
|
||||
@@ -1047,14 +1048,14 @@ const parseSkillCreatorOutput = (detail?: string, outputs?: ExecutionOutput[]):
|
||||
const parsed = JSON.parse(inputMatch[1]);
|
||||
if (parsed.skill_name) return parsed.skill_name;
|
||||
if (parsed.name) return parsed.name;
|
||||
} catch { /* ignore */ }
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Priority 3: Last skills/xxx path (skip skill-creator which is the tool, not the created skill)
|
||||
const allPaths = [...combined.matchAll(/skills\/([\w-]+)/g)]
|
||||
.map(m => m[1])
|
||||
.filter(name => name !== 'skill-creator');
|
||||
const allPaths = [...combined.matchAll(/skills\/([\w-]+)/g)].map(m => m[1]).filter(name => name !== 'skill-creator');
|
||||
if (allPaths.length > 0) return allPaths[allPaths.length - 1];
|
||||
|
||||
return null;
|
||||
@@ -1137,7 +1138,7 @@ const SkillCardRenderer: React.FC<{
|
||||
const [fileContent, setFileContent] = useState<string>('');
|
||||
const [showDetail, setShowDetail] = useState(true);
|
||||
const [downloading, setDownloading] = useState(false);
|
||||
|
||||
const [isAdded, setIsAdded] = useState(false);
|
||||
// Fetch skill detail on mount
|
||||
useEffect(() => {
|
||||
const fetchDetail = async () => {
|
||||
@@ -1212,8 +1213,11 @@ const SkillCardRenderer: React.FC<{
|
||||
}, [skillName]);
|
||||
|
||||
const handleAddToSkills = useCallback(() => {
|
||||
message.success(`技能 "${skillName}" 已添加到我的技能`);
|
||||
}, [skillName]);
|
||||
if (!isAdded) {
|
||||
setIsAdded(true);
|
||||
message.success(`技能 "${skillName}" 已添加到我的技能`);
|
||||
}
|
||||
}, [skillName, isAdded]);
|
||||
|
||||
const displayName = detailData?.metadata?.name || detailData?.skill_name || skillName;
|
||||
const description = detailData?.metadata?.description || '';
|
||||
@@ -1271,11 +1275,15 @@ const SkillCardRenderer: React.FC<{
|
||||
<Button
|
||||
type='primary'
|
||||
size='small'
|
||||
className='!bg-gray-900 !border-gray-900 dark:!bg-gray-100 dark:!border-gray-100 dark:!text-gray-900 !text-white !rounded-lg !text-xs !font-medium !px-3'
|
||||
icon={<PlusOutlined className='text-[10px]' />}
|
||||
className={`!rounded-lg !text-xs !font-medium !px-3 ${
|
||||
isAdded
|
||||
? '!bg-green-50 !text-green-600 !border-green-200 dark:!bg-green-900/20 dark:!text-green-500 dark:!border-green-800'
|
||||
: '!bg-gray-900 !border-gray-900 dark:!bg-gray-100 dark:!border-gray-100 dark:!text-gray-900 !text-white'
|
||||
}`}
|
||||
icon={isAdded ? <CheckOutlined className='text-[10px]' /> : <PlusOutlined className='text-[10px]' />}
|
||||
onClick={handleAddToSkills}
|
||||
>
|
||||
添加到我的技能
|
||||
{isAdded ? '已添加' : '添加到我的技能'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1335,11 +1343,15 @@ const SkillCardRenderer: React.FC<{
|
||||
<Button
|
||||
type='primary'
|
||||
size='small'
|
||||
className='!bg-gray-900 !border-gray-900 dark:!bg-gray-100 dark:!border-gray-100 dark:!text-gray-900 !text-white !rounded-lg !text-xs !font-medium !px-3'
|
||||
icon={<PlusOutlined className='text-[10px]' />}
|
||||
className={`!rounded-lg !text-xs !font-medium !px-3 ${
|
||||
isAdded
|
||||
? '!bg-green-50 !text-green-600 !border-green-200 dark:!bg-green-900/20 dark:!text-green-500 dark:!border-green-800'
|
||||
: '!bg-gray-900 !border-gray-900 dark:!bg-gray-100 dark:!border-gray-100 dark:!text-gray-900 !text-white'
|
||||
}`}
|
||||
icon={isAdded ? <CheckOutlined className='text-[10px]' /> : <PlusOutlined className='text-[10px]' />}
|
||||
onClick={handleAddToSkills}
|
||||
>
|
||||
添加到我的技能
|
||||
{isAdded ? '已添加' : '添加到我的技能'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1355,37 +1367,56 @@ const SkillCardRenderer: React.FC<{
|
||||
<div className='flex-1 min-w-0 overflow-auto p-4'>
|
||||
{fileContent ? (
|
||||
<div className='prose prose-sm dark:prose-invert max-w-none'>
|
||||
{fileContent.startsWith('---') ?
|
||||
(() => {
|
||||
const parts = fileContent.split('---');
|
||||
if (parts.length >= 3) {
|
||||
return (
|
||||
<>
|
||||
<pre className='text-xs bg-gray-50 dark:bg-[#161719] rounded-lg px-4 py-3 text-gray-600 dark:text-gray-300 font-mono leading-relaxed mb-4 border border-gray-200 dark:border-gray-700'>
|
||||
{parts[1].trim()}
|
||||
</pre>
|
||||
<MarkDownContext>{preprocessLaTeX(parts.slice(2).join('---').trim())}</MarkDownContext>
|
||||
</>
|
||||
);
|
||||
}
|
||||
return <MarkDownContext>{preprocessLaTeX(fileContent)}</MarkDownContext>;
|
||||
})()
|
||||
: (() => {
|
||||
const ext = selectedFile?.split('.').pop()?.toLowerCase();
|
||||
const langMap: Record<string, string> = {
|
||||
py: 'python', sh: 'bash', bash: 'bash', zsh: 'bash',
|
||||
js: 'javascript', ts: 'typescript', jsx: 'javascript', tsx: 'typescript',
|
||||
json: 'json', yaml: 'yaml', yml: 'yaml', toml: 'toml',
|
||||
sql: 'sql', md: 'markdown', html: 'html', css: 'css',
|
||||
xml: 'xml', java: 'java', go: 'go', rs: 'rust', rb: 'ruby',
|
||||
c: 'c', cpp: 'cpp', h: 'c', hpp: 'cpp',
|
||||
};
|
||||
const lang = ext ? langMap[ext] : undefined;
|
||||
if (lang) {
|
||||
return <CodePreview code={fileContent} language={lang} />;
|
||||
}
|
||||
return <MarkDownContext>{preprocessLaTeX(fileContent)}</MarkDownContext>;
|
||||
})()}
|
||||
{fileContent.startsWith('---')
|
||||
? (() => {
|
||||
const parts = fileContent.split('---');
|
||||
if (parts.length >= 3) {
|
||||
return (
|
||||
<>
|
||||
<pre className='text-xs bg-gray-50 dark:bg-[#161719] rounded-lg px-4 py-3 text-gray-600 dark:text-gray-300 font-mono leading-relaxed mb-4 border border-gray-200 dark:border-gray-700'>
|
||||
{parts[1].trim()}
|
||||
</pre>
|
||||
<MarkDownContext>{preprocessLaTeX(parts.slice(2).join('---').trim())}</MarkDownContext>
|
||||
</>
|
||||
);
|
||||
}
|
||||
return <MarkDownContext>{preprocessLaTeX(fileContent)}</MarkDownContext>;
|
||||
})()
|
||||
: (() => {
|
||||
const ext = selectedFile?.split('.').pop()?.toLowerCase();
|
||||
const langMap: Record<string, string> = {
|
||||
py: 'python',
|
||||
sh: 'bash',
|
||||
bash: 'bash',
|
||||
zsh: 'bash',
|
||||
js: 'javascript',
|
||||
ts: 'typescript',
|
||||
jsx: 'javascript',
|
||||
tsx: 'typescript',
|
||||
json: 'json',
|
||||
yaml: 'yaml',
|
||||
yml: 'yaml',
|
||||
toml: 'toml',
|
||||
sql: 'sql',
|
||||
md: 'markdown',
|
||||
html: 'html',
|
||||
css: 'css',
|
||||
xml: 'xml',
|
||||
java: 'java',
|
||||
go: 'go',
|
||||
rs: 'rust',
|
||||
rb: 'ruby',
|
||||
c: 'c',
|
||||
cpp: 'cpp',
|
||||
h: 'c',
|
||||
hpp: 'cpp',
|
||||
};
|
||||
const lang = ext ? langMap[ext] : undefined;
|
||||
if (lang) {
|
||||
return <CodePreview code={fileContent} language={lang} />;
|
||||
}
|
||||
return <MarkDownContext>{preprocessLaTeX(fileContent)}</MarkDownContext>;
|
||||
})()}
|
||||
</div>
|
||||
) : (
|
||||
<div className='flex flex-col items-center justify-center py-12 text-gray-400'>
|
||||
|
||||
@@ -302,12 +302,16 @@ const ToolPartDisplay: React.FC<ToolPartDisplayProps> = ({ part, defaultOpen = f
|
||||
<ToolIcon name='brain' size='medium' className='text-indigo-500' />
|
||||
</div>
|
||||
<div className='min-w-0 flex-1'>
|
||||
<div className='text-sm font-semibold text-gray-800 dark:text-gray-200 truncate'>{skillInfo.name}</div>
|
||||
<div className='text-sm font-semibold text-gray-800 dark:text-gray-200 truncate'>
|
||||
{skillInfo.name}
|
||||
</div>
|
||||
<div className='text-[11px] text-gray-400 dark:text-gray-500'>Skill</div>
|
||||
</div>
|
||||
</div>
|
||||
{skillInfo.description && (
|
||||
<p className='text-sm text-gray-600 dark:text-gray-400 leading-relaxed mt-2'>{skillInfo.description}</p>
|
||||
<p className='text-sm text-gray-600 dark:text-gray-400 leading-relaxed mt-2'>
|
||||
{skillInfo.description}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -45,7 +45,9 @@ const ThinkingProcess: React.FC<ThinkingProcessProps> = ({
|
||||
if (!content) return null;
|
||||
|
||||
return (
|
||||
<div className={classNames('text-sm text-gray-700 dark:text-gray-300 leading-relaxed whitespace-pre-wrap', className)}>
|
||||
<div
|
||||
className={classNames('text-sm text-gray-700 dark:text-gray-300 leading-relaxed whitespace-pre-wrap', className)}
|
||||
>
|
||||
{displayedText}
|
||||
{!isComplete && <span className='inline-block w-1.5 h-4 ml-1 bg-blue-500 animate-pulse' />}
|
||||
</div>
|
||||
|
||||
@@ -485,6 +485,18 @@ const EXAMPLE_CARDS = [
|
||||
iconBg: 'bg-violet-100 dark:bg-violet-900/40',
|
||||
skillName: 'financial-report-analyzer',
|
||||
},
|
||||
{
|
||||
id: 'create_sql_skill',
|
||||
icon: '🛠️',
|
||||
title: '创建SQL分析技能',
|
||||
description: '使用skill-creator创建一个实用的SQL数据分析技能',
|
||||
query:
|
||||
'请使用 skill-creator 帮我创建一个实用的SQL数据分析技能,包含连接数据库、执行SQL查询和数据可视化等核心功能。',
|
||||
color: 'from-amber-500/10 to-orange-500/10',
|
||||
borderColor: 'border-amber-200/60 dark:border-amber-800/40',
|
||||
iconBg: 'bg-amber-100 dark:bg-amber-900/40',
|
||||
skillName: 'skill-creator',
|
||||
},
|
||||
];
|
||||
|
||||
const Playground: NextPage = () => {
|
||||
@@ -1843,36 +1855,43 @@ const Playground: NextPage = () => {
|
||||
try {
|
||||
message.loading({ content: '正在加载示例...', key: 'example-loading', duration: 0 });
|
||||
|
||||
const res = await axios.post(`${process.env.API_BASE_URL ?? ''}/api/v1/examples/use`, {
|
||||
example_id: example.id,
|
||||
});
|
||||
let filePath: string | null = null;
|
||||
let fakeFile: File | null = null;
|
||||
|
||||
// If example has a file, request it from backend
|
||||
if (example.fileName) {
|
||||
const res = await axios.post(`${process.env.API_BASE_URL ?? ''}/api/v1/examples/use`, {
|
||||
example_id: example.id,
|
||||
});
|
||||
|
||||
if (res?.success && res?.data) {
|
||||
filePath = res.data;
|
||||
preloadedFilePathRef.current = filePath;
|
||||
fakeFile = new File([new ArrayBuffer(example.fileSize || 0)], example.fileName, {
|
||||
type: example.fileType,
|
||||
});
|
||||
setUploadedFile(fakeFile);
|
||||
} else {
|
||||
message.destroy('example-loading');
|
||||
const errMsg = res?.err_msg || 'Unknown error';
|
||||
message.error('加载示例失败: ' + errMsg);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
message.destroy('example-loading');
|
||||
|
||||
if (res?.success && res?.data) {
|
||||
const filePath = res.data;
|
||||
preloadedFilePathRef.current = filePath;
|
||||
|
||||
const fakeFile = new File([new ArrayBuffer(example.fileSize || 0)], example.fileName, {
|
||||
type: example.fileType,
|
||||
});
|
||||
setUploadedFile(fakeFile);
|
||||
|
||||
// Auto-select skill if example specifies one
|
||||
let exampleSkill: Skill | null = null;
|
||||
if (example.skillName && skillsList) {
|
||||
const matched = skillsList.find(s => s.name === example.skillName);
|
||||
if (matched) {
|
||||
exampleSkill = matched;
|
||||
setSelectedSkill(matched);
|
||||
}
|
||||
// Auto-select skill if example specifies one
|
||||
let exampleSkill: Skill | null = null;
|
||||
if (example.skillName && skillsList) {
|
||||
const matched = skillsList.find(s => s.name === example.skillName);
|
||||
if (matched) {
|
||||
exampleSkill = matched;
|
||||
setSelectedSkill(matched);
|
||||
}
|
||||
|
||||
handleStart(example.query, fakeFile, exampleSkill);
|
||||
} else {
|
||||
const errMsg = res?.err_msg || 'Unknown error';
|
||||
message.error('加载示例失败: ' + errMsg);
|
||||
}
|
||||
|
||||
handleStart(example.query, fakeFile, exampleSkill);
|
||||
} catch (err: unknown) {
|
||||
message.destroy('example-loading');
|
||||
console.error('Example click error:', err);
|
||||
|
||||
Reference in New Issue
Block a user