feat: add skils
This commit is contained in:
parent
5ae6ed73a5
commit
6a21effe69
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 相关
|
||||||
|
# -----------------------------
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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):
|
async def baidu_search_async(query: str, max_results: int = 5):
|
||||||
"""
|
"""
|
||||||
异步调用 baidusearch(内部用 asyncio.to_thread 封装同步函数)
|
异步调用 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 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}")
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue