From e4cdcc1f5a8b2d91f31b775458c953d9626f0f59 Mon Sep 17 00:00:00 2001 From: wangmeihua <13383952685@163.com> Date: Mon, 28 Jul 2025 18:06:29 +0800 Subject: [PATCH] =?UTF-8?q?=E5=88=A0=E9=99=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build/lib/rag/__init__.py | 2 - build/lib/rag/deletefile.py | 138 ------ build/lib/rag/embed.py | 178 ------- build/lib/rag/init.py | 15 - build/lib/rag/kdb.py | 81 ---- build/lib/rag/query.py | 180 ------- build/lib/rag/rag.bak.py | 53 -- build/lib/rag/rag.py | 53 -- build/lib/rag/vector.py | 539 --------------------- build/lib/rag/version.py | 1 - rag/__pycache__/__init__.cpython-310.pyc | Bin 173 -> 0 bytes rag/__pycache__/deletefile.cpython-310.pyc | Bin 5338 -> 0 bytes rag/__pycache__/embed.cpython-310.pyc | Bin 5672 -> 0 bytes rag/__pycache__/extract.cpython-310.pyc | Bin 6475 -> 0 bytes rag/__pycache__/kgc.cpython-310.pyc | Bin 7528 -> 0 bytes rag/dict/time.txt | 1 - rag/dict/unk.txt | 3 - 17 files changed, 1244 deletions(-) delete mode 100644 build/lib/rag/__init__.py delete mode 100644 build/lib/rag/deletefile.py delete mode 100644 build/lib/rag/embed.py delete mode 100644 build/lib/rag/init.py delete mode 100644 build/lib/rag/kdb.py delete mode 100644 build/lib/rag/query.py delete mode 100644 build/lib/rag/rag.bak.py delete mode 100644 build/lib/rag/rag.py delete mode 100644 build/lib/rag/vector.py delete mode 100644 build/lib/rag/version.py delete mode 100644 rag/__pycache__/__init__.cpython-310.pyc delete mode 100644 rag/__pycache__/deletefile.cpython-310.pyc delete mode 100644 rag/__pycache__/embed.cpython-310.pyc delete mode 100644 rag/__pycache__/extract.cpython-310.pyc delete mode 100644 rag/__pycache__/kgc.cpython-310.pyc delete mode 100644 rag/dict/time.txt delete mode 100644 rag/dict/unk.txt diff --git a/build/lib/rag/__init__.py b/build/lib/rag/__init__.py deleted file mode 100644 index 1f56709..0000000 --- a/build/lib/rag/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -from .version import __version__ - diff --git a/build/lib/rag/deletefile.py b/build/lib/rag/deletefile.py deleted file mode 100644 index 560031f..0000000 --- a/build/lib/rag/deletefile.py +++ /dev/null @@ -1,138 +0,0 @@ -import logging -import yaml -import os -from pymilvus import connections, Collection, utility -from vector import initialize_milvus_connection - -# 加载配置文件 -CONFIG_PATH = os.getenv('CONFIG_PATH', '/share/wangmeihua/rag/conf/milvusconfig.yaml') -try: - with open(CONFIG_PATH, 'r', encoding='utf-8') as f: - config = yaml.safe_load(f) - MILVUS_DB_PATH = config['database']['milvus_db_path'] - TEXT_EMBEDDING_MODEL = config['models']['text_embedding_model'] -except Exception as e: - print(f"加载配置文件 {CONFIG_PATH} 失败: {str(e)}") - raise RuntimeError(f"无法加载配置文件: {str(e)}") - -# 配置日志 -logger = logging.getLogger(config['logging']['name']) -logger.setLevel(getattr(logging, config['logging']['level'], logging.DEBUG)) -os.makedirs(os.path.dirname(config['logging']['file']), exist_ok=True) -formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s') -for handler in (logging.FileHandler(config['logging']['file'], encoding='utf-8'), logging.StreamHandler()): - handler.setFormatter(formatter) - logger.addHandler(handler) - -def delete_document(db_type: str, userid: str, filename: str) -> bool: - """ - 根据 db_type、userid 和 filename 删除用户的指定文件数据。 - - 参数: - db_type (str): 数据库类型(如 'textdb', 'pptdb') - userid (str): 用户 ID - filename (str): 文件名(如 'test.docx') - - 返回: - bool: 删除是否成功 - - 异常: - ValueError: 参数无效 - RuntimeError: 数据库操作失败 - """ - try: - # 参数验证 - if not db_type or "_" in db_type: - raise ValueError("db_type 不能为空且不能包含下划线") - if not userid or "_" in userid: - raise ValueError("userid 不能为空且不能包含下划线") - if not filename: - raise ValueError("filename 不能为空") - if len(db_type) > 100 or len(userid) > 100 or len(filename) > 255: - raise ValueError("db_type、userid 或 filename 的长度超出限制") - - # 初始化 Milvus 连接 - initialize_milvus_connection() - logger.debug(f"已连接到 Milvus Lite,路径: {MILVUS_DB_PATH}") - - # 检查集合是否存在 - collection_name = f"ragdb_{db_type}" - if not utility.has_collection(collection_name): - logger.warning(f"集合 {collection_name} 不存在") - return False - - # 加载集合 - try: - collection = Collection(collection_name) - collection.load() - logger.debug(f"加载集合: {collection_name}") - except Exception as e: - logger.error(f"加载集合 {collection_name} 失败: {str(e)}") - raise RuntimeError(f"加载集合失败: {str(e)}") - - # 查询匹配的 document_id - expr = f"userid == '{userid}' and filename == '{filename}'" - logger.debug(f"查询表达式: {expr}") - try: - results = collection.query( - expr=expr, - output_fields=["document_id"], - limit=1000 - ) - if not results: - logger.warning(f"没有找到 userid={userid}, filename={filename} 的记录") - return False - document_ids = list(set(result["document_id"] for result in results if "document_id" in result)) - logger.debug(f"找到 {len(document_ids)} 个 document_id: {document_ids}") - except Exception as e: - logger.error(f"查询 document_id 失败: {str(e)}") - raise RuntimeError(f"查询失败: {str(e)}") - - # 执行删除 - total_deleted = 0 - for doc_id in document_ids: - try: - delete_expr = f"userid == '{userid}' and document_id == '{doc_id}'" - logger.debug(f"删除表达式: {delete_expr}") - delete_result = collection.delete(delete_expr) - deleted_count = delete_result.delete_count - total_deleted += deleted_count - logger.info(f"成功删除 document_id={doc_id} 的 {deleted_count} 条记录") - except Exception as e: - logger.error(f"删除 document_id={doc_id} 失败: {str(e)}") - continue - - if total_deleted == 0: - logger.warning(f"没有删除任何记录,userid={userid}, filename={filename}") - return False - - logger.info(f"总计删除 {total_deleted} 条记录,userid={userid}, filename={filename}") - return True - - except ValueError as ve: - logger.error(f"参数验证失败: {str(ve)}") - return False - except RuntimeError as re: - logger.error(f"数据库操作失败: {str(re)}") - return False - except Exception as e: - logger.error(f"删除文件失败: {str(e)}") - import traceback - logger.debug(traceback.format_exc()) - return False - finally: - try: - connections.disconnect("default") - logger.debug("已断开 Milvus 连接") - except Exception as e: - logger.warning(f"断开 Milvus 连接失败: {str(e)}") - -if __name__ == "__main__": - # 测试用例 - db_type = "textdb" - userid = "testuser4" - filename = "聚类结果1.xlsx" - - logger.info(f"测试:删除 userid={userid}, filename={filename} 的文件") - result = delete_document(db_type, userid, filename) - print(f"删除结果: {result}") \ No newline at end of file diff --git a/build/lib/rag/embed.py b/build/lib/rag/embed.py deleted file mode 100644 index d81762f..0000000 --- a/build/lib/rag/embed.py +++ /dev/null @@ -1,178 +0,0 @@ -import os -import uuid -import yaml -import logging -from datetime import datetime -from typing import List -from langchain_core.documents import Document -from langchain_text_splitters import RecursiveCharacterTextSplitter -from pymilvus import connections -from .vector import get_vector_db -from filetxt.loader import fileloader - -# 加载配置文件 -CONFIG_PATH = os.getenv('CONFIG_PATH', '/share/wangmeihua/rag/conf/milvusconfig.yaml') -try: - with open(CONFIG_PATH, 'r', encoding='utf-8') as f: - config = yaml.safe_load(f) - MILVUS_DB_PATH = config['database']['milvus_db_path'] -except Exception as e: - print(f"加载配置文件 {CONFIG_PATH} 失败: {str(e)}") - raise RuntimeError(f"无法加载配置文件: {str(e)}") - -# 配置日志 -logger = logging.getLogger(config['logging']['name']) -logger.setLevel(getattr(logging, config['logging']['level'], logging.DEBUG)) -os.makedirs(os.path.dirname(config['logging']['file']), exist_ok=True) -formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s') -for handler in (logging.FileHandler(config['logging']['file'], encoding='utf-8'), logging.StreamHandler()): - handler.setFormatter(formatter) - logger.addHandler(handler) - -def load_and_split_data(file_path: str, userid: str) -> List[Document]: - """ - 加载文件,分片并生成带有元数据的 Document 对象。 - - 参数: - file_path (str): 文件路径 - userid (str): 用户ID - - 返回: - List[Document]: 分片后的文档列表 - - 异常: - ValueError: 文件或参数无效 - """ - try: - # 验证文件 - if not os.path.exists(file_path): - raise ValueError(f"文件 {file_path} 不存在") - if os.path.getsize(file_path) == 0: - raise ValueError(f"文件 {file_path} 为空") - logger.debug(f"检查文件: {file_path}, 大小: {os.path.getsize(file_path)} 字节") - ext = file_path.rsplit('.', 1)[1].lower() - logger.debug(f"文件扩展名: {ext}") - - # 使用 fileloader 加载文件内容 - logger.debug("开始加载文件") - text = fileloader(file_path) - if not text or not text.strip(): - raise ValueError(f"文件 {file_path} 加载为空") - - # 创建单个 Document 对象 - document = Document(page_content=text) - logger.debug(f"加载完成,生成 1 个文档") - - # 分割文本 - text_splitter = RecursiveCharacterTextSplitter( - chunk_size=2000, - chunk_overlap=200, - length_function=len, - ) - chunks = text_splitter.split_documents([document]) - logger.debug(f"分割完成,生成 {len(chunks)} 个文档块") - - # 为整个文件生成唯一的 document_id - document_id = str(uuid.uuid4()) - - # 添加元数据,确保包含所有必需字段 - filename = os.path.basename(file_path) - upload_time = datetime.now().isoformat() - documents = [] - for i, chunk in enumerate(chunks): - chunk.metadata.update({ - 'userid': userid, - 'document_id': document_id, - 'filename': filename, - 'file_path': file_path, - 'upload_time': upload_time, - 'file_type': ext, - 'chunk_index': i, # 可选,追踪分片顺序 - 'source': file_path, # 可选,追踪文件来源 - }) - # 验证元数据完整性 - required_fields = ['userid', 'document_id', 'filename', 'file_path', 'upload_time', 'file_type'] - if not all(field in chunk.metadata and chunk.metadata[field] for field in required_fields): - raise ValueError(f"文档元数据缺少必需字段或值为空: {chunk.metadata}") - documents.append(chunk) - logger.debug(f"生成文档块 {i}: metadata={chunk.metadata}") - - logger.debug(f"文件 {file_path} 加载并分割为 {len(documents)} 个文档块,document_id: {document_id}") - return documents - - except Exception as e: - logger.error(f"加载或分割文件 {file_path} 失败: {str(e)}") - import traceback - logger.debug(traceback.format_exc()) - raise ValueError(f"加载或分割文件失败: {str(e)}") - -def embed(file_path: str, userid: str, db_type: str) -> bool: - """ - 嵌入文件到 Milvus 向量数据库。 - - 参数: - file_path (str): 文件路径 - userid (str): 用户ID - db_type (str): 数据库类型 - - 返回: - bool: 嵌入是否成功 - - 异常: - ValueError: 参数无效 - RuntimeError: 数据库操作失败 - """ - try: - # 验证输入 - if not userid or not db_type: - raise ValueError("userid 和 db_type 不能为空") - if "_" in userid: - raise ValueError("userid 不能包含下划线") - if "_" in db_type: - raise ValueError("db_type 不能包含下划线") - if not os.path.exists(file_path): - raise ValueError(f"文件 {file_path} 不存在") - - supported_formats = {'pdf', 'doc', 'docx', 'xlsx', 'xls', 'ppt', 'pptx', 'csv', 'txt'} - ext = file_path.rsplit('.', 1)[1].lower() - if ext not in supported_formats: - logger.error(f"文件 {file_path} 格式不支持,支持的格式: {', '.join(supported_formats)}") - raise ValueError(f"不支持的文件格式: {ext}, 支持的格式: {', '.join(supported_formats)}") - - # 加载并分割文件 - logger.info(f"开始处理文件 {file_path},userid: {userid},db_type: {db_type}") - chunks = load_and_split_data(file_path, userid) - if not chunks: - logger.error(f"文件 {file_path} 未生成任何文档块") - raise ValueError("未生成任何文档块") - - logger.debug(f"处理文件 {file_path},生成 {len(chunks)} 个文档块") - logger.debug(f"第一个文档块: {chunks[0].page_content[:200]}") - - # 插入到 Milvus - db = get_vector_db(userid, db_type, documents=chunks) - if not db: - logger.error(f"无法初始化或插入到向量数据库 ragdb_{db_type}") - raise RuntimeError(f"数据库操作失败") - - logger.info(f"文件 {file_path} 成功嵌入到数据库 ragdb_{db_type}") - return True - - except ValueError as ve: - logger.error(f"嵌入文件 {file_path} 失败: {str(ve)}") - return False - except RuntimeError as re: - logger.error(f"嵌入文件 {file_path} 失败: {str(re)}") - return False - except Exception as e: - logger.error(f"嵌入文件 {file_path} 失败: {str(e)}") - import traceback - logger.debug(traceback.format_exc()) - return False - -if __name__ == "__main__": - test_file = "/share/wangmeihua/rag/data/test.txt" - userid = "testuser4" - db_type = "textdb" - result = embed(test_file, userid, db_type) - print(f"嵌入结果: {result}") diff --git a/build/lib/rag/init.py b/build/lib/rag/init.py deleted file mode 100644 index d65c0b5..0000000 --- a/build/lib/rag/init.py +++ /dev/null @@ -1,15 +0,0 @@ -from appPublic.worker import awaitify -from ahserver.serverenv import ServerEnv -from .kdb import add_kdb, add_dir, add_doc, get_all_docs -from .query import search_query -from .embed import embed -def load_rag(): - env = ServerEnv() - env.add_kdb = add_kdb - env.query = awaitify(search_query) - env.embed = awaitify(embed) - env.add_dir = add_dir - env.add_doc = add_doc - env.get_all_docs = get_all_docs - - diff --git a/build/lib/rag/kdb.py b/build/lib/rag/kdb.py deleted file mode 100644 index 47ef805..0000000 --- a/build/lib/rag/kdb.py +++ /dev/null @@ -1,81 +0,0 @@ - -from traceback import format_exc -from appPublic.uniqueID import getID -from appPublic.timeUtils import curDateString -from appPublic.dictObject import DictObject -from sqlor.dbpools import DBPools -from ahserver.serverenv import get_serverenv -from ahserver.filestorage import FileStorage - -async def add_kdb(kdb:dict) -> None: - """ - 添加知识库 - """ - kdb = DictObject(**kdb) - kdb.parentid=None - if kdb.id is None: - kdb.id = getID() - kdb.entity_type = '0' - kdb.create_date = curDateString() - if kdb.orgid is None: - e = Exception(f'Can not add none orgid kdb') - exception(f'{e}\n{format_exc()}') - raise e - - f = get_serverenv('get_module_dbname') - dbname = f('rag') - db = DBPools() - async with db.sqlorContext(dbname) as sor: - await C('kdb', kdb.copy()) - -async def add_dir(kdb:dict) -> None: - """ - 添加子目录 - """ - kdb = DictObject(**kdb) - if kdb.parentid is None: - e = Exception(f'Can not add root folder') - exception(f'{e}\n{format_exc()}') - raise e - if kdb.id is None: - kdb.id = getID() - kdb.entity_type = '1' - kdb.create_date = curDateString() - f = get_serverenv('get_module_dbname') - dbname = f('rag') - db = DBPools() - async with db.sqlorContext(dbname) as sor: - await C('kdb', kdb.copy()) - -async def add_doc(doc:dict) -> None: - """ - 添加文档 - """ - doc = DictObject(**doc) - if doc.parentid is None: - e = Exception(f'Can not add root document') - exception(f'{e}\n{format_exc()}') - raise e - if doc.id is None: - doc.id = getID() - fs = FileStorage() - doc.realpath = fs.realPath(doc.webpath) - doc.create_date = curDateString() - f = get_serverenv('get_module_dbname') - dbname = f('rag') - db = DBPools() - async with db.sqlorContext(dbname) as sor: - await C('doc', doc.copy()) - -async def get_all_docs(sor, kdbid): - """ - 获取所有kdbid下的文档,含子目录的 - """ - docs = await sor.R('doc', {'parentid':kdbid}) - kdbs = await sor.R('kdb', {'parentid':kdbid}) - for kdb in kdbs: - docs1 = await get_all_docs(kdb.id) - docs += docs1 - return docs - - diff --git a/build/lib/rag/query.py b/build/lib/rag/query.py deleted file mode 100644 index 7b4cc3f..0000000 --- a/build/lib/rag/query.py +++ /dev/null @@ -1,180 +0,0 @@ -import os -import yaml -import logging -from typing import List, Dict -from pymilvus import connections, Collection, utility -from langchain_huggingface import HuggingFaceEmbeddings -from .vector import get_vector_db, initialize_milvus_connection, cleanup_milvus_connection -import torch - -# 加载配置文件 -CONFIG_PATH = os.getenv('CONFIG_PATH', '/share/wangmeihua/rag/conf/milvusconfig.yaml') -try: - with open(CONFIG_PATH, 'r', encoding='utf-8') as f: - config = yaml.safe_load(f) - MILVUS_DB_PATH = config['database']['milvus_db_path'] - TEXT_EMBEDDING_MODEL = config['models']['text_embedding_model'] -except Exception as e: - print(f"加载配置文件 {CONFIG_PATH} 失败: {str(e)}") - raise RuntimeError(f"无法加载配置文件: {str(e)}") - -# 配置日志 -logger = logging.getLogger(config['logging']['name']) -logger.setLevel(getattr(logging, config['logging']['level'], logging.DEBUG)) -os.makedirs(os.path.dirname(config['logging']['file']), exist_ok=True) -formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s') -for handler in (logging.FileHandler(config['logging']['file'], encoding='utf-8'), logging.StreamHandler()): - handler.setFormatter(formatter) - logger.addHandler(handler) - -def search_query(query: str, userid: str, db_type: str, limit: int = 10, offset: int = 0) -> List[Dict]: - """ - 根据用户输入的查询文本,在指定 db_type 的知识库中搜索与 userid 相关的文档。 - - 参数: - query (str): 用户输入的查询文本 - userid (str): 用户ID,用于过滤 - db_type (str): 数据库类型(例如 'textdb') - limit (int): 返回的最大结果数,默认为 10 - offset (int): 偏移量,用于分页,默认为 0 - - 返回: - List[Dict]: 搜索结果,每个元素为包含 text 和 metadata 的字典 - - 异常: - ValueError: 参数无效 - RuntimeError: 模型加载或 Milvus 操作失败 - """ - try: - # 参数验证 - if not query: - raise ValueError("查询文本不能为空") - if not userid or not db_type: - raise ValueError("userid 和 db_type 不能为空") - if "_" in userid or "_" in db_type: - raise ValueError("userid 和 db_type 不能包含下划线") - if len(userid) > 100 or len(db_type) > 100: - raise ValueError("userid 和 db_type 的长度应小于 100") - if limit <= 0 or limit > 16384: - raise ValueError("limit 必须在 1 到 16384 之间") - if offset < 0: - raise ValueError("offset 不能为负数") - if limit + offset > 16384: - raise ValueError("limit + offset 不能超过 16384") - - # 初始化嵌入模型 - model_path = TEXT_EMBEDDING_MODEL - if not os.path.exists(model_path): - raise ValueError(f"模型路径 {model_path} 不存在") - - embedding = HuggingFaceEmbeddings( - model_name=model_path, - model_kwargs={'device': 'cuda' if torch.cuda.is_available() else 'cpu'}, - encode_kwargs={'normalize_embeddings': True} - ) - try: - test_vector = embedding.embed_query("test") - if len(test_vector) != 1024: - raise ValueError(f"嵌入模型输出维度 {len(test_vector)} 不匹配预期 1024") - logger.debug("嵌入模型加载成功") - except Exception as e: - logger.error(f"嵌入模型加载失败: {str(e)}") - raise RuntimeError(f"嵌入模型加载失败: {str(e)}") - - # 将查询转换为向量 - query_vector = embedding.embed_query(query) - logger.debug(f"查询向量维度: {len(query_vector)}") - - # 连接到 Milvus - initialize_milvus_connection() - - # 检查集合是否存在 - collection_name = f"ragdb_{db_type}" - if not utility.has_collection(collection_name): - logger.warning(f"集合 {collection_name} 不存在") - return [] - - # 加载集合 - try: - collection = Collection(collection_name) - collection.load() - logger.debug(f"加载集合: {collection_name}") - except Exception as e: - logger.error(f"加载集合 {collection_name} 失败: {str(e)}") - raise RuntimeError(f"加载集合失败: {str(e)}") - - # 构造搜索参数 - search_params = { - "metric_type": "COSINE", # 与 vector.py 一致 - "params": {"nprobe": 10} # 优化搜索性能 - } - - # 构造过滤表达式 - expr = f"userid == '{userid}'" - logger.debug(f"搜索参数: {search_params}, 表达式: {expr}, limit: {limit}, offset: {offset}") - - # 执行搜索 - try: - results = collection.search( - data=[query_vector], - anns_field="vector", - param=search_params, - limit=limit, - expr=expr, - output_fields=["text", "userid", "document_id", "filename", "file_path", "upload_time", "file_type"], - offset=offset - ) - except Exception as e: - logger.error(f"搜索失败: {str(e)}") - raise RuntimeError(f"搜索失败: {str(e)}") - - # 处理搜索结果 - search_results = [] - for hits in results: - for hit in hits: - metadata = { - "userid": hit.entity.get("userid"), - "document_id": hit.entity.get("document_id"), - "filename": hit.entity.get("filename"), - "file_path": hit.entity.get("file_path"), - "upload_time": hit.entity.get("upload_time"), - "file_type": hit.entity.get("file_type") - } - result = { - "text": hit.entity.get("text"), - "distance": hit.distance, - "metadata": metadata - } - search_results.append(result) - logger.debug(f"命中: text: {result['text'][:200]}..., 距离: {hit.distance}, 元数据: {metadata}") - - logger.debug(f"搜索完成,返回 {len(search_results)} 条结果") - return search_results - - except Exception as e: - logger.error(f"搜索失败: {str(e)}") - import traceback - logger.debug(traceback.format_exc()) - raise - finally: - cleanup_milvus_connection() - -if __name__ == "__main__": - # 测试代码 - query = "知识图谱的知识融合是什么?" - userid = "testuser1" - db_type = "textdb" - limit = 2 - offset = 0 - - try: - results = search_query(query, userid, db_type, limit, offset) - print(f"搜索结果 ({len(results)} 条):") - for idx, result in enumerate(results, 1): - print(f"结果 {idx}:") - print(f"内容: {result['text'][:200]}...") - print(f"距离: {result['distance']}") - print(f"元数据: {result['metadata']}") - print("-" * 50) - except Exception as e: - print(f"搜索失败: {str(e)}") diff --git a/build/lib/rag/rag.bak.py b/build/lib/rag/rag.bak.py deleted file mode 100644 index fa8527e..0000000 --- a/build/lib/rag/rag.bak.py +++ /dev/null @@ -1,53 +0,0 @@ -from ahserver.serverenv import ServerEnv -from ahserver.configuredServer import ConfiguredServer -from ahserver.webapp import webapp -from appPublic.worker import awaitify -from query import search_query -from embed import embed -from deletefile import delete_document -import logging -import os - -# 配置日志 -logging.basicConfig( - level=logging.DEBUG, - format='%(asctime)s - %(levelname)s - %(message)s', - handlers=[ - logging.FileHandler(os.path.expanduser('~/rag/logs/rag.log'), encoding='utf-8'), - logging.StreamHandler() - ] -) -logger = logging.getLogger(__name__) - - -def get_module_dbname(name): - """ - 获取默认数据库名称,优先使用环境变量 RAG_DB_TYPE,未设置时返回 'textdb'。 - """ - return os.getenv('RAG_DB_TYPE', 'textdb') - - -def init(): - """ - 初始化 RAG 系统,绑定核心函数到 ServerEnv。 - """ - try: - logger.info("初始化 RAG 系统") - g = ServerEnv() - - # 绑定核心函数为异步版本 - g.embed = awaitify(embed) - g.query = awaitify(search_query) - g.delete = awaitify(delete_document) - g.get_module_dbname = get_module_dbname - - logger.info("RAG 系统初始化完成") - return g - except Exception as e: - logger.error(f"初始化失败: {str(e)}") - raise - - -if __name__ == '__main__': - logger.info("启动 RAG Web 服务器") - webapp(init) \ No newline at end of file diff --git a/build/lib/rag/rag.py b/build/lib/rag/rag.py deleted file mode 100644 index fa8527e..0000000 --- a/build/lib/rag/rag.py +++ /dev/null @@ -1,53 +0,0 @@ -from ahserver.serverenv import ServerEnv -from ahserver.configuredServer import ConfiguredServer -from ahserver.webapp import webapp -from appPublic.worker import awaitify -from query import search_query -from embed import embed -from deletefile import delete_document -import logging -import os - -# 配置日志 -logging.basicConfig( - level=logging.DEBUG, - format='%(asctime)s - %(levelname)s - %(message)s', - handlers=[ - logging.FileHandler(os.path.expanduser('~/rag/logs/rag.log'), encoding='utf-8'), - logging.StreamHandler() - ] -) -logger = logging.getLogger(__name__) - - -def get_module_dbname(name): - """ - 获取默认数据库名称,优先使用环境变量 RAG_DB_TYPE,未设置时返回 'textdb'。 - """ - return os.getenv('RAG_DB_TYPE', 'textdb') - - -def init(): - """ - 初始化 RAG 系统,绑定核心函数到 ServerEnv。 - """ - try: - logger.info("初始化 RAG 系统") - g = ServerEnv() - - # 绑定核心函数为异步版本 - g.embed = awaitify(embed) - g.query = awaitify(search_query) - g.delete = awaitify(delete_document) - g.get_module_dbname = get_module_dbname - - logger.info("RAG 系统初始化完成") - return g - except Exception as e: - logger.error(f"初始化失败: {str(e)}") - raise - - -if __name__ == '__main__': - logger.info("启动 RAG Web 服务器") - webapp(init) \ No newline at end of file diff --git a/build/lib/rag/vector.py b/build/lib/rag/vector.py deleted file mode 100644 index fc2a3fd..0000000 --- a/build/lib/rag/vector.py +++ /dev/null @@ -1,539 +0,0 @@ -import os -import uuid -import json -import yaml -from datetime import datetime -from typing import List, Dict, Optional -from pymilvus import connections, utility, Collection, CollectionSchema, FieldSchema, DataType -from langchain_milvus import Milvus -from langchain_huggingface import HuggingFaceEmbeddings -from langchain_core.documents import Document -import torch -import logging -import time - -# 加载配置文件 -CONFIG_PATH = os.getenv('CONFIG_PATH', '/share/wangmeihua/rag/conf/milvusconfig.yaml') -try: - with open(CONFIG_PATH, 'r', encoding='utf-8') as f: - config = yaml.safe_load(f) - MILVUS_DB_PATH = config['database']['milvus_db_path'] - TEXT_EMBEDDING_MODEL = config['models']['text_embedding_model'] -except Exception as e: - print(f"加载配置文件 {CONFIG_PATH} 失败: {str(e)}") - raise RuntimeError(f"无法加载配置文件: {str(e)}") - -# 配置日志 -logger = logging.getLogger(config['logging']['name']) -logger.setLevel(getattr(logging, config['logging']['level'], logging.DEBUG)) -os.makedirs(os.path.dirname(config['logging']['file']), exist_ok=True) -formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s') -for handler in (logging.FileHandler(config['logging']['file'], encoding='utf-8'), logging.StreamHandler()): - handler.setFormatter(formatter) - logger.addHandler(handler) - -def ensure_milvus_directory() -> None: - """确保 Milvus 数据库目录存在""" - db_dir = os.path.dirname(MILVUS_DB_PATH) - if not os.path.exists(db_dir): - os.makedirs(db_dir, exist_ok=True) - logger.debug(f"创建 Milvus 目录: {db_dir}") - if not os.access(db_dir, os.W_OK): - raise RuntimeError(f"Milvus 目录 {db_dir} 不可写") - -def initialize_milvus_connection() -> None: - """初始化 Milvus 连接,确保单一连接""" - try: - if not connections.has_connection("default"): - connections.connect("default", uri=MILVUS_DB_PATH) - logger.debug(f"已连接到 Milvus Lite,路径: {MILVUS_DB_PATH}") - else: - logger.debug("已存在 Milvus 连接,跳过重复连接") - except Exception as e: - logger.error(f"连接 Milvus 失败: {str(e)}") - raise RuntimeError(f"连接 Milvus 失败: {str(e)}") - -def cleanup_milvus_connection() -> None: - """清理 Milvus 连接,确保资源释放""" - try: - if connections.has_connection("default"): - connections.disconnect("default") - logger.debug("已断开 Milvus 连接") - time.sleep(3) - except Exception as e: - logger.warning(f"断开 Milvus 连接失败: {str(e)}") - -def get_vector_db(userid: str, db_type: str, documents: List[Document]) -> Milvus: - """ - 初始化或访问 Milvus Lite 向量数据库集合,按 db_type 组织,利用 userid 区分用户,document_id 区分文档,并插入文档。 - """ - try: - # 参数验证 - if not userid or not db_type: - raise ValueError("userid 和 db_type 不能为空") - if "_" in userid or "_" in db_type: - raise ValueError("userid 和 db_type 不能包含下划线") - if len(userid) > 100 or len(db_type) > 100: - raise ValueError("userid 和 db_type 的长度应小于 100") - if not documents or not all(isinstance(doc, Document) for doc in documents): - raise ValueError("documents 不能为空且必须是 Document 对象列表") - required_fields = ["userid", "document_id", "filename", "file_path", "upload_time", "file_type"] - for doc in documents: - if not all(field in doc.metadata and doc.metadata[field] for field in required_fields): - raise ValueError(f"文档元数据缺少必需字段或字段值为空: {doc.metadata}") - if doc.metadata["userid"] != userid: - raise ValueError(f"文档元数据的 userid {doc.metadata['userid']} 与输入 userid {userid} 不一致") - - ensure_milvus_directory() - initialize_milvus_connection() - - # 初始化嵌入模型 - model_path = TEXT_EMBEDDING_MODEL - if not os.path.exists(model_path): - raise ValueError(f"模型路径 {model_path} 不存在") - - embedding = HuggingFaceEmbeddings( - model_name=model_path, - model_kwargs={'device': 'cuda' if torch.cuda.is_available() else 'cpu'}, - encode_kwargs={'normalize_embeddings': True} - ) - try: - test_vector = embedding.embed_query("test") - if len(test_vector) != 1024: - raise ValueError(f"嵌入模型输出维度 {len(test_vector)} 不匹配预期 1024") - logger.debug(f"嵌入模型加载成功,输出维度: {len(test_vector)}") - except Exception as e: - logger.error(f"嵌入模型加载失败: {str(e)}") - raise RuntimeError(f"加载模型失败: {str(e)}") - - # 集合名称 - collection_name = f"ragdb_{db_type}" - if len(collection_name) > 255: - raise ValueError(f"集合名称 {collection_name} 超过 255 个字符") - logger.debug(f"集合名称: {collection_name}") - - # 定义 schema,包含所有固定字段 - fields = [ - FieldSchema(name="pk", dtype=DataType.VARCHAR, is_primary=True, max_length=36, auto_id=True), - FieldSchema(name="userid", dtype=DataType.VARCHAR, max_length=100), - FieldSchema(name="document_id", dtype=DataType.VARCHAR, max_length=36), - FieldSchema(name="text", dtype=DataType.VARCHAR, max_length=65535), - FieldSchema(name="vector", dtype=DataType.FLOAT_VECTOR, dim=1024), - FieldSchema(name="filename", dtype=DataType.VARCHAR, max_length=255), - FieldSchema(name="file_path", dtype=DataType.VARCHAR, max_length=1024), - FieldSchema(name="upload_time", dtype=DataType.VARCHAR, max_length=64), - FieldSchema(name="file_type", dtype=DataType.VARCHAR, max_length=64), - ] - schema = CollectionSchema( - fields=fields, - description=f"{db_type} 数据集合,跨用户使用,包含 document_id 和元数据字段", - auto_id=True, - primary_field="pk", - ) - - # 检查集合是否存在 - if utility.has_collection(collection_name): - try: - collection = Collection(collection_name) - existing_schema = collection.schema - expected_fields = {f.name for f in fields} - actual_fields = {f.name for f in existing_schema.fields} - vector_field = next((f for f in existing_schema.fields if f.name == "vector"), None) - - schema_compatible = False - if expected_fields == actual_fields and vector_field is not None and vector_field.dtype == DataType.FLOAT_VECTOR: - dim = vector_field.params.get('dim', None) if hasattr(vector_field, 'params') and vector_field.params else None - schema_compatible = dim == 1024 - logger.debug(f"检查集合 {collection_name} 的 schema: 字段匹配={expected_fields == actual_fields}, " - f"vector_field存在={vector_field is not None}, dtype={vector_field.dtype if vector_field else '无'}, " - f"dim={dim if dim is not None else '未定义'}") - if not schema_compatible: - logger.warning(f"集合 {collection_name} 的 schema 不兼容,原因: " - f"字段不匹配: {expected_fields.symmetric_difference(actual_fields) or '无'}, " - f"vector_field: {vector_field is not None}, " - f"dtype: {vector_field.dtype if vector_field else '无'}, " - f"dim: {vector_field.params.get('dim', '未定义') if vector_field and hasattr(vector_field, 'params') and vector_field.params else '未定义'}") - utility.drop_collection(collection_name) - else: - collection.load() - logger.debug(f"集合 {collection_name} 已存在并加载成功") - except Exception as e: - logger.error(f"加载集合 {collection_name} 失败: {str(e)}") - raise RuntimeError(f"加载集合失败: {str(e)}") - - # 创建新集合 - if not utility.has_collection(collection_name): - try: - collection = Collection(collection_name, schema) - collection.create_index( - field_name="vector", - index_params={"index_type": "AUTOINDEX", "metric_type": "COSINE"} - ) - collection.create_index( - field_name="userid", - index_params={"index_type": "INVERTED"} - ) - collection.create_index( - field_name="document_id", - index_params={"index_type": "INVERTED"} - ) - collection.create_index( - field_name="filename", - index_params={"index_type": "INVERTED"} - ) - collection.create_index( - field_name="file_path", - index_params={"index_type": "INVERTED"} - ) - collection.create_index( - field_name="upload_time", - index_params={"index_type": "INVERTED"} - ) - collection.create_index( - field_name="file_type", - index_params={"index_type": "INVERTED"} - ) - collection.load() - logger.debug(f"成功创建并加载集合: {collection_name}") - except Exception as e: - logger.error(f"创建集合 {collection_name} 失败: {str(e)}") - raise RuntimeError(f"创建集合失败: {str(e)}") - - # 初始化 Milvus 向量存储 - try: - vector_store = Milvus( - embedding_function=embedding, - collection_name=collection_name, - connection_args={"uri": MILVUS_DB_PATH}, - drop_old=False, - auto_id=True, - primary_field="pk", - ) - logger.debug(f"成功初始化 Milvus 向量存储: {collection_name}") - except Exception as e: - logger.error(f"初始化 Milvus 向量存储失败: {str(e)}") - raise RuntimeError(f"初始化向量存储失败: {str(e)}") - - # 插入文档 - try: - logger.debug(f"正在为 userid {userid} 插入 {len(documents)} 个文档到 {collection_name}") - for doc in documents: - logger.debug(f"插入文档元数据: {doc.metadata}") - vector_store.add_documents(documents=documents) - logger.debug(f"成功插入 {len(documents)} 个文档") - - # 立即查询验证 - collection = Collection(collection_name) - collection.load() - results = collection.query( - expr=f"userid == '{userid}'", - output_fields=["pk", "text", "document_id", "filename", "file_path", "upload_time", "file_type"], - limit=10 - ) - for result in results: - logger.debug(f"插入后查询结果: pk={result['pk']}, document_id={result['document_id']}, " - f"metadata={{'filename': '{result['filename']}', 'file_path': '{result['file_path']}', " - f"'upload_time': '{result['upload_time']}', 'file_type': '{result['file_type']}'}}") - except Exception as e: - logger.error(f"插入文档失败: {str(e)}") - raise RuntimeError(f"插入文档失败: {str(e)}") - - return vector_store - - except Exception as e: - logger.error(f"初始化 Milvus 向量存储失败: {str(e)}") - raise - finally: - cleanup_milvus_connection() - -def get_document_mapping(userid: str, db_type: str) -> Dict[str, Dict]: - """ - 获取指定 userid 和 db_type 下的 document_id 与元数据的映射。 - """ - try: - if not userid or "_" in userid: - raise ValueError("userid 不能为空且不能包含下划线") - if not db_type or "_" in db_type: - raise ValueError("db_type 不能为空且不能包含下划线") - - initialize_milvus_connection() - collection_name = f"ragdb_{db_type}" - if not utility.has_collection(collection_name): - logger.warning(f"集合 {collection_name} 不存在") - return {} - - collection = Collection(collection_name) - collection.load() - - results = collection.query( - expr=f"userid == '{userid}'", - output_fields=["userid", "document_id", "filename", "file_path", "upload_time", "file_type"], - limit=100 - ) - mapping = {} - for result in results: - doc_id = result.get("document_id") - if doc_id: - mapping[doc_id] = { - "userid": result.get("userid", ""), - "filename": result.get("filename", ""), - "file_path": result.get("file_path", ""), - "upload_time": result.get("upload_time", ""), - "file_type": result.get("file_type", "") - } - logger.debug(f"document_id: {doc_id}, metadata: {mapping[doc_id]}") - - logger.debug(f"找到 {len(mapping)} 个文档的映射") - return mapping - - except Exception as e: - logger.error(f"获取文档映射失败: {str(e)}") - raise RuntimeError(f"获取文档映射失败: {str(e)}") - -def list_user_collections() -> Dict[str, Dict]: - """ - 列出所有数据库类型(db_type)及其包含的用户(userid)与对应的文档(document_id)映射。 - """ - try: - ensure_milvus_directory() - initialize_milvus_connection() - collections = utility.list_collections() - - db_types_with_data = {} - for col in collections: - if col.startswith("ragdb_"): - db_type = col[len("ragdb_"):] - logger.debug(f"处理集合: {col} (db_type: {db_type})") - - collection = Collection(col) - collection.load() - - batch_size = 1000 - offset = 0 - user_document_map = {} - while True: - try: - results = collection.query( - expr="", - output_fields=["userid", "document_id"], - limit=batch_size, - offset=offset - ) - if not results: - break - for result in results: - userid = result.get("userid") - doc_id = result.get("document_id") - if userid and doc_id: - if userid not in user_document_map: - user_document_map[userid] = set() - user_document_map[userid].add(doc_id) - offset += batch_size - except Exception as e: - logger.error(f"查询集合 {col} 的用户和文档失败: {str(e)}") - break - - # 转换为列表以便序列化 - user_document_map = {uid: list(doc_ids) for uid, doc_ids in user_document_map.items()} - logger.debug(f"集合 {col} 中找到用户和文档映射: {user_document_map}") - - db_types_with_data[db_type] = { - "userids": user_document_map - } - - logger.debug(f"可用 db_types 和数据: {db_types_with_data}") - return db_types_with_data - - except Exception as e: - logger.error(f"列出集合和用户失败: {str(e)}") - raise - -def view_collection_details(userid: str) -> None: - """ - 查看特定 userid 在所有集合中的内容和容量,包含 document_id 和元数据。 - """ - try: - if not userid or "_" in userid: - raise ValueError("userid 不能为空且不能包含下划线") - - logger.debug(f"正在查看 userid {userid} 的集合") - ensure_milvus_directory() - initialize_milvus_connection() - collections = utility.list_collections() - db_types = [col[len("ragdb_"):] for col in collections if col.startswith("ragdb_")] - - if not db_types: - logger.debug(f"未找到任何集合") - return - - for db_type in db_types: - collection_name = f"ragdb_{db_type}" - if not utility.has_collection(collection_name): - logger.warning(f"集合 {collection_name} 不存在") - continue - - collection = Collection(collection_name) - collection.load() - - try: - all_pks = collection.query(expr=f"userid == '{userid}'", output_fields=["pk"], limit=10000) - num_entities = len(all_pks) - results = collection.query( - expr=f"userid == '{userid}'", - output_fields=["userid","text", "document_id", "filename", "file_path", "upload_time", "file_type"], - limit=10 - ) - logger.debug(f"集合 {collection_name} 中 userid {userid} 的文档数: {num_entities}") - - if num_entities == 0: - logger.debug(f"集合 {collection_name} 中 userid {userid} 无文档") - continue - - logger.debug(f"集合 {collection_name} 中 userid {userid} 的内容:") - for idx, doc in enumerate(results, 1): - metadata = { - "userid": doc.get("userid", ""), - "filename": doc.get("filename", ""), - "file_path": doc.get("file_path", ""), - "upload_time": doc.get("upload_time", ""), - "file_type": doc.get("file_type", "") - } - logger.debug(f"文档 {idx}: 内容: {doc.get('text', '')[:200]}..., 元数据: {metadata}") - except Exception as e: - logger.error(f"查询集合 {collection_name} 的文档失败: {str(e)}") - continue - - except Exception as e: - logger.error(f"无法查看 userid {userid} 的集合详情: {str(e)}") - raise - -def view_vector_data(db_type: str, userid: Optional[str] = None, document_id: Optional[str] = None, limit: int = 100) -> Dict[str, Dict]: - """ - 查看指定 db_type 中的向量数据,可选按 userid 和 document_id 过滤,包含完整元数据和向量。 - """ - try: - if not db_type or "_" in db_type: - raise ValueError("db_type 不能为空且不能包含下划线") - if limit <= 0 or limit > 16384: - raise ValueError("limit 必须在 1 到 16384 之间") - if userid and "_" in userid: - raise ValueError("userid 不能包含下划线") - if document_id and "_" in document_id: - raise ValueError("document_id 不能包含下划线") - - initialize_milvus_connection() - collection_name = f"ragdb_{db_type}" - if not utility.has_collection(collection_name): - logger.warning(f"集合 {collection_name} 不存在") - return {} - - collection = Collection(collection_name) - collection.load() - logger.debug(f"加载集合: {collection_name}") - - expr = [] - if userid: - expr.append(f"userid == '{userid}'") - if document_id: - expr.append(f"document_id == '{document_id}'") - expr = " && ".join(expr) if expr else "" - - results = collection.query( - expr=expr, - output_fields=["pk", "text", "document_id", "vector", "filename", "file_path", "upload_time", "file_type"], - limit=limit - ) - - vector_data = {} - for doc in results: - pk = doc.get("pk", str(uuid.uuid4())) - text = doc.get("text", "") - doc_id = doc.get("document_id", "") - vector = doc.get("vector", []) - metadata = { - "filename": doc.get("filename", ""), - "file_path": doc.get("file_path", ""), - "upload_time": doc.get("upload_time", ""), - "file_type": doc.get("file_type", "") - } - vector_data[pk] = { - "text": text, - "vector": vector, - "document_id": doc_id, - "metadata": metadata - } - logger.debug(f"pk: {pk}, text: {text[:200]}..., document_id: {doc_id}, vector_length: {len(vector)}, metadata: {metadata}") - - logger.debug(f"共找到 {len(vector_data)} 条向量数据") - return vector_data - - except Exception as e: - logger.error(f"查看向量数据失败: {str(e)}") - raise - -def main(): - userid = "testuser1" - db_type = "textdb" - - # logger.info("\n测试 1:带文档初始化") - # documents = [ - # Document( - # page_content="深度学习是基于深层神经网络的机器学习子集。", - # metadata={ - # "userid": userid, - # "document_id": str(uuid.uuid4()), - # "filename": "test_doc1.txt", - # "file_path": "/path/to/test_doc1.txt", - # "upload_time": datetime.now().isoformat(), - # "file_type": "txt" - # } - # ), - # Document( - # page_content="知识图谱是一个结构化的语义知识库。", - # metadata={ - # "userid": userid, - # "document_id": str(uuid.uuid4()), - # "filename": "test_doc2.txt", - # "file_path": "/path/to/test_doc2.txt", - # "upload_time": datetime.now().isoformat(), - # "file_type": "txt" - # } - # ), - # ] - # - # try: - # vector_store = get_vector_db(userid, db_type, documents=documents) - # logger.info(f"集合: ragdb_{db_type}") - # logger.info(f"成功为 userid {userid} 在 {db_type} 中插入文档") - # except Exception as e: - # logger.error(f"失败: {str(e)}") - - logger.info("\n测试 2:列出所有 db_types 和文档映射") - try: - db_types = list_user_collections() - logger.info(f"可用 db_types 和文档: {db_types}") - except Exception as e: - logger.error(f"失败: {str(e)}") - - logger.info(f"\n测试 3:查看 userid {userid} 的所有集合") - try: - view_collection_details(userid) - except Exception as e: - logger.error(f"失败: {str(e)}") - - # logger.info(f"\n测试 4:查看向量数据") - # try: - # vector_data = view_vector_data(db_type, userid=userid) - # logger.info(f"向量数据: {vector_data}") - # except Exception as e: - # logger.error(f"失败: {str(e)}") - - logger.info(f"\n测试 5:获取 userid {userid} 在{db_type}数据库的文档映射") - try: - mapping = get_document_mapping(userid, db_type) - logger.info(f"文档映射: {mapping}") - except Exception as e: - logger.error(f"失败: {str(e)}") - -if __name__ == "__main__": - main() \ No newline at end of file diff --git a/build/lib/rag/version.py b/build/lib/rag/version.py deleted file mode 100644 index b8023d8..0000000 --- a/build/lib/rag/version.py +++ /dev/null @@ -1 +0,0 @@ -__version__ = '0.0.1' diff --git a/rag/__pycache__/__init__.cpython-310.pyc b/rag/__pycache__/__init__.cpython-310.pyc deleted file mode 100644 index 0bb9ffe9e7c36bc24b52866f2f43b58b11ebbfa3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 173 zcmd1j<>g`kf_W?k8A?F(44TX@8G*u@ zjJLSs=%006j1CdL2& diff --git a/rag/__pycache__/deletefile.cpython-310.pyc b/rag/__pycache__/deletefile.cpython-310.pyc deleted file mode 100644 index ffd70fbe7c7ce093a4545f4b0df482d283b0ec29..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5338 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 CG@w@i diff --git a/rag/__pycache__/embed.cpython-310.pyc b/rag/__pycache__/embed.cpython-310.pyc deleted file mode 100644 index 6809d33d54765d842f6cf8d5360a6efaf21f9b4b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5672 zcmcIoS#TW18J?b*UCo|4EX!J1mc|Bym)MdG&Lxf_U<_1Y2e5%;3AI^mx1_cAqGv{y zR3l1QHppPd!q}J!Hmt%;z(5kf_y~E-V_xzyPpQJQvY+x2s#3ZCzk7Biuv2-;ZcR^j z|40Ab{rCTWJ(WZvs=)7dee2jms}-DTT5-3 zHli@4)=ncdx}at{rmklzSO<;KI87YUjwUjlOs%bCoeLz>^%&TuE4nhP5v6QwQLG>B zBSgXfkKtimG?`LpYf8zidQ2&=)fLt~d5|cX6j7#DvL4#TI$lv|J6p|Cv*Z=!v_1>c zte18)d(>I7pHPxXJ)YV}_9;xOtpTkoKx=180So$AU#*{+ObdEfrhsS9+F5M@H7R@6 z!E;^h`hePnGISN~KB59utF5Og&~XDu+_=Oc=BWa8!-6uRZd0C!XVNTPyNTT(boSN0 z#nM=N@^`d{t){Et>1APNq?C~`T|@g0=$Va8O243P8ZoFz*8*jL4bpXc)p^1Ry)Ltv zUZ2@Q*Jo~K18g(f!fu|`GPl6G+`w*u)quIfu8>{gN#A${J!EH^cm~bc%r2A|)b$4n zmW_SeDtV{G%2@AvnB^UA6~@`!V>zD7+l=pK6ZWoZv0y_TIAZxqxynHXhu zb{yIjo~6Uk5gjQMS+SC%jC--Wf4Fn|{r6@c{K4+~rZx^*ppp&l%aun7~)h=JWc;(=c#fxX^FFkwd{HNwu5$HGlH$J}nn>U9{rw3Z~lePMaxf*}a)oRfNIIPVPFT3oflP-5Plugw!+3bgNF^68a1$2aw z!;LBLhB3})%IPA5^6s0lym%F^NgeBrP^_srsw0y!^#+|CLvlG-AYgyfOR}LuO#>{LjEFP+nhn-Jd&odEuSF zo(uc^vuBs2AI%jVb`R$j-Y}&;_mVG+#zTE#PT2HS3ml=w@yBdvUOIck|KMf+)H@uO zOmUD7ediY6J?A8m_w||jYi~*kh!H5V5kkh>{zpflDY*R)PF{X)zefhH2o3EhB?3(S z(7XP}C;a&%z}W%4{)HL;?dO;8h?9WsV2|XW5J}=y2o70@cQ%AwXDtef*yW!&3|j`9 zDEH3X0(QP97pCwBu;j|`fi>Q-FTw3+ifz4iMq`->wUE+m=QST~$TX*g3 zgBggGh8Z2GPI{5sMp>CnRQVlQG(m?#omr<$-5}(K#FZf>^ymD#=oTz^06Li^`3(^z7Y1%+WZgwDf`lLA(8!{Ucy4}GQ z3;YKTwcJS%H=Q*|SiXDUH_P@7l#8twj)kKNACPWqa&-KiLPg-LUuS4!%yhq}RxD4) z9P5P{=0D4eE*(U#1@gae!!YJf$oV14hMiH!abm3?hgSA6&J7sO3qceqR5`9I4`GJ} ze&w|WiyC;G5c=g7s7I@XJE<~9*92TY30g3FH*27BNblC*SruW67XpJS+>6)%aM*AzzZXD@(3E8p`E;xFQE?75 zz&@UCV`(&&)=OYpx&n;og@ub*%X}Q&+lw^#EsqquR?ePu3Y^ib@J#4q{9a(=8?m?v zi%lrC;u@}I0>PqCsL8n6M(}TvPAH?ALE_>aC90+p{D=P;4|S2a27jUhEi8H^O_s)A zjaU~-kse~IVbZ4d5Q8L12Qg;UfAELVj23`Zc&PJu9vi`q?8mk#Dzy?UVA}xcyxT{05PAsR0QNy(eO^U?Z>LT1|6Cg*V zYC8&mfy}B1Ppt7}`r89YqR@^&M~tozIy&vR8xkphr36zT6{B5lh^|U0dlIyprVgqp z0BZq^N&+xZf!YHgDC~xzC0YPVbP5gq z!Wz?Vb6b|6#A9%KqTSOiPqomsfc`~oAKbI-*2%wsG~DN|k~XfAHl8v9X{ZV5b^tQ{ zARhtw{%?#PS(3;5QLzI68mtHasTC5)1U5yUiUeArCP2(OXDcKCi9J3!>>mu0us`>a zxkKCtOn?4`E6*O4(8WLZ;{OM#AlAWO;PeeB(;Rj2~Pa`fI63F zFUsuVk*w3Z#3o7p;e-DCFE5>a-k&?RcyDtI>(ci>&h)AH81r$KP@ zv??8eqH24Z4FOV{C_yS7ZT4Q9hrCgz$%}!l+ZNQ1x zanNR#J%DE>f;Du?tzH;W1sxVQ61feF=U=S9dTPi_ZP z#e!p^_PUGkZ6!a32IMQ84KyK$z+B78UxP4~L}NyEQhrq7VN@2DUv=cJb>STVEFNgQ z%xEmS$Uj`_@Gw+Zh2(Hi&(i4mPI~*NaD+mm)vpx1CKtkE4%G3@kvN-JZ_p7 zs`5hFmhSLV?1+HQ2Lz%N4(1saw8iHbM4UMrAYy3SJ$F5FuV<8Udl@ZoOL!+hmjV}G zZz9`;uOLK=NVmaiY&%TkKEU!tXe4&ooaIW5X3JR6ABVw~q@y`X0~U#nbPW2<{l>wv zS-F*f?m#b|MCrNkrN?rLwnTZt9hZF4xUE#7PLbV#cOdHlC=8|Hka56u%|9T!HfZ(! myG>1!Zi&2n;%b)TM{ojtHvfH(<=q=pnpVJQ;e5$k|q=p-#U)KMa3Dr>7Y!|evR#9r)~ z0m;p*LkpxtNj#v7w8d)?>$ELVavYK3A^idQ;E$=APpJ|XN2OA#d`Y>e#LTaIb{9aT zWVgDf=k@D;uV24&3eC+?1;2m(x5xLbH5BD*@v`_QW*2npl(usZK*D1E-slF&3&c zv)B|##`l1ZrfAAUfGGL!7R4GqNQff-e}lG1$*^Sfhn<*vcCtdw{k~qHZIoC7TS7J#jIXgL6>2Vl~}QA0Yjcm#q6C^%82@y@j1E+2Tkloxrg;hjP;d`vmLKNP1Cw@k?{*%CjP7n=K2LiIWycI?8F1*YxPqPwjZ($Ejx?-u-xKa>G=_0`JU&71SbPR`!^WaiAv z)7L*YoCx4X_1(|r{`lUY;q>Bd=4@r=AK(5i?x5j?vW3x6?B;2CGsnDOmW{Klr;TK? zYB z?!!_=mN&EZVK0(*a;d${%voNC7;+?T6NsbzIkDF>)p)?DFKddopub>Z;P2Pa6k;*v4KSGt-TyiX}>2V$Fzao4fq+^cSyGKRGe;_8({8 zIa6;tpex83J;VB8tQ#lmHDrPgb7l430-7G@D zG@YoOU<|)E5g>tA_EB!uzr0*4ppaU}2(=C^yq~fAO#v zmxNQakalv|;C`&MmZOWQ8qr8t?I2O2sZpZKUQ_vIOs0jh)PFGyn$d(7S#ewlL`~|V zulrBHRER%71GoYOSFpJpbc0kIQ~3rOEJM(IovT)GDV4w!;skt@z^q?L8*p{t4vRhN z(C^|OXsgeO5#PBKV^pD`e;}_YFDoO2hH2zzSSZ5;DXWs(sH@hwrA=ks)om!t1Q}D{ zSA(wV>LTuehi_e-Hg_xaS-fM8#=Dgsr5uvu7BQ~ibW?=Cvk0P7eJO;-Y3nhyM?oYC zl)(-5Stj?1$Z^}EeZE`-@%LE-S+Wn#;d2Onn%ofoPdAJfL5rd_p%KVAB;@Rd-6r{r z$UaKj&j*F424@^eSYeEKMv>-QYzl%7N z=ylT2jJXefGjnmW?vrNUx&-GYOy>OWg)ckt1>C>U@P5V+R54>8-ZBi>;UW`HAF{Ou z5l_ytrkyF|hmD!ajp~ilP6y~zPhF~h@Y>@22aO$k9cDZ-wA&-YiBKspWDS>sLq(Gg zm$ab)YnZo)c|Dba-${ul+;)n`0bHW#!r2H1BZ8JC9POy$DlbIK#3jd-al%zsw%64I z>Mq|1r6t1etNoijVk7dw+*mqeW_>4T$$372t-R<^7Evl)$Q6f$XSM{f6-sGC%~n%2 zFK?xUKxqF^wvaZn)-VS3WzI4zslI!3cJig_SE|P*X0IO|G;Wc; zl9oVJEd|isB8hq7j}swJ7ttBHLy)q)&L~d+3U?=t!~4WID!smSFoR%&#DL0CA|{<5 z!lVqiw&m$EEZ~W7WSgU&zSkKU!M0i&Nr*=>UZ|!sh_*sy>hD|q_pM&It}njO?*naL z@?GCA;=cxh^*lOFZ6#Vjxr?nM_&WF}*gE{NpLLft zU<7H{)H+3t(j5XFYyCHa^7U9(ZZ@mzb zG{S%`qiy)N(~gtDO2m#%k#e~7gx$11=7wo!kMPM|7ke?*EKj%Mg7kH6u+r)_ z3oZ{F*`pj7))af$;Xe_j94Dxgu)2*doO7k!?#BIUNvzm|>Waq8@n6Ib4Es+}CqZ5< zcE^NLZgqo@*y{2!cNtxUh|md-yNur17#aR(Bv5X1+uT-okUzVvl`gkU?CrKxx?NOA z;Ok%P<@}uSeT5c~rWv_7C=^A<$U5lD(lC0$DRvORVS?5wmJU+yNeq5O=0q^S5pwEb~xzo4ARD zclM)e9!WVHvEu(lt)4hmo&4SO)z_-W-n|}q&SS( z58iML>|axX<7a$A3#`<>0mw8avD%CN)XX|;JLd(2eo@Ku?O5$;nfypW)+T%h-u=uX z!cdelZrTjlOXC}cY8O16vhhqBWtXhqz3?c@`*=}|Qey{EyjxyuuW6_ErE0~g4Awd< z0x60Y*?uU^isHt-5RmgiSpu+Tbj-#Fh{8c+CIM)XpYEsy%hQS`5*%%RA(Qvoc0c>X zGds8MN-^1{Duv=pnvI5d4YQgfOqg$!AdJH+J_li?KYvSMrmg!X8F4;*(Lqv4x?CuQ47O zktv?!l>fchPL!&Pvfv&tFG|ytnWrhs9ADV~C=ji;&}p$4Qm6>~)`!=qxLYfvnqW(n->X=!B z7L{|o*iF!Qh^V($-Chd{z2Y61VQm$5ZAE1)l0T$+GfX z<}SZfJ@@6@r=P*UPG3C&6PvyM=FG*T)e~o6c>afi8rrot5i4e{d{({oe)Z$`r@yEO zmwf5k^tDq0_}PoI?_Hd|etP!i>$BI-W9`iOYxwlzV^%+&oIm#xmcw5$%Z3l$vUM9Y z9%JD)w>Tnu^zBQMKR;-|gGxs_#19F^gLZ+Z_wn^&9E1Gxme4lFEiS4MF0xTBzQ}o+ z_>{**ed6h(45l#7g;V3M=uQc6#4<-%DqBDyq5GfzxDW<^MN9^ZJd?M%Eyl5D9jHjI zX37!lwo}##zCGh>hP_jM#tUHv{uTAZW7{8j?#Es@XO1zN0dI%JHX#h9IZ7(6GstmxXrf>_2%nd2yz%)KBxCP1B##W<^f+(HpokdXrcMtn~cVZ`@a zzYv=g#EJkUCKbi>R4P_JOM!hO;Iwj1WFSrDJAhVpTy~w_#rO8q=JOtx+ zvTPWEPqD_)g%wznZ&P^2r0&Pairdf|s@E;if9ii;-qhCK`#^M$Cci7ifT!L1A47x^RsaA1 diff --git a/rag/__pycache__/kgc.cpython-310.pyc b/rag/__pycache__/kgc.cpython-310.pyc deleted file mode 100644 index 0ee4b1596cedc88d4a3a56862edc2c4fccc15140..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7528 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