- pyproject 注册 nonebot_plugin_wtfllm 插件 - 新增 wtfllm_gate 插件:仅在群 621016172 启用 wtfllm 响应 - 新增 kb_build:抓取 danding.vip 文档并导入 wtfllm 知识库的脚本 - requirements: 升级 nonebot2 2.5.0 / pydantic 2.13.5,补 aiohttp 与 wtfllm 依赖
139 lines
4.4 KiB
Python
139 lines
4.4 KiB
Python
"""把 kb_build/pages/*.txt 的站点文档写入 nonebot-plugin-wtfllm 知识库。
|
|
|
|
用法:
|
|
python kb_build/import_kb.py --agent-id <机器人QQ号>
|
|
|
|
首次运行会自动下载 Qdrant 与 embedding 模型,需要几分钟。
|
|
"""
|
|
import argparse
|
|
import asyncio
|
|
import re
|
|
import sys
|
|
import time
|
|
import uuid
|
|
from pathlib import Path
|
|
|
|
# NoneBot 必须先初始化,wtfllm 的配置才可用
|
|
import nonebot
|
|
|
|
nonebot.init()
|
|
|
|
sys.path.insert(0, str(Path(__file__).parent.parent))
|
|
|
|
PAGES_DIR = Path(__file__).parent / "pages"
|
|
|
|
# 每条知识的目标长度(字符),按标题切块后超过再按段落切
|
|
CHUNK_SIZE = 600
|
|
|
|
CATEGORY_BY_DIR = {
|
|
"功能帮助": "功能帮助",
|
|
"脚本进阶": "脚本进阶",
|
|
"简单使用": "简单使用",
|
|
"其它": "其它",
|
|
}
|
|
|
|
|
|
def parse_page(path: Path) -> list[dict]:
|
|
"""把单页文本按标题切成知识条目。"""
|
|
raw = path.read_text(encoding="utf-8")
|
|
lines = raw.splitlines()
|
|
page_title = lines[0].lstrip("# ").replace("\u200b", "") if lines else path.stem
|
|
source = lines[1].lstrip("> 来源: ").strip() if len(lines) > 1 else ""
|
|
|
|
parts: list[dict] = []
|
|
cur_head = page_title
|
|
cur_body: list[str] = []
|
|
|
|
def flush() -> None:
|
|
text = "\n".join(cur_body).strip()
|
|
if text:
|
|
parts.append({"title": cur_head, "content": text})
|
|
|
|
for line in lines[2:]:
|
|
m = re.match(r"^(#{1,4})\s+(.+)$", line)
|
|
if m:
|
|
flush()
|
|
cur_body = []
|
|
h = m.group(2).replace("\u200b", "").strip()
|
|
cur_head = h if h == page_title else f"{page_title} · {h}"
|
|
else:
|
|
cur_body.append(line)
|
|
flush()
|
|
|
|
# 超长条目按段落二次切分
|
|
chunks: list[dict] = []
|
|
for p in parts:
|
|
if len(p["content"]) <= CHUNK_SIZE * 1.5:
|
|
chunks.append(p)
|
|
continue
|
|
paras = p["content"].split("\n\n")
|
|
buf: list[str] = []
|
|
for para in paras:
|
|
if buf and sum(len(b) for b in buf) + len(para) > CHUNK_SIZE:
|
|
chunks.append({"title": p["title"], "content": "\n\n".join(buf)})
|
|
buf = []
|
|
buf.append(para)
|
|
if buf:
|
|
chunks.append({"title": p["title"], "content": "\n\n".join(buf)})
|
|
|
|
# 附上页面来源与分类
|
|
first_dir = path.stem.split("_")[0]
|
|
category = CATEGORY_BY_DIR.get(first_dir, "general")
|
|
for c in chunks:
|
|
c["category"] = category
|
|
c["source"] = source
|
|
c["page_title"] = page_title
|
|
return chunks
|
|
|
|
|
|
async def main(agent_id: str, dry_run: bool) -> None:
|
|
entries_spec: list[dict] = []
|
|
for f in sorted(PAGES_DIR.glob("*.txt")):
|
|
entries_spec.extend(parse_page(f))
|
|
print(f"共解析出 {len(entries_spec)} 条知识条目")
|
|
if dry_run:
|
|
for e in entries_spec[:10]:
|
|
print(f" [{e['category']}] {e['title']} ({len(e['content'])} chars)")
|
|
return
|
|
|
|
# 必须先以插件形式加载,让 localstore 能定位数据目录
|
|
nonebot.load_plugin("nonebot_plugin_wtfllm")
|
|
|
|
from nonebot_plugin_wtfllm.memory.items.knowledge_base import KnowledgeEntry
|
|
from nonebot_plugin_wtfllm.v_db.lifecycle import on_startup as vdb_startup
|
|
from nonebot_plugin_wtfllm.v_db.repositories import KnowledgeBaseRepository
|
|
|
|
print("初始化 Qdrant(首次会自动下载)...")
|
|
await vdb_startup()
|
|
|
|
now = int(time.time())
|
|
entries = [
|
|
KnowledgeEntry.create(
|
|
content=e["content"],
|
|
title=e["title"],
|
|
agent_id=agent_id,
|
|
category=e["category"],
|
|
tags=[e["page_title"], "danding.vip"],
|
|
token_count=len(e["content"]),
|
|
)
|
|
for e in entries_spec
|
|
]
|
|
repo = KnowledgeBaseRepository()
|
|
ids = await repo.save_many_knowledge(entries)
|
|
print(f"已写入 {len(ids)} 条知识")
|
|
|
|
# 验证检索
|
|
for q in ["怎么设置定时调度", "积分怎么获得", "下载安装"]:
|
|
results = await repo.search_relevant(agent_id=agent_id, query=q, limit=3)
|
|
print(f"\n检索「{q}」:")
|
|
for r in results:
|
|
print(f" {r.score:.3f} {r.item.title}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument("--agent-id", required=True, help="机器人 QQ 号(wtfllm 的 agent_id)")
|
|
parser.add_argument("--dry-run", action="store_true", help="只解析不写入")
|
|
args = parser.parse_args()
|
|
asyncio.run(main(args.agent_id, args.dry_run))
|