from appPublic.timeUtils import curDateString from filemgr.filemgr import FileMgr from rag.uapi_service import APIService from appPublic.registerfunction import RegisterFunction from appPublic.log import debug, error, info from sqlor.dbpools import DBPools import asyncio import aiohttp from langchain_core.documents import Document from langchain_text_splitters import RecursiveCharacterTextSplitter import os import re import time import uuid from datetime import datetime import traceback from filetxt.loader import fileloader,File2Text from ahserver.serverenv import get_serverenv from typing import List, Dict, Any from rag.service_opts import get_service_params, sor_get_service_params, sor_get_embedding_mode, get_embedding_mode from rag.fileprocess import extract_images_from_file from rag.rag_operations import RagOperations import json from rag.transaction_manager import TransactionContext from dataclasses import dataclass from enum import Enum import base64 from pathlib import Path class RagFileMgr(FileMgr): def __init__(self, fiid): super().__init__(fiid) self.rag_ops = RagOperations() async def get_folder_ownerid(self, sor): fiid = self.fiid recs = await sor.R('kdb', {'id': self.fiid}) if len(recs) > 0: return recs[0].orgid return None async def get_organization_quota(self, sor, orgid): sql = """select a.* from ragquota a, kdb b where a.orgid = b.orgid and b.id = ${id}$ and ${today}$ >= a.enabled_date and ${today}$ < a.expired_date """ recs = await sor.sqlExe(sql, { 'id': self.fiid, 'today': curDateString() }) if len(recs) > 0: r = recs[0] return r.quota, r.expired_date return None, None async def file_to_base64(self,path: str) -> str: with open(path, "rb") as f: return base64.b64encode(f.read()).decode("utf-8") async def file_uploaded(self, request, ns, userid): """将文档插入 Milvus 并抽取三元组到 Neo4j""" debug(f'Received ns: {ns=}') env = request._run_ns realpath = ns.get('realpath', '') fiid = ns.get('fiid', '') id = ns.get('id', '') orgid = ns.get('ownerid', '') db_type = '' debug( f'Inserting document: file_path={realpath}, userid={orgid}, db_type={db_type}, knowledge_base_id={fiid}, document_id={id}') timings = {} start_total = time.time() result = { "status": "error", "userid": orgid, "document_id": id, "collection_name": "ragdb", "timings": timings, "message": "", "status_code": 400 } # 初始化回滚上下文 rollback_context = { "request": request, "userid": userid, "service_params": None # 在 try 块中设置 } async with TransactionContext(f"file_upload_{id}") as transaction_mgr: # 将 rollback_context 绑定到 TransactionContext transaction_mgr.transaction_context = rollback_context try: # 验证必填字段 if not orgid or not fiid or not id: raise ValueError("orgid、fiid 和 id 不能为空") if len(orgid) > 32 or len(fiid) > 255: raise ValueError("orgid 或 fiid 的长度超出限制") if not os.path.exists(realpath): raise ValueError(f"文件 {realpath} 不存在") # 获取服务参数 service_params = await get_service_params(orgid) debug(f"服务参数是:{service_params}") if not service_params: raise ValueError("无法获取服务参数") rollback_context["service_params"] = service_params #获取嵌入模式 embedding_mode = await get_embedding_mode(orgid) debug(f"检测到 embedding_mode = {embedding_mode}(0=文本, 1=多模态)") # 加载和分片文档 chunks = await self.rag_ops.load_and_chunk_document( realpath, timings, transaction_mgr=transaction_mgr ) text_embeddings = None multi_results = None image_paths = [] if embedding_mode == 1: inputs = [] # 文本 for chunk in chunks: inputs.append({"type": "text", "content": chunk.page_content}) debug("开始多模态图像抽取与嵌入") image_paths = extract_images_from_file(realpath) debug(f"从文档中抽取 {len(image_paths)} 张图像") if image_paths: for img_path in image_paths: try: # 1. 自动识别真实格式 ext = Path(img_path).suffix.lower() if ext not in {".png", ".jpg", ".jpeg", ".webp", ".bmp"}: ext = ".jpg" mime_map = { ".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".webp": "image/webp", ".bmp": "image/bmp" } mime_type = mime_map.get(ext, "image/jpeg") # # 2. 智能压缩(>1MB 才压缩,节省 70% 流量) # img = Image.open(img_path).convert("RGB") # if os.path.getsize(img_path) > 1024 * 1024: # >1MB # buffer = BytesIO() # img.save(buffer, format="JPEG", quality=85, optimize=True) # b64 = base64.b64encode(buffer.getvalue()).decode() # data_uri = f"data:image/jpeg;base64,{b64}" # else: b64 = await self.file_to_base64(img_path) data_uri = f"data:{mime_type};base64,{b64}" inputs.append({ "type": "image", "data": data_uri }) debug(f"已添加图像({mime_type}, {len(b64) / 1024:.1f}KB): {Path(img_path).name}") except Exception as e: debug(f"图像处理失败,跳过: {img_path} → {e}") # 即使失败也加个占位,防止顺序错乱 inputs.append({ "type": "image", "data": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=" }) debug(f"混排输入总数: {len(inputs)}(文本 {len(chunks)} + 图像 {len(image_paths)})") multi_results = await self.rag_ops.generate_multi_embeddings( request=request, inputs=inputs, service_params=service_params, userid=userid, timings=timings, transaction_mgr=transaction_mgr ) debug(f"多模态嵌入成功,返回 {len(multi_results)} 条结果") else: # 生成嵌入向量 debug("【纯文本模式】使用 BGE 嵌入") text_embeddings = await self.rag_ops.generate_embeddings( request, chunks, service_params, userid, timings, transaction_mgr=transaction_mgr ) debug(f"BGE 嵌入完成: {len(text_embeddings)} 条") inserted = await self.rag_ops.insert_all_vectors( request=request, text_chunks=chunks, realpath=realpath, orgid=orgid, fiid=fiid, document_id=id, service_params=service_params, userid=userid, db_type=db_type, timings=timings, img_paths=image_paths, text_embeddings=text_embeddings, multi_results=multi_results, transaction_mgr=transaction_mgr ) debug(f"统一插入: 文本 {inserted['text']}, 图像 {inserted['image']}, 人脸 {inserted['face']}") # 抽取三元组 triples = await self.rag_ops.extract_triples( request, chunks, service_params, userid, timings, transaction_mgr=transaction_mgr ) # 插入 Neo4j await self.rag_ops.insert_to_graph_db( request, triples, id, fiid, orgid, service_params, userid, timings, transaction_mgr=transaction_mgr ) timings["total"] = time.time() - start_total result.update({ "status": "success", "unique_triples": triples, "message": f"文件 {realpath} 成功嵌入并处理三元组", "status_code": 200 }) debug(f"总耗时: {timings['total']:.2f} 秒") except Exception as e: error(f"插入文档失败: {str(e)}, 堆栈: {traceback.format_exc()}") timings["total"] = time.time() - start_total result.update({ "message": f"插入文档失败: {str(e)}", "timings": timings }) raise ValueError(str(e)) from e debug(f"最终结果是:{result}") return result async def file_deleted(self, request, recs, userid): """删除用户指定文件数据,包括 Milvus 和 Neo4j 中的记录""" if not isinstance(recs, list): recs = [recs] results = [] total_nodes_deleted = 0 total_rels_deleted = 0 for rec in recs: id = rec.get('id', '') realpath = rec.get('realpath', '') fiid = rec.get('fiid', '') orgid = rec.get('ownerid', '') db_type = '' collection_name = "ragdb" if not db_type else f"ragdb_{db_type}" try: required_fields = ['id', 'realpath', 'fiid', 'ownerid'] missing_fields = [field for field in required_fields if not rec.get(field, '')] if missing_fields: raise ValueError(f"缺少必填字段: {', '.join(missing_fields)}") service_params = await get_service_params(orgid) if not service_params: raise ValueError("无法获取服务参数") # 调用 Milvus 删除 await self.rag_ops.delete_from_vector_db(request, orgid, realpath, fiid, id, service_params, userid, db_type) # 调用 Neo4j 删除 neo4j_deleted_nodes = 0 neo4j_deleted_rels = 0 try: nodes_deleted, rels_deleted = await self.rag_ops.delete_from_graph_db(request, id, service_params, userid) neo4j_deleted_nodes += nodes_deleted neo4j_deleted_rels += rels_deleted total_nodes_deleted += nodes_deleted total_rels_deleted += rels_deleted except Exception as e: error(f"删除 document_id={id} 的 Neo4j 数据失败: {str(e)}") results.append({ "status": "success", "collection_name": collection_name, "document_id": id, "message": f"成功删除文件 {realpath} 的 Milvus 记录,{neo4j_deleted_nodes} 个 Neo4j 节点,{neo4j_deleted_rels} 个 Neo4j 关系", "status_code": 200 }) except Exception as e: error(f"删除文档 {realpath} 失败: {str(e)}, 堆栈: {traceback.format_exc()}") results.append({ "status": "error", "collection_name": collection_name, "document_id": id, "message": f"删除文档 {realpath} 失败: {str(e)}", "status_code": 400 }) return { "status": "success" if all(r["status"] == "success" for r in results) else "partial", "results": results, "total_nodes_deleted": total_nodes_deleted, "total_rels_deleted": total_rels_deleted, "message": f"处理 {len(recs)} 个文件,成功删除 {sum(1 for r in results if r['status'] == 'success')} 个", "status_code": 200 if all(r["status"] == "success" for r in results) else 207 }