From 15db4e2e1a1d012022548b29d9a1182006c06da6 Mon Sep 17 00:00:00 2001 From: ymq Date: Fri, 4 Sep 2026 15:58:14 +0800 Subject: [PATCH] =?UTF-8?q?feat(api):=20kb=5Flist=E7=AB=AF=E7=82=B9(?= =?UTF-8?q?=E5=8F=AF=E8=A7=81=E7=9F=A5=E8=AF=86=E5=BA=93)+=E5=88=A0?= =?UTF-8?q?=E9=99=A4tools.py=E7=9B=B4=E8=B0=83=E9=80=9A=E9=81=93(=E7=BB=9F?= =?UTF-8?q?=E4=B8=80API=E6=A8=A1=E5=BC=8F)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- rag/api_core.py | 29 ++++++ rag/tools.py | 186 --------------------------------------- scripts/load_path.py | 1 + wwwroot/api/README.md | 13 +-- wwwroot/api/kb_list.dspy | 7 ++ 5 files changed, 44 insertions(+), 192 deletions(-) delete mode 100644 rag/tools.py create mode 100644 wwwroot/api/kb_list.dspy diff --git a/rag/api_core.py b/rag/api_core.py index 1c36713..f7c6181 100644 --- a/rag/api_core.py +++ b/rag/api_core.py @@ -506,3 +506,32 @@ async def search(env, ns): } for h in enriched] return _ok(results=results, total=len(results), recall=len(all_hits), kbs_searched=len(kb_ids)) + + +async def kb_list(env, ns): + """列出调用者(按会话/Key 身份)可见的知识库:机构隔离 + search_roles 过滤。 + + 与检索同一权限解析(_resolve_search_kbs 缺省返回全部可见库),供宿主 + Agent 在检索/建议入库前先枚举可用知识库。 + """ + from rag.init import _resolve_search_kbs + org_id = await env.get_userorgid() + kb_ids = await _resolve_search_kbs(env, org_id, "") + if not kb_ids: + return _ok(kbs=[], total=0) + async with get_sor_context(env, 'rag') as sor: + nsmap = {("k%d" % i): k for i, k in enumerate(kb_ids)} + placeholders = ",".join("${" + k + "}$" for k in nsmap) + recs = await sor.sqlExe( + "SELECT id, name, description, embedding_engine, doc_count, status " + "FROM rag_knowledge_bases WHERE id IN (" + placeholders + ") " + "ORDER BY created_at DESC", nsmap) + await sor.sqlExe("COMMIT", {}) + kbs = [{ + "id": r.id, "name": getattr(r, 'name', '') or '', + "description": getattr(r, 'description', '') or '', + "embedding_engine": getattr(r, 'embedding_engine', '') or '', + "doc_count": int(getattr(r, 'doc_count', 0) or 0), + "status": getattr(r, 'status', '') or '', + } for r in (recs or [])] + return _ok(kbs=kbs, total=len(kbs)) diff --git a/rag/tools.py b/rag/tools.py deleted file mode 100644 index 053af01..0000000 --- a/rag/tools.py +++ /dev/null @@ -1,186 +0,0 @@ -# -*- coding:utf-8 -*- -"""RAG tools — 与对外 API 相同功能的函数包装,供内部助手(agent)调用。 - -对外 API(wwwroot/api/*.dspy → api_core.py)是给其他系统的 HTTP 机器接口; -内部助手(本进程/同生态的 LLM agent)不必绕 HTTP + API Key,直接调本模块: - - from rag.tools import RAG_TOOL_SCHEMAS, exec_rag_tool - # 助手注册工具时注入 schema;执行时: - result = await exec_rag_tool("rag_search", {"query": "...", "kb_id": "..."}, org_id=my_org) - -约定: -- schema 为 OpenAI function-calling 格式(RAG_TOOL_SCHEMAS 列表); -- exec_rag_tool 返回 dict:成功 {"status":"ok","data":{...}},失败 {"status":"error","message":...,"data":null} - —— 与对外 API 的 JSON 完全同构,助手侧处理逻辑可共用; -- org 隔离与 api_core 一致:以传入的 org_id 为身份边界(内部助手由宿主注入可信 org,不走 key); -- rag_doc_upload 与 HTTP 版的差异:HTTP 收原始字节;tools 收 file_path(服务端可读路径)或 file_base64。 - -2026-09-03 新增(配合对外 API,避免"API 一套、助手又写一套"的分叉)。 -""" -import base64 -import json -import os - -from rag import api_core as C - - -# ────────────────────────── OpenAI tool schema ────────────────────────── - -RAG_TOOL_SCHEMAS = [ - { - "type": "function", - "function": { - "name": "rag_kb_create", - "description": "创建知识库。embedding_engine 创建时定死:bge-m3(纯文本) / clip-vith14(多媒体) / qwen3-vl-embedding(多模态在线)。", - "parameters": { - "type": "object", - "properties": { - "name": {"type": "string", "description": "知识库名称"}, - "description": {"type": "string", "description": "描述(可选)"}, - "embedding_engine": {"type": "string", "enum": ["bge-m3", "clip-vith14", "qwen3-vl-embedding"], "description": "向量引擎,默认 bge-m3"}, - }, - "required": ["name"], - }, - }, - }, - { - "type": "function", - "function": { - "name": "rag_kb_delete", - "description": "删除知识库(级联清理向量、分块、文件,不可恢复)。", - "parameters": { - "type": "object", - "properties": {"kb_id": {"type": "string", "description": "知识库ID"}}, - "required": ["kb_id"], - }, - }, - }, - { - "type": "function", - "function": { - "name": "rag_doc_upload", - "description": "上传文件入库(异步:返回后后台解析+向量化,可稍后用 rag_search 验证)。file_path 与 file_base64 二选一。", - "parameters": { - "type": "object", - "properties": { - "kb_id": {"type": "string", "description": "目标知识库ID"}, - "file_name": {"type": "string", "description": "文件名(含扩展名)"}, - "file_path": {"type": "string", "description": "服务端可读的文件绝对路径(与 file_base64 二选一)"}, - "file_base64": {"type": "string", "description": "文件内容 base64(与 file_path 二选一)"}, - }, - "required": ["kb_id", "file_name"], - }, - }, - }, - { - "type": "function", - "function": { - "name": "rag_doc_delete", - "description": "删除文档(级联清理向量、分块、磁盘文件)。", - "parameters": { - "type": "object", - "properties": {"doc_id": {"type": "string", "description": "文档ID"}}, - "required": ["doc_id"], - }, - }, - }, - { - "type": "function", - "function": { - "name": "rag_tag_create", - "description": "创建标签(同名幂等,已存在则返回原标签)。", - "parameters": { - "type": "object", - "properties": { - "kb_id": {"type": "string", "description": "知识库ID"}, - "name": {"type": "string", "description": "标签名"}, - "color": {"type": "string", "description": "颜色 #rrggbb(可选)"}, - }, - "required": ["kb_id", "name"], - }, - }, - }, - { - "type": "function", - "function": { - "name": "rag_doc_set_tags", - "description": "给文档设置标签(全量语义:传入的集合即最终标签,未传的解绑;空集合=清空)。标签名不存在时自动创建。", - "parameters": { - "type": "object", - "properties": { - "kb_id": {"type": "string", "description": "知识库ID"}, - "doc_id": {"type": "string", "description": "文档ID"}, - "tags": {"type": "array", "items": {"type": "string"}, "description": "标签名列表"}, - }, - "required": ["kb_id", "doc_id"], - }, - }, - }, - { - "type": "function", - "function": { - "name": "rag_search", - "description": "知识库检索(向量召回+重排)。不传 kb_id 则检索本机构全部知识库。", - "parameters": { - "type": "object", - "properties": { - "query": {"type": "string", "description": "检索问题/关键词"}, - "kb_id": {"type": "string", "description": "限定知识库ID(可选)"}, - "top_k": {"type": "integer", "description": "返回条数,默认10"}, - }, - "required": ["query"], - }, - }, - }, -] - -RAG_TOOL_NAMES = [t["function"]["name"] for t in RAG_TOOL_SCHEMAS] - - -# ────────────────────────── 分发执行 ────────────────────────── - -async def exec_rag_tool(tool_name: str, params: dict, org_id: str) -> dict: - """执行一个 rag tool。params 与 schema 对齐;org_id 为调用方可操作机构(宿主注入)。 - - 返回 dict:{"status":"ok","data":{...}} 或 {"status":"error","message":...,"data":null} - """ - if tool_name not in RAG_TOOL_NAMES: - return {"status": "error", "message": f"unknown rag tool: {tool_name}", "data": None} - if not org_id: - return {"status": "error", "message": "org_id required", "data": None} - ns = dict(params or {}) - env = C.make_api_env({"org_id": org_id, "user_id": ns.pop("_user_id", "") or ""}) - - if tool_name == "rag_doc_upload": - file_data = None - if ns.get("file_path"): - try: - with open(ns["file_path"], "rb") as f: - file_data = f.read() - except Exception as e: - return {"status": "error", "message": f"read file_path failed: {e}", "data": None} - elif ns.get("file_base64"): - try: - file_data = base64.b64decode(ns["file_base64"]) - except Exception as e: - return {"status": "error", "message": f"base64 decode failed: {e}", "data": None} - else: - return {"status": "error", "message": "file_path or file_base64 required", "data": None} - ns.pop("file_path", None) - ns.pop("file_base64", None) - raw = await C.doc_upload(env, ns, file_data, ns.get("file_name", "upload.bin")) - else: - fn = { - "rag_kb_create": C.kb_create, - "rag_kb_delete": C.kb_delete, - "rag_doc_delete": C.doc_delete, - "rag_tag_create": C.tag_create, - "rag_doc_set_tags": C.doc_set_tags, - "rag_search": C.search, - }[tool_name] - raw = await fn(env, ns) - - try: - return json.loads(raw) - except Exception: - return {"status": "error", "message": f"tool result not JSON: {str(raw)[:200]}", "data": None} diff --git a/scripts/load_path.py b/scripts/load_path.py index f86119f..7aa4bb8 100644 --- a/scripts/load_path.py +++ b/scripts/load_path.py @@ -64,6 +64,7 @@ PATHS_LOGINED = [ f"/{MOD}/api/tag_create.dspy", f"/{MOD}/api/doc_set_tags.dspy", f"/{MOD}/api/search.dspy", + f"/{MOD}/api/kb_list.dspy", # CRUD 管理页 f"/{MOD}/documents_list/index.ui", f"/{MOD}/engine_configs_list/index.ui", diff --git a/wwwroot/api/README.md b/wwwroot/api/README.md index e71c8c7..98209c5 100644 --- a/wwwroot/api/README.md +++ b/wwwroot/api/README.md @@ -40,7 +40,8 @@ HTTP 状态码恒为 200(除鉴权失败 401),业务成败看 `status` 字 | `/rag/api/doc_delete.dspy` | `doc_id`* | `{doc_id, kb_id, chunks_deleted, file_removed}` | | `/rag/api/tag_create.dspy` | `kb_id`*, `name`*, `color?` | `{tag_id, name, color}`(同名幂等返回已有并带 `duplicate:true`) | | `/rag/api/doc_set_tags.dspy` | `kb_id`*, `doc_id`*, `tags`(名称列表) \| `tag_ids`(ID列表) | `{doc_id, added, removed, tags:[{id,name,color}]}`(全量语义,空数组=清空) | -| `/rag/api/search.dspy` | `query`*, `kb_id?`(缺省=本机构全部KB), `top_k?`(默认10), `recall_k?`(默认top_k*3) | `{results:[{chunk_id, text, score, kb_id, doc:{id,file_name,file_type,kb_id}}], total, recall, kbs_searched}` | +| `/rag/api/search.dspy` | `query`*, `kb_id?`(缺省=本机构全部知识库), `top_k?`(默认10), `recall_k?`(默认top_k*3) | `{results:[{chunk_id, text, score, kb_id, doc:{id,file_name,file_type,kb_id}}], total, recall, kbs_searched}` | +| `/rag/api/kb_list.dspy` | (无参数) | `{kbs:[{id, name, description, embedding_engine, doc_count, status}], total}`(调用者可见知识库:机构隔离+检索角色过滤) | `*` 为必填。缺失/非法一律返回 `{"status":"error","message":...}`,不会产生 500。 @@ -89,9 +90,9 @@ curl -s -X POST $BASE/kb_delete.dspy -H "Authorization: Bearer $KEY" \ - 依赖基础设施:embedding/rerank 在线 API 与 VDB 服务 (`upapp.rag-vdb` 的 baseurl)必须从部署机网络可达;不可达时 ingest/search 会明确报错而非假成功。 -## 内部助手(非 HTTP) +## 内部助手(宿主 Agent) -同一套能力封装为 OpenAI function-calling schema 供宿主 Agent 直调,不绕 HTTP: -见 `rag/tools.py`(`rag_kb_create` 等 7 个)。宿主 env 需具备 -`db` / `get_user` / `get_userorgid` / `password_encode` / `get_module_dbname`; -org 由宿主注入,与 API 通道的机构隔离语义一致。 +不提供同进程直调通道——宿主 Agent(如产线平台)一律走 HTTP API(Bearer key): +见 `wwwroot/api/README.md` 与 `rag/api_core.py`。key 由平台 dapi 模块按用户发放, +检索范围受用户机构 + 知识库 `search_roles` 约束。 +(2026-09-04 原 `rag/tools.py` 直调通道已删除:未投产,且与「接口统一 API 模式」冲突。) diff --git a/wwwroot/api/kb_list.dspy b/wwwroot/api/kb_list.dspy new file mode 100644 index 0000000..6395af3 --- /dev/null +++ b/wwwroot/api/kb_list.dspy @@ -0,0 +1,7 @@ +# 对外 API:列出可见知识库(机构隔离 + search_roles 过滤,与检索同一权限解析) +# POST /rag/api/kb_list.dspy 认证:cookie 会话或 Authorization: Bearer *** key>,RBAC logined 把关 +# body/query: {}(无参数) +from rag import api_core as C +_ns = await C.read_json_body(request, params_kw) +env = C.session_env(request) +return await C.kb_list(env, _ns)