feat: add skils

This commit is contained in:
李如威 2025-12-26 17:06:45 +08:00
parent 5ae6ed73a5
commit 6a21effe69
7 changed files with 174 additions and 2 deletions

View File

@ -1,4 +1,4 @@
#!/usr/bin/env bash #!/usr/bin/env bash
export $(cat .env | xargs) export $(cat .env | xargs)
pytest -s -W ignore::DeprecationWarning src/tests/test_nodes.py::test_rerank pytest -s -W ignore::DeprecationWarning src/tests/test_utils.py::test_skills

View File

@ -7,6 +7,9 @@ from src.pipeline.core import llm
from src.pipeline.core import es from src.pipeline.core import es
from itertools import chain from itertools import chain
# -----------------------------
# RAG 相关
# -----------------------------
class ChunkDocumentsNode(AsyncBatchNode): class ChunkDocumentsNode(AsyncBatchNode):
async def prep_async(self, shared): async def prep_async(self, shared):
@ -215,3 +218,8 @@ class RerankNode(AsyncBatchNode):
results.sort(key=lambda x: x["rerank_score"], reverse=True) results.sort(key=lambda x: x["rerank_score"], reverse=True)
shared["results"] = results shared["results"] = results
return "default" return "default"
# -----------------------------
# Agent 相关
# -----------------------------

View File

@ -0,0 +1,63 @@
import importlib.util
import aiofiles
import os
import inspect
import asyncio
import yaml
from dataclasses import dataclass
@dataclass
class Skill:
name: str
desc: str
run: callable
async def _load_single_skill(path: str, folder: str) -> Skill | None:
md = os.path.join(path, folder, "SKILL.md")
py = os.path.join(path, folder, "run.py")
if not (os.path.isfile(md) and os.path.isfile(py)):
return None
# async 读 SKILL.md
content = ""
async with aiofiles.open(md, "r", encoding="utf-8") as f:
content = await f.read()
frontmatter = content.split("---")[1]
meta = yaml.safe_load(frontmatter)
name = meta.get("name", "")
desc = meta.get("description", "")
# 动态加载 run.py这一步只能 sync
spec = importlib.util.spec_from_file_location(f"{folder}_skill", py)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
run = module.run
# 统一封装sync → async
if not inspect.iscoroutinefunction(run):
async def async_run(*args, **kwargs):
return run(*args, **kwargs)
run = async_run
return Skill(name=name, desc=desc, run=run)
async def load_skills(path="./skills") -> list[Skill]:
skills = []
folders = [f for f in os.listdir(path) if os.path.isdir(os.path.join(path, f))]
tasks = [_load_single_skill(path, folder) for folder in folders]
results = await asyncio.gather(*tasks)
for skill in results:
if skill:
skills.append(skill)
return skills

View File

@ -193,7 +193,6 @@ def rag_user_prompt(query: str, documents: list[dict]) -> str:
# 其他工具 # 其他工具
# ----------------------------- # -----------------------------
async def baidu_search_async(query: str, max_results: int = 5): async def baidu_search_async(query: str, max_results: int = 5):
""" """
异步调用 baidusearch内部用 asyncio.to_thread 封装同步函数 异步调用 baidusearch内部用 asyncio.to_thread 封装同步函数

View File

@ -0,0 +1,71 @@
---
name: baidu-search
description: >
当用户问题需要查询互联网公开信息、技术概念解释、
新闻动态、人物或项目信息,且本地知识库无法覆盖时使用。
本技能用于指导如何通过百度搜索获取信息并整理成答案。
allowed_tools: [baidu_search]
tags: [search, web, baidu]
---
# 百度搜索技能
## 技能目标
通过互联网搜索获取公开信息,
并将多个搜索结果整理为可信、简洁的自然语言回答。
---
## 适用场景
- 查询某个概念 / 技术 / 框架是什么
- 了解项目、公司、人物的背景信息
- 需要获取较新的公开资料
- 本地知识库未命中或信息不足
---
## 工作流程SOP
### 1. 分析用户意图
判断问题属于:
- 概念解释
- 背景介绍
- 现状 / 进展
- 对比或事实查询
---
### 2. 构造搜索关键词
- 使用简洁、明确的中文关键词
- 避免过长句子
- 必要时添加限定词(如:官网、介绍、教程)
---
### 3. 获取搜索结果
- 优先关注权威来源
- 多条结果进行交叉参考
---
### 4. 信息整理
- 去除广告和无关内容
- 合并重复信息
- 提炼核心要点
---
### 5. 生成最终回答
- 使用自然语言总结
- 不暴露搜索过程
- 直接回答用户问题
---
## 注意事项
- 搜索结果可能存在噪声,应谨慎判断
- 若信息不充分,应在回答中说明不确定性
- 不需要解释搜索工具或接口细节

View File

@ -0,0 +1,22 @@
import re
import asyncio
from baidusearch.baidusearch import search
async def run(query: str, max_results: int = 5):
def sync_search():
return list(search(query, num_results=max_results))
results = await asyncio.to_thread(sync_search)
docs = []
for r in results:
docs.append(
{
"title": r.get("title"),
"content": re.sub(r"\s+", " ", r.get("abstract", "")),
"url": r.get("url"),
"type": "web",
"score": 0.5,
}
)
return docs

View File

@ -1,6 +1,7 @@
import pytest import pytest
import json import json
from src.pipeline.core.utils import logger, baidu_search_async from src.pipeline.core.utils import logger, baidu_search_async
from src.pipeline.core.skills import load_skills
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_search_web(): async def test_search_web():
@ -8,3 +9,11 @@ async def test_search_web():
logger.debug(f"query: {query} ...") logger.debug(f"query: {query} ...")
results = await baidu_search_async(query, max_results=5) results = await baidu_search_async(query, max_results=5)
logger.debug(json.dumps(results, indent=4, ensure_ascii=False)) logger.debug(json.dumps(results, indent=4, ensure_ascii=False))
@pytest.mark.asyncio
async def test_skills():
skills = await load_skills("./src/skills")
logger.debug("Loaded skills")
for s in skills:
logger.debug(f"\n- {s.name}: {s.desc}")