Browse Source

feat: - 转移viking知识库的添加和删除操作到java代码上

zhangwl 1 day ago
parent
commit
19fc534de4
7 changed files with 4 additions and 476 deletions
  1. 0 7
      .env
  2. 0 5
      app/config/config.py
  3. 0 139
      app/core/viking_client.py
  4. 1 86
      app/db/mongo.py
  5. 0 234
      app/routers/knowledge_tool.py
  6. 3 3
      main.py
  7. 0 2
      requirements.txt

+ 0 - 7
.env

@@ -39,11 +39,4 @@ TENCENT_IM_SECRET_KEY=f8a4c34728e74d235da1ea2b1775a39c27787a4954e379de5ac424806e
 
 # Viking 知识库上传配置
 VIKING_API_KEY=W1D0N6X3GAQTFJXWAYAV09CPPMRZ8VP01YRXE028HTB2ZZPDBC4G60R30C9P6CVK8
-# 火山引擎-子账号-ak & sk
-VIKING_AK=AKLTNDE4YmE3MjU0MjE3NDIwYzgwZmNlZjA1MDRlNDNiNzM
-VIKING_SK=TlRaaU5HVXdNbVJoT0dNeU5EZ3pOVGs1Tm1KaU9UazVOekppTlRSbE0yVQ==
-
-VIKING_TOS_BUCKET=auto-knowledge-doc
-VIKING_TOS_ENDPOINT=tos-cn-beijing.volces.com
-VIKING_TOS_REGION=cn-beijing
 VIKING_HOST=api-knowledgebase.mlp.cn-beijing.volces.com

+ 0 - 5
app/config/config.py

@@ -21,9 +21,4 @@ class Config:
 
     # Viking 知识库上传配置
     VIKING_API_KEY: str = os.getenv("VIKING_API_KEY", "")
-    VIKING_AK: str = os.getenv("VIKING_AK", "")
-    VIKING_SK: str = os.getenv("VIKING_SK", "")
-    VIKING_TOS_BUCKET: str = os.getenv("VIKING_TOS_BUCKET", "auto-knowledge-doc")
-    VIKING_TOS_ENDPOINT: str = os.getenv("VIKING_TOS_ENDPOINT", "tos-cn-beijing.volces.com")
-    VIKING_TOS_REGION: str = os.getenv("VIKING_TOS_REGION", "cn-beijing")
     VIKING_HOST: str = os.getenv("VIKING_HOST", "api-knowledgebase.mlp.cn-beijing.volces.com")

+ 0 - 139
app/core/viking_client.py

@@ -19,116 +19,6 @@ cfg = Config()
 _executor = ThreadPoolExecutor(max_workers=4)
 
 
