From 6a21effe69aee205d6f71e04740378904f5b8105 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E5=A6=82=E5=A8=81?= Date: Fri, 26 Dec 2025 17:06:45 +0800 Subject: [PATCH] feat: add skils --- scripts/run_test.sh | 2 +- src/pipeline/core/nodes.py | 8 ++++ src/pipeline/core/skills.py | 63 ++++++++++++++++++++++++++++ src/pipeline/core/utils.py | 1 - src/skills/search-baidu/SKILL.md | 71 ++++++++++++++++++++++++++++++++ src/skills/search-baidu/run.py | 22 ++++++++++ src/tests/test_utils.py | 9 ++++ 7 files changed, 174 insertions(+), 2 deletions(-) create mode 100644 src/pipeline/core/skills.py create mode 100644 src/skills/search-baidu/SKILL.md create mode 100644 src/skills/search-baidu/run.py diff --git a/scripts/run_test.sh b/scripts/run_test.sh index eecc2e2..ce8304c 100644 --- a/scripts/run_test.sh +++ b/scripts/run_test.sh @@ -1,4 +1,4 @@ #!/usr/bin/env bash 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 diff --git a/src/pipeline/core/nodes.py b/src/pipeline/core/nodes.py index 230a062..f48c778 100644 --- a/src/pipeline/core/nodes.py +++ b/src/pipeline/core/nodes.py @@ -7,6 +7,9 @@ from src.pipeline.core import llm from src.pipeline.core import es from itertools import chain +# ----------------------------- +# RAG 相关 +# ----------------------------- class ChunkDocumentsNode(AsyncBatchNode): async def prep_async(self, shared): @@ -215,3 +218,8 @@ class RerankNode(AsyncBatchNode): results.sort(key=lambda x: x["rerank_score"], reverse=True) shared["results"] = results return "default" + +# ----------------------------- +# Agent 相关 +# ----------------------------- + diff --git a/src/pipeline/core/skills.py b/src/pipeline/core/skills.py new file mode 100644 index 0000000..8ff4a33 --- /dev/null +++ b/src/pipeline/core/skills.py @@ -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 diff --git a/src/pipeline/core/utils.py b/src/pipeline/core/utils.py index 0319ad7..a1e6014 100644 --- a/src/pipeline/core/utils.py +++ b/src/pipeline/core/utils.py @@ -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): """ 异步调用 baidusearch(内部用 asyncio.to_thread 封装同步函数) diff --git a/src/skills/search-baidu/SKILL.md b/src/skills/search-baidu/SKILL.md new file mode 100644 index 0000000..379c739 --- /dev/null +++ b/src/skills/search-baidu/SKILL.md @@ -0,0 +1,71 @@ +--- +name: baidu-search +description: > + 当用户问题需要查询互联网公开信息、技术概念解释、 + 新闻动态、人物或项目信息,且本地知识库无法覆盖时使用。 + 本技能用于指导如何通过百度搜索获取信息并整理成答案。 +allowed_tools: [baidu_search] +tags: [search, web, baidu] +--- + +# 百度搜索技能 + +## 技能目标 + +通过互联网搜索获取公开信息, +并将多个搜索结果整理为可信、简洁的自然语言回答。 + +--- + +## 适用场景 + +- 查询某个概念 / 技术 / 框架是什么 +- 了解项目、公司、人物的背景信息 +- 需要获取较新的公开资料 +- 本地知识库未命中或信息不足 + +--- + +## 工作流程(SOP) + +### 1. 分析用户意图 +判断问题属于: +- 概念解释 +- 背景介绍 +- 现状 / 进展 +- 对比或事实查询 + +--- + +### 2. 构造搜索关键词 +- 使用简洁、明确的中文关键词 +- 避免过长句子 +- 必要时添加限定词(如:官网、介绍、教程) + +--- + +### 3. 获取搜索结果 +- 优先关注权威来源 +- 多条结果进行交叉参考 + +--- + +### 4. 信息整理 +- 去除广告和无关内容 +- 合并重复信息 +- 提炼核心要点 + +--- + +### 5. 生成最终回答 +- 使用自然语言总结 +- 不暴露搜索过程 +- 直接回答用户问题 + +--- + +## 注意事项 + +- 搜索结果可能存在噪声,应谨慎判断 +- 若信息不充分,应在回答中说明不确定性 +- 不需要解释搜索工具或接口细节 diff --git a/src/skills/search-baidu/run.py b/src/skills/search-baidu/run.py new file mode 100644 index 0000000..a31c2a7 --- /dev/null +++ b/src/skills/search-baidu/run.py @@ -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 diff --git a/src/tests/test_utils.py b/src/tests/test_utils.py index 3d5c104..6989836 100644 --- a/src/tests/test_utils.py +++ b/src/tests/test_utils.py @@ -1,6 +1,7 @@ import pytest import json from src.pipeline.core.utils import logger, baidu_search_async +from src.pipeline.core.skills import load_skills @pytest.mark.asyncio async def test_search_web(): @@ -8,3 +9,11 @@ async def test_search_web(): logger.debug(f"query: {query} ...") results = await baidu_search_async(query, max_results=5) 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}")