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