-def _tos_upload_sync(local_path: str, object_key: str) -> str:
-    """
-    同步上传本地文件到 TOS,返回 tos:// URI。
-    在线程池中调用,避免阻塞事件循环。
-    """
-    try:
-        import tos as tos_sdk
-    except ImportError:
-        raise RuntimeError("缺少 tos 依赖,请执行 pip install tos")
-
-    client = tos_sdk.TosClientV2(
-        ak=cfg.VIKING_AK,
-        sk=cfg.VIKING_SK,
-        endpoint=cfg.VIKING_TOS_ENDPOINT,
-        region=cfg.VIKING_TOS_REGION,
-    )
-    client.put_object_from_file(
-        bucket=cfg.VIKING_TOS_BUCKET,
-        key=object_key,
-        file_path=local_path,
-    )
-    return f"tos://{cfg.VIKING_TOS_BUCKET}/{object_key}"
-
-
-async def upload_to_tos(local_path: str, app_name: str, original_filename: str) -> tuple[str, str]:
-    """
-    异步上传本地文件到 TOS。
-    返回 (object_key, tos_uri)。
-    object_key 格式:{app_name}/{uuid32}_{原始文件名}
-    """
-    # 清理文件名中的特殊字符,只保留字母数字下划线中文和常见标点
-    safe_name = os.path.basename(original_filename)
-    uid = uuid.uuid4().hex
-    object_key = f"{app_name}/{uid}_{safe_name}"
-
-    loop = asyncio.get_event_loop()
-    tos_uri = await loop.run_in_executor(_executor, _tos_upload_sync, local_path, object_key)
-    return object_key, tos_uri
-
-
-async def add_doc_to_knowledge(
-    resource_id: str,
-    doc_id: str,
-    tos_uri: str,
-    doc_name: str,
-    doc_type: str,
-    tag_list: list | None = None,
-    path_segments: list | None = None,
-) -> dict:
-    """
-    调用 Viking /api/knowledge/doc/v2/add 接口,将 TOS 文件导入知识库。
-    使用 resource_id 指定知识库(对应 ai_config.knowledgeId)。
-    返回响应体 data 字段。
-    """
-    url = f"https://{cfg.VIKING_HOST}/api/knowledge/doc/v2/add"
-    payload: dict = {
-        "resource_id": resource_id,
-        "doc_id": doc_id,
-        "doc_name": doc_name,
-        "doc_type": doc_type,
-        "uri": tos_uri,
-    }
-    if tag_list:
-        payload["tag_list"] = tag_list
-    if path_segments:
-        payload["path_segments"] = path_segments
-
-    async with httpx.AsyncClient(timeout=30) as client:
-        resp = await client.post(
-            url,
-            json=payload,
-            headers={
-                "Content-Type": "application/json",
-                "Authorization": f"Bearer {cfg.VIKING_API_KEY}",
-            },
-        )
-        body = resp.json()
-        code = body.get("code", -1)
-        if code != 0:
-            raise ValueError(
-                f"Viking API 返回错误: {body.get('message')} "
-                f"(code={code}, request_id={body.get('request_id')})"
-            )
-        return body["data"]
-
-
-def _tos_delete_sync(object_key: str) -> None:
-    """
-    同步删除 TOS 对象。
-    在线程池中调用,避免阻塞事件循环。
-    """
-    try:
-        import tos as tos_sdk
-    except ImportError:
-        raise RuntimeError("缺少 tos 依赖,请执行 pip install tos")
-
-    client = tos_sdk.TosClientV2(
-        ak=cfg.VIKING_AK,
-        sk=cfg.VIKING_SK,
-        endpoint=cfg.VIKING_TOS_ENDPOINT,
-        region=cfg.VIKING_TOS_REGION,
-    )
-    client.delete_object(bucket=cfg.VIKING_TOS_BUCKET, key=object_key)
-
-
-async def delete_from_tos(object_key: str) -> None:
-    """异步删除 TOS 对象。"""
-    loop = asyncio.get_event_loop()
-    await loop.run_in_executor(_executor, _tos_delete_sync, object_key)
-
 
 def _search_knowledge_sync(resource_id: str, query: str, limit: int = 5) -> list:
     """
@@ -170,32 +60,3 @@ async def search_knowledge(resource_id: str, query: str, limit: int = 5) -> list
     loop = asyncio.get_event_loop()
     return await loop.run_in_executor(_executor, _search_knowledge_sync, resource_id, query, limit)
 
-
-async def delete_doc_from_knowledge(resource_id: str, doc_id: str) -> dict:
-    """
-    调用 Viking /api/knowledge/doc/delete 接口删除知识库文档。
-    返回完整响应体。
-    """
-    url = f"https://{cfg.VIKING_HOST}/api/knowledge/doc/delete"
-    payload = {
-        "resource_id": resource_id,
-        "doc_id": doc_id,
-    }
-
-    async with httpx.AsyncClient(timeout=30) as client:
-        resp = await client.post(
-            url,
-            json=payload,
-            headers={
-                "Content-Type": "application/json",
-                "Authorization": f"Bearer {cfg.VIKING_API_KEY}",
-            },
-        )
-        body = resp.json()
-        code = body.get("code", -1)
-        if code != 0:
-            raise ValueError(
-                f"Viking API 返回错误: {body.get('message')} "
-                f"(code={code}, request_id={body.get('request_id')})"
-            )
-        return body

+ 1 - 86
app/db/mongo.py

@@ -20,8 +20,7 @@ chat_history_col = db["chat_history"]
 circle_prompts = db["circle_prompt"]
 # 历史人物集合
 historical_figures = db["historical_figures"]
-# 知识库文档上传记录
-knowledge_docs = db["knowledge_docs"]
+
 
 def _ensure_index():
     try:
@@ -356,90 +355,6 @@ def get_group_ai_reply_count(session_id: str, ai_role: str = None) -> int:
         return 0
 
 
-
-# ==================== 知识库文档上传记录 ====================
-
-def _ensure_knowledge_docs_index():
-    try:
-        knowledge_docs.create_index([("app_name", 1), ("created_at", -1)])
-        knowledge_docs.create_index([("doc_id", 1)], unique=True)
-    except Exception:
-        pass
-
-
-def save_knowledge_doc(doc_data: dict) -> str | None:
-    """
-    保存知识库文档上传记录,返回插入的 ObjectId 字符串。
-    doc_data 应包含:doc_id, resource_id, collection_name, project,
-                     doc_name, doc_type, app_name
-    """
-    try:
-        _ensure_knowledge_docs_index()
-        doc_data["created_at"] = datetime.now()
-        result = knowledge_docs.insert_one(doc_data)
-        return str(result.inserted_id)
-    except Exception as e:
-        print(f"MongoDB 知识库文档记录写入失败: {e}")
-        return None
-
-
-def get_knowledge_docs(app_name: str, page: int = 1, page_size: int = 20) -> dict:
-    """
-    分页查询知识库文档上传记录。
-    返回 {"total": int, "page": int, "page_size": int, "list": [...]}
-    """
-    try:
-        _ensure_knowledge_docs_index()
-        query = {"app_name": app_name}
-        total = knowledge_docs.count_documents(query)
-        skip = (page - 1) * page_size
-        docs = list(
-            knowledge_docs.find(query, {"_id": 1, "doc_id": 1, "resource_id": 1,
-                                        "collection_name": 1, "project": 1,
-                                        "doc_name": 1, "doc_type": 1,
-                                        "app_name": 1, "created_at": 1})
-            .sort("created_at", -1)
-            .skip(skip)
-            .limit(page_size)
-        )
-        for doc in docs:
-            doc["id"] = str(doc.pop("_id"))
-            if "created_at" in doc:
-                doc["created_at"] = doc["created_at"].strftime("%Y-%m-%d %H:%M:%S")
-        return {"total": total, "page": page, "page_size": page_size, "list": docs}
-    except Exception as e:
-        print(f"MongoDB 知识库文档记录查询失败: {e}")
-        return {"total": 0, "page": page, "page_size": page_size, "list": []}
-
-
-def get_knowledge_doc_by_doc_id(doc_id: str) -> dict | None:
-    """
-    按 doc_id 查询单条知识库文档记录。
-    返回文档 dict(含 id 字符串字段),未找到返回 None。
-    """
-    try:
-        doc = knowledge_docs.find_one({"doc_id": doc_id})
-        if doc:
-            doc["id"] = str(doc.pop("_id"))
-        return doc
-    except Exception as e:
-        print(f"MongoDB 知识库文档记录查询失败: {e}")
-        return None
-
-
-def delete_knowledge_doc(doc_id: str) -> bool:
-    """
-    按 doc_id 删除知识库文档记录。
-    返回 True 表示成功删除,False 表示记录不存在或删除异常。
-    """
-    try:
-        result = knowledge_docs.delete_one({"doc_id": doc_id})
-        return result.deleted_count > 0
-    except Exception as e:
-        print(f"MongoDB 知识库文档记录删除失败: {e}")
-        return False
-
-
 # ==================== 用户长期记忆 ====================
 
 # facts 使用数组结构 [{key, value, updated_at}],避免动态字段名带来的 Mongo 注入风险

+ 0 - 234
app/routers/knowledge_tool.py

@@ -1,234 +0,0 @@
-"""
-知识库工具路由
-提供本地文档上传到 Viking 知识库的接口,无需 JWT 鉴权。
-接口:POST /main/knowledgeTool/upload
-"""
-import os
-import json
-import uuid
-import asyncio
-import tempfile
-from typing import Optional
-
-from fastapi import APIRouter, HTTPException, UploadFile, File, Form
-from pydantic import BaseModel
-
-from ..core.viking_client import upload_to_tos, add_doc_to_knowledge, delete_from_tos, delete_doc_from_knowledge
-from ..db.ai_config import get_config_by_app_name
-from ..db.mongo import save_knowledge_doc, get_knowledge_docs, get_knowledge_doc_by_doc_id, delete_knowledge_doc
-
-router = APIRouter()
-
-
-class KnowledgeDocsQuery(BaseModel):
-    app_name: str
-    page: int = 1
-    page_size: int = 20
-
-
-class KnowledgeDeleteRequest(BaseModel):
-    doc_id: str
-    app_name: str
-
-# 支持的文档类型:后缀 → Viking doc_type 值
-ALLOWED_DOC_EXTENSIONS: dict[str, str] = {
-    ".txt":      "txt",
-    ".doc":      "doc",
-    ".docx":     "docx",
-    ".pdf":      "pdf",
-    ".md":       "markdown",
-    ".markdown": "markdown",
-    ".pptx":     "pptx",
-    ".ppt":      "ppt",
-}
-
-# 单文件大小上限:50 MB
-MAX_DOC_SIZE = 50 * 1024 * 1024
-
-
-def _generate_doc_id() -> str:
-    """生成符合 Viking 命名规范的唯一 doc_id(字母开头,仅含字母/数字/下划线)"""
-    return f"doc_{uuid.uuid4().hex}"
-
-
-async def _cleanup_tos(object_key: str) -> None:
-    """上传成功后异步清理 TOS 临时文件,失败仅打印日志不影响主流程。"""
-    try:
-        await delete_from_tos(object_key)
-    except Exception as e:
-        print(f"TOS 临时文件清理失败(key={object_key}): {e}")
-
-
-@router.post("/upload", summary="上传本地文档到知识库")
-async def upload_knowledge_doc(
-    file: UploadFile = File(..., description="待上传文档,支持 txt/doc/docx/pdf/markdown/pptx/ppt"),
-    app_name: str = Form(..., description="应用包名,用于查询对应知识库配置"),
-    doc_id: Optional[str] = Form(None, description="文档唯一标识,不填则自动生成"),
-    doc_name: Optional[str] = Form(None, description="文档显示名称,不填则使用原始文件名"),
-    path_segments: Optional[str] = Form(None, description="目录路径 JSON 数组,如 [\"产品文档\",\"V2.0\"]"),
-    tag_list: Optional[str] = Form(None, description="标签列表 JSON 数组,格式与 Viking API 一致"),
-):
-    """
-    上传本地文档到火山引擎 Viking 知识库。
-
-    流程:
-    1. 校验文件类型和大小
-    2. 从 ai_config 查询 knowledgeId(作为 resource_id)
-    3. 上传文件到 TOS 对象存储
-    4. 调用 Viking /api/knowledge/doc/v2/add 导入知识库
-    5. 返回 doc_id 和知识库信息
-    """
-    # ── 1. 校验文件类型 ──────────────────────────────────────────
-    original_filename = file.filename or ""
-    _, ext = os.path.splitext(original_filename.lower())
-    if ext not in ALLOWED_DOC_EXTENSIONS:
-        allowed = "、".join(ALLOWED_DOC_EXTENSIONS.keys())
-        raise HTTPException(
-            status_code=400,
-            detail=f"不支持的文件类型 '{ext}',仅支持:{allowed}",
-        )
-    doc_type = ALLOWED_DOC_EXTENSIONS[ext]
-
-    # ── 2. 读取并校验文件大小 ────────────────────────────────────
-    file_bytes = await file.read()
-    if len(file_bytes) > MAX_DOC_SIZE:
-        raise HTTPException(
-            status_code=400,
-            detail=f"文件大小超过限制(最大 50 MB,当前 {len(file_bytes) / 1024 / 1024:.1f} MB)",
-        )
-    if len(file_bytes) == 0:
-        raise HTTPException(status_code=400, detail="文件内容为空")
-
-    # ── 3. 查询知识库配置 ────────────────────────────────────────
-    ai_config = get_config_by_app_name(app_name)
-    if not ai_config:
-        raise HTTPException(status_code=404, detail=f"未找到 appName '{app_name}' 的配置")
-
-    resource_id = ai_config.get("knowledgeId", "")
-    if not resource_id:
-        raise HTTPException(
-            status_code=400,
-            detail=f"appName '{app_name}' 的配置中未设置 knowledgeId",
-        )
-
-    # ── 4. 解析可选 JSON 参数 ────────────────────────────────────
-    # 强制携带 app_name 标签,前端额外传入的标签追加在后面
-    parsed_tag_list: list = [
-        {"field_name": "app_name", "field_type": "string", "field_value": app_name}
-    ]
-    if tag_list:
-        try:
-            extra_tags = json.loads(tag_list)
-            parsed_tag_list.extend(extra_tags)
-        except json.JSONDecodeError:
-            raise HTTPException(status_code=400, detail="tag_list 格式错误,需为合法的 JSON 数组")
-
-    parsed_path_segments: list | None = None
-    if path_segments:
-        try:
-            parsed_path_segments = json.loads(path_segments)
-        except json.JSONDecodeError:
-            raise HTTPException(status_code=400, detail="path_segments 格式错误,需为合法的 JSON 数组")
-
-    # ── 5. 确定 doc_id 和 doc_name ──────────────────────────────
-    final_doc_id = doc_id.strip() if doc_id else _generate_doc_id()
-    final_doc_name = doc_name.strip() if doc_name else original_filename
-
-    # ── 6. 写临时文件 ────────────────────────────────────────────
-    tmp_path = None
-    try:
-        suffix = ext  # 保留原后缀,TOS SDK 可能依赖文件名做 Content-Type 推断
-        with tempfile.NamedTemporaryFile(delete=False, suffix=suffix) as tmp:
-            tmp.write(file_bytes)
-            tmp_path = tmp.name
-
-        # ── 7. 上传到 TOS ────────────────────────────────────────
-        try:
-            object_key, tos_uri = await upload_to_tos(tmp_path, app_name, original_filename)
-        except RuntimeError as e:
-            raise HTTPException(status_code=500, detail=str(e))
-        except Exception as e:
-            raise HTTPException(status_code=500, detail=f"TOS 上传失败: {str(e)}")
-
-        # ── 8. 调用 Viking API 导入知识库 ───────────────────────
-        try:
-            kb_data = await add_doc_to_knowledge(
-                resource_id=resource_id,
-                doc_id=final_doc_id,
-                tos_uri=tos_uri,
-                doc_name=final_doc_name,
-                doc_type=doc_type,
-                tag_list=parsed_tag_list,
-                path_segments=parsed_path_segments,
-            )
-        except ValueError as e:
-            raise HTTPException(status_code=400, detail=str(e))
-        except Exception as e:
-            raise HTTPException(status_code=500, detail=f"导入知识库失败: {str(e)}")
-
-        # ── 9. 异步删除 TOS 临时文件(不阻塞响应)─────────────────
-        asyncio.create_task(_cleanup_tos(object_key))
-
-    finally:
-        # ── 10. 清理本地临时文件 ─────────────────────────────────
-        if tmp_path and os.path.exists(tmp_path):
-            os.unlink(tmp_path)
-
-    # ── 11. 构造返回结果并存储到 MongoDB ─────────────────────────
-    result = {
-        "doc_id": kb_data.get("doc_id", final_doc_id),
-        "resource_id": kb_data.get("resource_id", resource_id),
-        "collection_name": kb_data.get("collection_name", ""),
-        "project": kb_data.get("project", "default"),
-        "doc_name": final_doc_name,
-        "doc_type": doc_type,
-        "app_name": app_name,
-    }
-    inserted_id = save_knowledge_doc(result.copy())
-    result["id"] = inserted_id or ""
-
-    return result
-
-
-@router.post("/docs", summary="分页查询知识库文档上传记录")
-async def list_knowledge_docs(request: KnowledgeDocsQuery):
-    """
-    按 app_name 分页查询已上传的知识库文档记录,按上传时间倒序。
-    请求体使用 JSON,方便后续扩展查询条件。
-    """
-    page = max(1, request.page)
-    page_size = request.page_size if 1 <= request.page_size <= 100 else 20
-    return get_knowledge_docs(app_name=request.app_name, page=page, page_size=page_size)
-
-
-@router.post("/delete", summary="删除知识库文档")
-async def delete_knowledge_doc_api(request: KnowledgeDeleteRequest):
-    """
-    删除指定文档,依次执行:
-    1. 按 doc_id 查询 MongoDB,获取 resource_id
-    2. 调用 Viking /api/knowledge/doc/delete 删除知识库文档
-    3. 删除 MongoDB 中的上传记录
-    注:TOS 临时文件由桶生命周期规则自动过期清理,此处不处理。
-    """
-    # ── 1. 查询 MongoDB 记录 ────────────────────────────────────
-    doc = get_knowledge_doc_by_doc_id(request.doc_id)
-    if not doc:
-        raise HTTPException(status_code=404, detail=f"未找到 doc_id '{request.doc_id}' 的文档记录")
-
-    if doc.get("app_name") != request.app_name:
-        raise HTTPException(status_code=403, detail="doc_id 与 app_name 不匹配")
-
-    resource_id = doc.get("resource_id", "")
-
-    # ── 2. 删除 Viking 知识库文档 ───────────────────────────────
-    try:
-        await delete_doc_from_knowledge(resource_id=resource_id, doc_id=request.doc_id)
-    except ValueError as e:
-        raise HTTPException(status_code=400, detail=str(e))
-    except Exception as e:
-        raise HTTPException(status_code=500, detail=f"删除 Viking 知识库文档失败: {str(e)}")
-
-    # ── 3. 删除 MongoDB 记录 ────────────────────────────────────
-    delete_knowledge_doc(request.doc_id)
-
-    return {"doc_id": request.doc_id, "message": "删除成功"}

+ 3 - 3
main.py

@@ -1,7 +1,7 @@
 from fastapi import FastAPI
 from fastapi.middleware.cors import CORSMiddleware
 
-from app.routers import users, chat, chat_tools, ai_config, moderation, knowledge_tool
+from app.routers import users, chat, chat_tools, ai_config, moderation
 
 # 创建FastAPI应用实例
 app = FastAPI(title="聊天机器人", version="1.0.0", description="基于fastapi+VUE的聊天机器人")
@@ -23,7 +23,7 @@ app.include_router(chat.router, prefix="/main/chat", tags=["聊天管理"])
 app.include_router(chat_tools.router, prefix="/main/chatTools", tags=["AI工具管理"])
 app.include_router(ai_config.router, prefix="/main/aiConfig", tags=["AI配置管理"])
 app.include_router(moderation.router, prefix="/main/moderation", tags=["内容审核"])
-app.include_router(knowledge_tool.router, prefix="/main/knowledgeTool", tags=["知识库工具"])
+
 
 
 
@@ -37,7 +37,7 @@ if __name__ == "__main__":
     print("聊天机器人服务启动中...")
     print("=" * 50)
     print(f"Web界面: http://localhost:8000")
-    print(f"API文档: http://localhost:8000/docs")
+    # print(f"API文档: http://localhost:8000/docs")
     print("=" * 50)
 
     # 启动服务器

+ 0 - 2
requirements.txt

@@ -14,7 +14,5 @@ openai==2.30.0
 tls-sig-api-v2==1.1
 python-multipart==0.0.32
 websockets==16.0
-tos==2.9.3
-vikingdb-python-sdk==0.1.34
 #llama-index-core==0.14.25
 #Jinja2==3.1.6