from rag.uapi_service import APIService from sqlor.dbpools import DBPools from appPublic.log import debug, error, info import time import traceback import json import math import uuid import os from rag.service_opts import get_service_params, sor_get_service_params, sor_get_embedding_mode, get_embedding_mode from rag.rag_operations import RagOperations from langchain_core.documents import Document REAL_PHOTO_ROOT = "/home/wangmeihua/kyrag/files" helptext = """kyrag API: 1. 得到kdb表: path: /v1/get_kdbs headers: { "Content-Type": "application/json" } response: [{"id": "1", "name": "textdb", "description": "文本数据库"}, {"id": "testkdb", "name": "testkdb", "description": ""}, {"id": "Vdtbt3qBfocteit1HIxVH", "name": "trm", "description": ""}] 2. 向量检索文本块: path: /v1/fusedsearch headers: { "Content-Type": "application/json" } data: { "query": "什么是知识抽取。", "fiids":["1"], "limit":5 } 3、docs文档 path: /v1/docs 4. 纯文本插入接口: path: /v1/textinsert headers: { "Content-Type": "application/json" } data: { "text": "要插入的纯文本内容", "fiid": "知识库ID", "db_type": "数据库类型(如 textdb)" } response: { "status": "success", "userid": "用户组织ID", "collection_name": "ragdb_{dbtype}", "message": "文本成功嵌入并处理三元组", "status_code": 200, "unique_triples": ["提取的三元组列表"], "timings": {"total": 0.123} } error response: { "status": "error", "userid": "用户组织ID", "collection_name": "ragdb_{dbtype}", "message": "错误信息", "status_code": 400, "timings": {"total": 0.123} } 4. 添加用户消息到记忆: path: /v1/add_user_messages headers: { "Content-Type": "application/json" } data: { "orgid": "用户组织ID", "messages": [{"role": "user", "content": "消息内容"}, ...] } 5. 获取用户所有记忆: path: /v1/get_user_memories headers: { "Content-Type": "application/json" } data: { "orgid": "用户组织ID", "limit": 10 # 可选,默认为 10 } """ async def docs(request, params_kw, *params, **kw): return helptext async def get_kdbs(request, params_kw, *params, **kw): """返回 kdb 表的全部内容,返回 JSON""" f = kw.get('get_userorgid') orgid = await f() debug(f"orgid: {orgid},{f=}") debug(f"params_kw: {params_kw}") db = DBPools() dbname = kw.get('get_module_dbname')('rag') sql_opts = """ SELECT id, name, description FROM kdb WHERE orgid = ${orgid}$ """ try: async with db.sqlorContext(dbname) as sor: opts_result = await sor.sqlExe(sql_opts, {"orgid": orgid}) if not opts_result: error("未找到 kdb 表记录") return json.dumps({"status": "error", "message": "未找到记录"}) return json.dumps(opts_result, ensure_ascii=False) except Exception as e: error(f"查询 kdb 表失败: {str(e)}, 堆栈: {traceback.format_exc()}") return json.dumps({"status": "error", "message": str(e)}) except Exception as e: error(f"列出用户文件失败: {str(e)}, 堆栈: {traceback.format_exc()}") result.update({ "status": "error", "files_by_knowledge_base": {}, "message": f"列出用户文件失败: {str(e)}", "status_code": 400 }) return result async def fusedsearch(request, params_kw, *params): """ 融合搜索,调用服务化端点 """ kw = request._run_ns f = kw.get('get_userorgid') orgid = await f() debug(f"orgid: {orgid},{f=}") f = kw.get('get_user') userid = await f() debug(f"params_kw: {params_kw}") # orgid = "04J6VbxLqB_9RPMcgOv_8" # userid = "04J6VbxLqB_9RPMcgOv_8" query = params_kw.get('query', '').strip() img_path = params_kw.get('image') if isinstance(img_path, str): img_path = img_path.strip() relative_part = img_path.lstrip("/") real_img_path = os.path.join(REAL_PHOTO_ROOT, relative_part) if not os.path.exists(real_img_path): raise FileNotFoundError(f"图片不存在: {real_img_path}") img_path = real_img_path debug(f"自动修复图片路径成功: {img_path}") # 统一模式处理 limit 参数,为了对接dify和coze raw_limit = params_kw.get('limit') or ( params_kw.get('retrieval_setting', {}).get('top_k') if isinstance(params_kw.get('retrieval_setting'), dict) else None ) # 标准化为整数值 if raw_limit is None: limit = 5 # 两个来源都不存在时使用默认值 elif isinstance(raw_limit, (int, float)): limit = int(raw_limit) # 数值类型直接转换 elif isinstance(raw_limit, str): try: # 字符串转换为整数 limit = int(raw_limit) except (TypeError, ValueError): limit = 5 # 转换失败使用默认值 else: limit = 5 # 其他意外类型使用默认值 debug(f"limit: {limit}") raw_fiids = params_kw.get('fiids') or params_kw.get('knowledge_id') # # 标准化为列表格式 if raw_fiids is None: fiids = [] # 两个参数都不存在 elif isinstance(raw_fiids, list): fiids = [str(item).strip() for item in raw_fiids] # 已经是列表 elif isinstance(raw_fiids, str): # fiids = [f.strip() for f in raw_fiids.split(',') if f.strip()] try: # 尝试解析 JSON 字符串 parsed = json.loads(raw_fiids) if isinstance(parsed, list): fiids = [str(item).strip() for item in parsed] # JSON 数组转为字符串列表 else: # 处理逗号分隔的字符串或单个 ID 字符串 fiids = [f.strip() for f in raw_fiids.split(',') if f.strip()] except json.JSONDecodeError: # 如果不是合法 JSON,按逗号分隔 fiids = [f.strip() for f in raw_fiids.split(',') if f.strip()] elif isinstance(raw_fiids, (int, float)): fiids = [str(int(raw_fiids))] # 数值类型转为字符串列表 else: fiids = [] # 其他意外类型 debug(f"fiids: {fiids}") # 验证 fiids的orgid与orgid = await f()是否一致 await _validate_fiids_orgid(fiids, orgid, kw) service_params = await get_service_params(orgid) if not service_params: raise ValueError("无法获取服务参数") # 获取嵌入模式 embedding_mode = await get_embedding_mode(orgid) debug(f"检测到 embedding_mode = {embedding_mode}(0=文本, 1=多模态)") # 情况1:query 和 image 都为空 → 报错 if not query and not img_path: raise ValueError("查询文本和图片不能同时为空") # 情况2:query 和 image 都存在 → 报错(你当前业务不允许同时传) if query and img_path: raise ValueError("查询文本和图片只能二选一,不能同时提交") # 3. 只有图片 → 以图搜图 走纯多模态分支 if img_path and not query: try: debug("检测到纯图片查询,执行以图搜图") rag_ops = RagOperations() timings = {} start_time = time.time() # 直接生成图片向量 img_vector = await rag_ops.generate_image_vector( request, img_path, service_params, userid, timings, embedding_mode ) # 向量搜索(多取 50 条再截断,和文本分支保持一致) search_results = await rag_ops.vector_search( request, img_vector, orgid, fiids, limit + 50, service_params, userid, timings ) timings["total_time"] = time.time() - start_time # 可选:搜索完后删除图片,省磁盘(看你需求) # try: # os.remove(img_path) # except: # pass final_results = [] for item in search_results[:limit]: final_results.append({ "text": item["text"], "distance": item["distance"] }) return { "results": final_results, "timings": timings } except Exception as e: error(f"融合搜索失败: {str(e)}, 堆栈: {traceback.format_exc()}") return { "records": [], "timings": {"total_time": time.time() - start_time if 'start_time' in locals() else 0}, "error": str(e) } if not img_path and query: try: timings = {} start_time = time.time() rag_ops = RagOperations() query_entities = await rag_ops.extract_entities(request, query, service_params, userid, timings) all_triplets = await rag_ops.match_triplets(request, query, query_entities, orgid, fiids, service_params, userid, timings) combined_text = _combine_query_with_triplets(query, all_triplets) query_vector = await rag_ops.generate_query_vector(request, combined_text, service_params, userid, timings, embedding_mode) search_results = await rag_ops.vector_search(request, query_vector, orgid, fiids, limit + 50, service_params, userid, timings) use_rerank = True if use_rerank and search_results: final_results = await rag_ops.rerank_results(request, combined_text, search_results, limit, service_params, userid, timings) debug(f"final_results: {final_results}") else: final_results = [{k: v for k, v in r.items() if k != 'rerank_score'} for r in search_results] formatted_results = rag_ops.format_search_results(final_results, limit) timings["total_time"] = time.time() - start_time debug(f"融合搜索完成,返回 {len(formatted_results)} 条结果,总耗时: {timings['total_time']:.3f} 秒") return { "records": formatted_results, "timings": timings } except Exception as e: error(f"融合搜索失败: {str(e)}, 堆栈: {traceback.format_exc()}") return { "records": [], "timings": {"total_time": time.time() - start_time if 'start_time' in locals() else 0}, "error": str(e) } # async def fusedsearch(request, params_kw, *params): # """ # 融合搜索,调用服务化端点 # # """ # kw = request._run_ns # f = kw.get('get_userorgid') # orgid = await f() # debug(f"orgid: {orgid},{f=}") # f = kw.get('get_user') # userid = await f() # debug(f"params_kw: {params_kw}") # # orgid = "04J6VbxLqB_9RPMcgOv_8" # # userid = "04J6VbxLqB_9RPMcgOv_8" # query = params_kw.get('query', '') # # 统一模式处理 limit 参数,为了对接dify和coze # raw_limit = params_kw.get('limit') or ( # params_kw.get('retrieval_setting', {}).get('top_k') # if isinstance(params_kw.get('retrieval_setting'), dict) # else None # ) # # # 标准化为整数值 # if raw_limit is None: # limit = 5 # 两个来源都不存在时使用默认值 # elif isinstance(raw_limit, (int, float)): # limit = int(raw_limit) # 数值类型直接转换 # elif isinstance(raw_limit, str): # try: # # 字符串转换为整数 # limit = int(raw_limit) # except (TypeError, ValueError): # limit = 5 # 转换失败使用默认值 # else: # limit = 5 # 其他意外类型使用默认值 # debug(f"limit: {limit}") # raw_fiids = params_kw.get('fiids') or params_kw.get('knowledge_id') # # # # 标准化为列表格式 # if raw_fiids is None: # fiids = [] # 两个参数都不存在 # elif isinstance(raw_fiids, list): # fiids = [str(item).strip() for item in raw_fiids] # 已经是列表 # elif isinstance(raw_fiids, str): # # fiids = [f.strip() for f in raw_fiids.split(',') if f.strip()] # try: # # 尝试解析 JSON 字符串 # parsed = json.loads(raw_fiids) # if isinstance(parsed, list): # fiids = [str(item).strip() for item in parsed] # JSON 数组转为字符串列表 # else: # # 处理逗号分隔的字符串或单个 ID 字符串 # fiids = [f.strip() for f in raw_fiids.split(',') if f.strip()] # except json.JSONDecodeError: # # 如果不是合法 JSON,按逗号分隔 # fiids = [f.strip() for f in raw_fiids.split(',') if f.strip()] # elif isinstance(raw_fiids, (int, float)): # fiids = [str(int(raw_fiids))] # 数值类型转为字符串列表 # else: # fiids = [] # 其他意外类型 # # debug(f"fiids: {fiids}") # # # 验证 fiids的orgid与orgid = await f()是否一致 # await _validate_fiids_orgid(fiids, orgid, kw) # # service_params = await get_service_params(orgid) # if not service_params: # raise ValueError("无法获取服务参数") # # 获取嵌入模式 # embedding_mode = await get_embedding_mode(orgid) # debug(f"检测到 embedding_mode = {embedding_mode}(0=文本, 1=多模态)") # # try: # timings = {} # start_time = time.time() # rag_ops = RagOperations() # # query_entities = await rag_ops.extract_entities(request, query, service_params, userid, timings) # all_triplets = await rag_ops.match_triplets(request, query, query_entities, orgid, fiids, service_params, # userid, timings) # combined_text = _combine_query_with_triplets(query, all_triplets) # query_vector = await rag_ops.generate_query_vector(request, combined_text, service_params, userid, timings, embedding_mode) # search_results = await rag_ops.vector_search(request, query_vector, orgid, fiids, limit + 50, service_params, # userid, timings) # # use_rerank = False # if use_rerank and search_results: # final_results = await rag_ops.rerank_results(request, combined_text, search_results, limit, service_params, # userid, timings) # debug(f"final_results: {final_results}") # else: # final_results = [{k: v for k, v in r.items() if k != 'rerank_score'} for r in search_results] # # formatted_results = rag_ops.format_search_results(final_results, limit) # timings["total_time"] = time.time() - start_time # debug(f"融合搜索完成,返回 {len(formatted_results)} 条结果,总耗时: {timings['total_time']:.3f} 秒") # # return { # "records": formatted_results, # "timings": timings # } # except Exception as e: # error(f"融合搜索失败: {str(e)}, 堆栈: {traceback.format_exc()}") # return { # "records": [], # "timings": {"total_time": time.time() - start_time if 'start_time' in locals() else 0}, # "error": str(e) # } # async def text_insert(text: str, fiid: str, orgid: str, db_type: str): async def textinsert(request, params_kw, *params): kw = request._run_ns f = kw.get('get_userorgid') orgid = await f() debug(f"orgid: {orgid},{f=}") f = kw.get('get_user') userid = await f() text = params_kw.get('text', '') fiid = params_kw.get('fiid') db_type = params_kw.get('db_type') id = str(uuid.uuid4()) debug(f"params_kw: {params_kw}") # orgid = "04J6VbxLqB_9RPMcgOv_8" # userid = "04J6VbxLqB_9RPMcgOv_8" # text = text # fiid = fiid # orgid = orgid # db_type = db_type # id = str(uuid.uuid4()) # debug(f'Inserting document: text={text},userid={orgid}, db_type={db_type}, knowledge_base_id={fiid}') timings = {} start_total = time.time() result = { "status": "error", "userid": orgid, "collection_name": f"ragdb_{db_type}", "message": "", "status_code": 400 } try: # 验证必填字段 if not orgid or not fiid or not text or not db_type: raise ValueError("orgid、fiid、db_type 和 text 不能为空") if len(orgid) > 32 or len(fiid) > 255: raise ValueError("orgid 或 fiid 的长度超出限制") # 获取服务参数 service_params = await get_service_params(orgid) if not service_params: raise ValueError("无法获取服务参数") rag_ops = RagOperations() # 生成嵌入向量 embedding = await rag_ops.generate_query_vector(request, text, service_params, userid, timings) # 插入 Milvus fields = { "text": text, "knowledge_base_id": fiid, "userid": orgid, "vector": embedding, "document_id": id } chunks_data = await rag_ops.insert_to_vector_text(request, db_type, fields, service_params, userid, timings) # 抽取三元组 document = Document(page_content=text) chunks = [document] triples = await rag_ops.extract_triples( request, chunks, service_params, userid, timings ) # 插入 Neo4j await rag_ops.insert_to_graph_db( request, triples, id, fiid, orgid, service_params, userid, timings ) timings["total"] = time.time() - start_total result.update({ "status": "success", "unique_triples": triples, "message": f"文本成功嵌入并处理三元组", "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 _validate_fiids_orgid(fiids, orgid, kw): """验证 fiids 的 orgid 与当前用户 orgid 是否一致""" if fiids: db = DBPools() dbname = kw.get('get_module_dbname')('rag') sql_opts = """SELECT orgid FROM kdb WHERE id = ${id}$""" try: async with db.sqlorContext(dbname) as sor: result = await sor.sqlExe(sql_opts, {"id": fiids[0]}) if not result: raise ValueError(f"未找到 fiid={fiids[0]} 的记录") kdb_orgid = result[0].get('orgid') if kdb_orgid != orgid: raise ValueError(f"orgid 不一致: kdb.orgid={kdb_orgid}, user orgid={orgid}") except Exception as e: error(f"orgid 验证失败: {str(e)}") raise async def _validate_fiids_orgid(fiids, orgid, kw): """验证 fiids 的 orgid 与当前用户 orgid 是否一致""" if fiids: db = DBPools() dbname = kw.get('get_module_dbname')('rag') sql_opts = """SELECT orgid FROM kdb WHERE id = ${id}$""" try: async with db.sqlorContext(dbname) as sor: result = await sor.sqlExe(sql_opts, {"id": fiids[0]}) if not result: raise ValueError(f"未找到 fiid={fiids[0]} 的记录") kdb_orgid = result[0].get('orgid') if kdb_orgid != orgid: raise ValueError(f"orgid 不一致: kdb.orgid={kdb_orgid}, user orgid={orgid}") except Exception as e: error(f"orgid 验证失败: {str(e)}") raise def _combine_query_with_triplets(query, triplets): """拼接查询文本和三元组文本""" triplet_texts = [] for triplet in triplets: head = triplet.get('head', '') type_ = triplet.get('type', '') tail = triplet.get('tail', '') if head and type_ and tail: triplet_texts.append(f"{head} {type_} {tail}") else: debug(f"无效三元组: {triplet}") combined_text = query if triplet_texts: combined_text += "".join(triplet_texts) debug(f"拼接文本: {combined_text[:200]}... (总长度: {len(combined_text)}, 三元组数量: {len(triplet_texts)})") return combined_text