viking_client.py 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160
  1. """
  2. Viking 知识库上传工具模块
  3. 职责:
  4. 1. 将本地临时文件上传到火山引擎 TOS 对象存储
  5. 2. 调用 Viking /api/knowledge/doc/v2/add 接口将 TOS 文件导入知识库
  6. """
  7. import os
  8. import uuid
  9. import asyncio
  10. from concurrent.futures import ThreadPoolExecutor
  11. import httpx
  12. from ..config.config import Config
  13. cfg = Config()
  14. # 线程池用于执行 TOS 同步 SDK 调用
  15. _executor = ThreadPoolExecutor(max_workers=4)
  16. def _tos_upload_sync(local_path: str, object_key: str) -> str:
  17. """
  18. 同步上传本地文件到 TOS,返回 tos:// URI。
  19. 在线程池中调用,避免阻塞事件循环。
  20. """
  21. try:
  22. import tos as tos_sdk
  23. except ImportError:
  24. raise RuntimeError("缺少 tos 依赖,请执行 pip install tos")
  25. client = tos_sdk.TosClientV2(
  26. ak=cfg.VIKING_AK,
  27. sk=cfg.VIKING_SK,
  28. endpoint=cfg.VIKING_TOS_ENDPOINT,
  29. region=cfg.VIKING_TOS_REGION,
  30. )
  31. client.put_object_from_file(
  32. bucket=cfg.VIKING_TOS_BUCKET,
  33. key=object_key,
  34. file_path=local_path,
  35. )
  36. return f"tos://{cfg.VIKING_TOS_BUCKET}/{object_key}"
  37. async def upload_to_tos(local_path: str, app_name: str, original_filename: str) -> tuple[str, str]:
  38. """
  39. 异步上传本地文件到 TOS。
  40. 返回 (object_key, tos_uri)。
  41. object_key 格式:{app_name}/{uuid32}_{原始文件名}
  42. """
  43. # 清理文件名中的特殊字符,只保留字母数字下划线中文和常见标点
  44. safe_name = os.path.basename(original_filename)
  45. uid = uuid.uuid4().hex
  46. object_key = f"{app_name}/{uid}_{safe_name}"
  47. loop = asyncio.get_event_loop()
  48. tos_uri = await loop.run_in_executor(_executor, _tos_upload_sync, local_path, object_key)
  49. return object_key, tos_uri
  50. async def add_doc_to_knowledge(
  51. resource_id: str,
  52. doc_id: str,
  53. tos_uri: str,
  54. doc_name: str,
  55. doc_type: str,
  56. tag_list: list | None = None,
  57. path_segments: list | None = None,
  58. ) -> dict:
  59. """
  60. 调用 Viking /api/knowledge/doc/v2/add 接口,将 TOS 文件导入知识库。
  61. 使用 resource_id 指定知识库(对应 ai_config.knowledgeId)。
  62. 返回响应体 data 字段。
  63. """
  64. url = f"https://{cfg.VIKING_HOST}/api/knowledge/doc/v2/add"
  65. payload: dict = {
  66. "resource_id": resource_id,
  67. "doc_id": doc_id,
  68. "doc_name": doc_name,
  69. "doc_type": doc_type,
  70. "uri": tos_uri,
  71. }
  72. if tag_list:
  73. payload["tag_list"] = tag_list
  74. if path_segments:
  75. payload["path_segments"] = path_segments
  76. async with httpx.AsyncClient(timeout=30) as client:
  77. resp = await client.post(
  78. url,
  79. json=payload,
  80. headers={
  81. "Content-Type": "application/json",
  82. "Authorization": f"Bearer {cfg.VIKING_API_KEY}",
  83. },
  84. )
  85. body = resp.json()
  86. code = body.get("code", -1)
  87. if code != 0:
  88. raise ValueError(
  89. f"Viking API 返回错误: {body.get('message')} "
  90. f"(code={code}, request_id={body.get('request_id')})"
  91. )
  92. return body["data"]
  93. def _tos_delete_sync(object_key: str) -> None:
  94. """
  95. 同步删除 TOS 对象。
  96. 在线程池中调用,避免阻塞事件循环。
  97. """
  98. try:
  99. import tos as tos_sdk
  100. except ImportError:
  101. raise RuntimeError("缺少 tos 依赖,请执行 pip install tos")
  102. client = tos_sdk.TosClientV2(
  103. ak=cfg.VIKING_AK,
  104. sk=cfg.VIKING_SK,
  105. endpoint=cfg.VIKING_TOS_ENDPOINT,
  106. region=cfg.VIKING_TOS_REGION,
  107. )
  108. client.delete_object(bucket=cfg.VIKING_TOS_BUCKET, key=object_key)
  109. async def delete_from_tos(object_key: str) -> None:
  110. """异步删除 TOS 对象。"""
  111. loop = asyncio.get_event_loop()
  112. await loop.run_in_executor(_executor, _tos_delete_sync, object_key)
  113. async def delete_doc_from_knowledge(resource_id: str, doc_id: str) -> dict:
  114. """
  115. 调用 Viking /api/knowledge/doc/delete 接口删除知识库文档。
  116. 返回完整响应体。
  117. """
  118. url = f"https://{cfg.VIKING_HOST}/api/knowledge/doc/delete"
  119. payload = {
  120. "resource_id": resource_id,
  121. "doc_id": doc_id,
  122. }
  123. async with httpx.AsyncClient(timeout=30) as client:
  124. resp = await client.post(
  125. url,
  126. json=payload,
  127. headers={
  128. "Content-Type": "application/json",
  129. "Authorization": f"Bearer {cfg.VIKING_API_KEY}",
  130. },
  131. )
  132. body = resp.json()
  133. code = body.get("code", -1)
  134. if code != 0:
  135. raise ValueError(
  136. f"Viking API 返回错误: {body.get('message')} "
  137. f"(code={code}, request_id={body.get('request_id')})"
  138. )
  139. return body