"""知识库服务:调用 BaoDan Dataset API 管理文档。""" import requests from flask import current_app class KBService: """知识库管理业务逻辑。""" def _baodan_request(self, method: str, path: str, **kwargs): """发送 BaoDan API 请求的通用方法。""" api_key = current_app.config.get("BAODAN_KB_API_KEY", "") base_url = current_app.config.get("BAODAN_API_URL", "http://localhost:5001") headers = {"Authorization": f"Bearer {api_key}"} try: resp = requests.request(method, f"{base_url}{path}", headers=headers, timeout=30, **kwargs) return resp.json() except requests.Timeout: return {"code": 2002, "message": "BaoDan API 超时", "data": None} except Exception as e: return {"code": 2001, "message": f"BaoDan API 调用失败: {str(e)}", "data": None} def upload_documents(self, files, insurance_type: str, company: str) -> dict: """上传文档到 BaoDan 知识库。""" api_key = current_app.config.get("BAODAN_KB_API_KEY", "") base_url = current_app.config.get("BAODAN_API_URL", "http://localhost:5001") uploaded = [] for f in files: resp = requests.post( f"{base_url}/v1/datasets/documents/upload", headers={"Authorization": f"Bearer {api_key}"}, files={"file": (f.filename, f.stream, f.content_type)}, data={ "indexing_technique": "high_quality", "process_rule_mode": "automatic", }, timeout=60, ) data = resp.json() uploaded.append({ "doc_id": data.get("id", ""), "filename": f.filename, "status": "processing", }) # 记录操作日志 from insurance.utils.audit import log_operation log_operation("system", "upload", "document", "", { "files": [f.filename for f in files], "insurance_type": insurance_type, "company": company, }) return {"code": 0, "data": {"upload_id": "", "documents": uploaded}} def list_documents(self, params: dict) -> dict: """获取文档列表。""" result = self._baodan_request("GET", "/v1/datasets/documents", params={ "page": params["page"], "limit": params["page_size"], }) # BaoDan API 返回 {code: 0, data: [...]},直接透传 if result and "data" in result: documents = result["data"] if params.get("keyword"): keyword = params["keyword"].lower() documents = [d for d in documents if keyword in d.get("name", "").lower()] return {"code": 0, "data": documents} return {"code": 0, "data": []} def get_document_status(self, doc_id: str) -> dict: """查询文档处理状态。""" result = self._baodan_request("GET", f"/v1/datasets/documents/{doc_id}/status") return {"code": 0, "data": result} def retry_document(self, doc_id: str) -> dict: """重试失败文档。""" result = self._baodan_request("POST", f"/v1/datasets/documents/{doc_id}/retry") return {"code": 0, "data": result} def update_document(self, doc_id: str, data: dict) -> dict: """更新文档元数据。""" result = self._baodan_request("PATCH", f"/v1/datasets/documents/{doc_id}", json=data) return {"code": 0, "data": result} def delete_document(self, doc_id: str) -> dict: """删除文档。""" result = self._baodan_request("DELETE", f"/v1/datasets/documents/{doc_id}") # 记录操作日志 from insurance.utils.audit import log_operation log_operation("system", "delete", "document", doc_id) return {"code": 0, "message": "success", "data": None} def generate_doc_number(self, insurance_type: str, company: str) -> str: """生成文档编号。""" from insurance.db.compat import db from sqlalchemy import text type_codes = { "重疾险": "CJ", "寿险": "RS", "医疗险": "YL", "意外险": "YW", "车险": "CX", "年金险": "NJ", "储蓄险": "CX", } type_code = type_codes.get(insurance_type, "XX") company_code = company[:2] if company else "XX" # 查询当前序号 result = db.session.execute( text(""" SELECT COUNT(*) FROM document_metadata WHERE insurance_type = :type AND company = :company """), {"type": insurance_type, "company": company} ) count = result.scalar() or 0 return f"{type_code}-{company_code}-{count + 1:03d}" def list_datasources(self) -> dict: """获取保司 API 数据源列表。""" from insurance.db.compat import db from sqlalchemy import text try: result = db.session.execute(text("SELECT * FROM kb_datasources ORDER BY created_at DESC")) return {"code": 0, "data": [dict(row) for row in result]} except Exception: return {"code": 0, "data": []} def create_datasource(self, data: dict) -> dict: """新建数据源配置。""" from insurance.db.compat import db from sqlalchemy import text try: db.session.execute( text("INSERT INTO kb_datasources (name, api_url, api_key, sync_frequency) VALUES (:name, :url, :key, :freq)"), {"name": data["name"], "url": data["api_url"], "key": data.get("api_key", ""), "freq": data.get("sync_frequency", "")}, ) db.session.commit() # 记录操作日志 from insurance.utils.audit import log_operation log_operation("system", "create", "datasource", "", {"name": data["name"]}) return {"code": 0, "message": "success", "data": None} except Exception as e: return {"code": 5001, "message": f"创建数据源失败: {str(e)}", "data": None} def sync_datasource(self, ds_id: str) -> dict: """手动触发同步。""" import threading from insurance.db.compat import db from sqlalchemy import text # 获取数据源配置 try: result = db.session.execute( text("SELECT * FROM kb_datasources WHERE id = :ds_id"), {"ds_id": ds_id}, ) datasource = result.fetchone() if not datasource: return {"code": 1005, "message": "数据源不存在", "data": None} except Exception as e: return {"code": 5001, "message": f"查询数据源失败: {str(e)}", "data": None} # 记录同步开始 self._log_sync_start(ds_id) # 异步执行同步任务 from flask import current_app flask_app = current_app._get_current_object() def sync_task(): with flask_app.app_context(): try: self._execute_sync(ds_id, dict(datasource) if datasource else {}) self._log_sync_success(ds_id) except Exception as e: self._log_sync_failure(ds_id, str(e)) from insurance.utils.notify import notify_sync_failure notify_sync_failure(f"数据源#{ds_id}", str(e)) thread = threading.Thread(target=sync_task, daemon=True) thread.start() return {"code": 0, "message": "同步任务已提交", "data": None} def _log_sync_start(self, ds_id: str): """记录同步开始。""" from insurance.db.compat import db from sqlalchemy import text try: db.session.execute( text("INSERT INTO kb_sync_logs (datasource_id, status, message) VALUES (:ds_id, :status, :message)"), {"ds_id": ds_id, "status": "running", "message": "同步开始"}, ) db.session.commit() except Exception: db.session.rollback() def _log_sync_success(self, ds_id: str): """记录同步成功。""" from insurance.db.compat import db from sqlalchemy import text try: db.session.execute( text("UPDATE kb_sync_logs SET status = :status, message = :message WHERE datasource_id = :ds_id AND status = 'running'"), {"ds_id": ds_id, "status": "success", "message": "同步完成"}, ) db.session.commit() except Exception: db.session.rollback() def _log_sync_failure(self, ds_id: str, error: str): """记录同步失败。""" from insurance.db.compat import db from sqlalchemy import text try: db.session.execute( text("UPDATE kb_sync_logs SET status = :status, message = :message WHERE datasource_id = :ds_id AND status = 'running'"), {"ds_id": ds_id, "status": "failed", "message": error[:500]}, ) db.session.commit() except Exception: db.session.rollback() def _execute_sync(self, ds_id: str, datasource: dict): """执行实际的同步操作。""" import requests from flask import current_app api_url = datasource.get("api_url", "") api_key = datasource.get("api_key", "") if not api_url: raise Exception("API地址未配置") # 调用保司API获取数据 headers = {"Authorization": f"Bearer {api_key}"} if api_key else {} resp = requests.get(api_url, headers=headers, timeout=60) resp.raise_for_status() data = resp.json() # TODO: 根据实际API响应格式解析数据并同步到知识库 # 这里需要根据具体保司API格式进行适配 return data def get_sync_logs(self, ds_id: str) -> dict: """获取同步日志。""" from insurance.db.compat import db from sqlalchemy import text try: result = db.session.execute( text("SELECT * FROM kb_sync_logs WHERE datasource_id = :ds_id ORDER BY created_at DESC LIMIT 50"), {"ds_id": ds_id}, ) return {"code": 0, "data": [dict(row) for row in result]} except Exception: return {"code": 0, "data": []} def retrieval_search(self, query: str, filters: dict, top_k: int) -> dict: """向量检索:调用 BaoDan Dataset Retrieval API。""" api_key = current_app.config.get("BAODAN_KB_API_KEY", "") base_url = current_app.config.get("BAODAN_API_URL", "http://localhost:5001") try: resp = requests.post( f"{base_url}/v1/datasets/retrieve", json={ "query": query, "retrieval_model": { "search_method": "semantic_search", "reranking_enable": False, "top_k": top_k, "score_threshold_enabled": False, }, }, headers={"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}, timeout=30, ) data = resp.json() results = [ { "doc_name": r.get("document_name", ""), "chunk": r.get("segment_content", ""), "score": r.get("score", 0), "metadata": r.get("metadata", {}), } for r in data.get("records", []) ] return {"code": 0, "data": {"results": results}} except Exception as e: return {"code": 2001, "message": f"检索失败: {str(e)}", "data": None}