import httpx from openai import OpenAI import os import requests import json import asyncio import aiohttp from typing import List, Dict, Any,Optional from tqdm import tqdm # 进度条支持(可选) from openai import AsyncOpenAI from neo4j_graphrag.embeddings.base import Embedder #now_model = "Qwen3.5-35B-A3B" #now_model = "Qwen3-32B" from config import LLM_CONFIG, EMBEDDING_CONFIG # 模块级单例客户端,避免每次调用都创建新连接 _async_client: AsyncOpenAI = None def _get_async_client() -> AsyncOpenAI: """获取或创建 AsyncOpenAI 单例客户端""" global _async_client if _async_client is None: _async_client = AsyncOpenAI( api_key=LLM_CONFIG["api_key"], base_url=LLM_CONFIG["base_url"], ) return _async_client class OpenaiAPI: @staticmethod def get_embeddings(embedding_texts: List[str]): # ✅ 2. 使用 EMBEDDING_CONFIG base_url = EMBEDDING_CONFIG.get("base_url_v2", EMBEDDING_CONFIG["base_url"].replace("/embeddings", "")) embedding_base_url = os.path.join(base_url, "embeddings") with httpx.Client(timeout=60) as client: response = client.post( embedding_base_url, json={"model": EMBEDDING_CONFIG["model"], "input": embedding_texts}, headers={"Authorization": f"Bearer {EMBEDDING_CONFIG['api_key']}"}, ) # 注意:batch_embeddings 返回的是列表,get_embeddings 原代码只取了第一个元素的 embedding # 这里保持原逻辑,如果输入是列表,通常期望返回所有 embedding,原代码可能有误,此处仅替换配置源 data = response.json()["data"] if isinstance(data, list) and len(data) > 0: return data[0]["embedding"] return [] @staticmethod def batch_embeddings(embedding_texts: List[str]): # ✅ 3. 使用 EMBEDDING_CONFIG base_url = EMBEDDING_CONFIG.get("base_url_v2", EMBEDDING_CONFIG["base_url"].replace("/embeddings", "")) embedding_base_url = os.path.join(base_url, "embeddings") with httpx.Client(timeout=60) as client: response = client.post( embedding_base_url, json={"model": EMBEDDING_CONFIG["model"], "input": embedding_texts}, headers={"Authorization": f"Bearer {EMBEDDING_CONFIG['api_key']}"}, ) embedding_list = response.json()["data"] return embedding_list @staticmethod def rerank_query(query: str, documents: List[str], top_n: str = 3): headers = { 'Content-Type': 'application/json', 'Authorization': f'Bearer {EMBEDDING_CONFIG["api_key"]}' # 假设 Rerank 也使用相同的 Key 或在此处扩展配置 } data = { "model": os.getenv("OPENAI_RERANKER_MODEL", "bge-reranker-v2-m3"), # Rerank 模型未在 config.py 定义,暂保留环境变量或可添加到 config "query": query, "top_n": top_n, "documents": documents } # 假设 Rerank 地址与 Embedding 地址相近,或者需要单独配置。此处暂用环境变量或默认逻辑 rerank_base = os.getenv("OPENAI_API_RERANKER_BASE", EMBEDDING_CONFIG["base_url_v2"]) response = requests.post(f"{rerank_base}/rerank", headers=headers, data=json.dumps(data)) results_list = [] if response.status_code == 200: result_json = response.json() for result in result_json["results"]: results_list.append({ "index": result["index"], "text": result["document"]["text"], "scores": result["relevance_score"] }) return results_list else: raise Exception(f"Error: {response.status_code}\n{response.text}") @staticmethod def embedding_cosine_similarity(text_1, text_2): headers = { 'Content-Type': 'application/json', 'Authorization': f'Bearer {EMBEDDING_CONFIG["api_key"]}' } data = { "text_1": text_1, "text_2": text_2, "model": "bge-rerank" } rerank_base = os.getenv("OPENAI_API_RERANKER_BASE", EMBEDDING_CONFIG["base_url_v2"]) response = requests.post(f"{rerank_base}/score", headers=headers, data=json.dumps(data)) if response.status_code == 200: result_json = response.json() score = result_json["data"][0]["score"] return score else: raise Exception(f"Error: {response.status_code}\n{response.text}") @staticmethod def open_api_chat(query: str, model: str = None): # ✅ 4. 使用 LLM_CONFIG if model is None: model = LLM_CONFIG["model"] response = OpenAI( api_key=LLM_CONFIG["api_key"], base_url=LLM_CONFIG["base_url"], ).chat.completions.create( model=model, messages=[ {"role": "system", "content": "You are a helpful assistant."}, {"role": "user", "content": query}, ], temperature=LLM_CONFIG["temperature"], stream=False ) return response.choices[0].message.content @staticmethod async def open_api_chat_async(query: str, model: str = None, temperature: float = None) -> str: if model is None: model = LLM_CONFIG["model"] if temperature is None: temperature = LLM_CONFIG["temperature"] client = AsyncOpenAI( api_key=LLM_CONFIG["api_key"], base_url=LLM_CONFIG["base_url"] ) response = await client.chat.completions.create( model=model, messages=[ {"role": "system", "content": "You are a helpful assistant."}, {"role": "user", "content": query} ], max_tokens=LLM_CONFIG["max_tokens"], temperature=temperature, stream=False, extra_body={"chat_template_kwargs": {"enable_thinking": False}} ) return response.choices[0].message.content @staticmethod async def open_api_vl_without_thinking( image_url: str ) -> str: """ 专门用于从图片中提取【设备名称】和【故障现象】。 自动过滤思考过程,仅返回核心结果。 参数: image_url: Base64 格式的图片数据 (data:image/...;base64,...) model: 模型名称,默认为配置中的模型 custom_instruction: 额外的特定指令 (可选) """ # 1. 确定模型名称 model = LLM_CONFIG.get("model") # 2. 构建强约束的 System Prompt # 核心目标:禁止思考标签,禁止废话,只给结果 system_prompt = ( "你是一个视觉分析专家。你的任务是从图片中识别内容并进行描述。\n" "【严格约束】\n" "1. 绝对禁止输出 , , 等任何思考过程标签。\n" "2. 绝对禁止输出'好的'、'根据图片'、'分析如下'等开场白或结束语。\n" "3. 直接输出最终结论,格式必须严格遵守下面的模板。" ) # 3. 构建针对性的 User Prompt default_task = ( "请分析这张图片,描述图片内容,重点关注以下信息\n" "1. 图片整体阐述了什么主题,描述了哪些内容\n" "请用专业、客观、简短的语言描述。" ) final_user_prompt = default_task # 4. 初始化客户端 client = _get_async_client() # 5. 构建请求参数 kwargs = { "model": model, "messages": [ {"role": "system", "content": system_prompt}, { "role": "user", "content": [ {"type": "text", "text": final_user_prompt}, {"type": "image_url", "image_url": {"url": image_url}} ] } ], "temperature": 0.1, # 低温度以保证事实准确性 "stream": False, "max_tokens": LLM_CONFIG.get("max_tokens"), } # 尝试通过参数关闭思考 (取决于后端支持情况) kwargs["extra_body"] = {"chat_template_kwargs": {"enable_thinking": False}} try: print(f"🚀 正在调用 {model} 进行图片内容分析...") response = await client.chat.completions.create(**kwargs) raw_content = response.choices[0].message.content or "" print(f"✅ 分析完成:\n{raw_content}") return raw_content except Exception as e: import traceback error_msg = f"❌ 图片内容分析失败:{str(e)}\n{traceback.format_exc()}" print(error_msg) return f"Error: {str(e)}" @staticmethod async def open_api_chat_async_json(query: str, model: str = None, temperature: float = None) -> str: if model is None: model = LLM_CONFIG["model"] if temperature is None: temperature = LLM_CONFIG["temperature"] client = AsyncOpenAI( api_key=LLM_CONFIG["api_key"], base_url=LLM_CONFIG["base_url"] ) response = await client.chat.completions.create( model=model, messages=[ {"role": "system", "content": "You are a helpful assistant that outputs only valid JSON."}, {"role": "user", "content": query} ], temperature=temperature, stream=False, max_tokens=LLM_CONFIG["max_tokens"], response_format={"type": "json_object"}, extra_body={"chat_template_kwargs": {"enable_thinking": False}} ) return response.choices[0].message.content @staticmethod def open_api_caht_without_thinking(query: str, model: str = None): if model is None: model = LLM_CONFIG["model"] response = OpenAI( api_key=LLM_CONFIG["api_key"], base_url=LLM_CONFIG["base_url"], ).chat.completions.create( model=model, messages=[ {"role": "system", "content": "You are a helpful assistant."}, {"role": "user", "content": query}, ], temperature=LLM_CONFIG["temperature"], stream=False, extra_body={"chat_template_kwargs": {"enable_thinking": False}} ) return response.choices[0].message.content @staticmethod def open_api_chat_stream(query: str, model: str = None): if model is None: model = LLM_CONFIG["model"] response = OpenAI( api_key=LLM_CONFIG["api_key"], base_url=LLM_CONFIG["base_url"], ).chat.completions.create( model=model, messages=[ {"role": "system", "content": "You are a helpful assistant."}, {"role": "user", "content": query}, ], temperature=LLM_CONFIG["temperature"], stream=True ) for chunk in response: content = chunk.choices[0].delta.content if content: yield content.encode('utf-8') @staticmethod def openai_chat(query: str, timeout: int = None): if timeout is None: timeout = LLM_CONFIG["timeout"] try: client = OpenAI( api_key=LLM_CONFIG["api_key"], base_url=LLM_CONFIG["base_url"], timeout=timeout ) response = client.chat.completions.create( model=LLM_CONFIG["model"], messages=[ {"role": "user", "content": query}, ], temperature=LLM_CONFIG["temperature"], stream=False, response_format={"type": "json_object"}, extra_body={"chat_template_kwargs": {"enable_thinking": False}} ) return response.choices[0].message.content except Exception as e: print(f"调用OpenAI API时出错: {e}") return None @staticmethod async def openai_chat_aysnc(query: str, timeout: int = None) -> str | None: if timeout is None: timeout = LLM_CONFIG["timeout"] try: client = AsyncOpenAI( api_key=LLM_CONFIG["api_key"], base_url=LLM_CONFIG["base_url"], timeout=timeout ) response = await client.chat.completions.create( model=LLM_CONFIG["model"], messages=[ {"role": "user", "content": query}, ], temperature=LLM_CONFIG["temperature"], stream=False, response_format={"type": "json_object"}, extra_body={"chat_template_kwargs": {"enable_thinking": False}} ) return response.choices[0].message.content except Exception as e: print(f"调用 OpenAI API 时出错: {e}") return None finally: await client.close() @staticmethod async def openai_chat_aysnc_nothink(query: str, timeout: int = None) -> str | None: if timeout is None: timeout = LLM_CONFIG["timeout"] try: client = AsyncOpenAI( api_key=LLM_CONFIG["api_key"], base_url=LLM_CONFIG["base_url"], timeout=timeout ) # 注意:原代码中此方法使用了不同的模型 "46-qwen3.6-35B" # 如果需要统一,请使用 LLM_CONFIG["model"],否则需单独配置 model_to_use = LLM_CONFIG["model"] response = await client.chat.completions.create( model=model_to_use, messages=[ {"role": "user", "content": query}, ], temperature=LLM_CONFIG["temperature"], stream=False, extra_body={"chat_template_kwargs": {"enable_thinking": False}} ) return response.choices[0].message.content except Exception as e: print(f"调用 OpenAI API 时出错: {e}") return None finally: await client.close() @staticmethod def generate_embedding(text: str) -> Optional[List[float]]: try: client = OpenAI( api_key=EMBEDDING_CONFIG["api_key"], base_url=EMBEDDING_CONFIG.get("base_url_v2", EMBEDDING_CONFIG["base_url"].replace("/embeddings", "")), ) response = client.embeddings.create( model=EMBEDDING_CONFIG["model"], input=text ) return response.data[0].embedding except Exception as e: print(f"生成embedding失败: {e}") return None async def batch_chunk_process( markdown_context: str, chunks: List[Dict[str, Any]], batch_size: int = 5, timeout: int = 30 ) -> List[Dict[str, Any]]: """ 批量处理文档块(异步并发) Args: markdown_context: 完整文档内容 chunks: 文档块列表,每个块需包含"text"键 batch_size: 并发请求数 (默认5) timeout: 单个请求超时时间(秒) Returns: 处理后的文档块列表 """ # 初始化参数 api_key = os.getenv("OPENAI_API_KEY") base_url = os.getenv("OPENAI_API_BASE") model = os.getenv("OPENAI_MODEL", "qwen3.5-35b") if not api_key or not base_url: raise ValueError("Missing OpenAI API configuration") async def process_single_chunk( session: aiohttp.ClientSession, chunk: Dict[str, Any], semaphore: asyncio.Semaphore ) -> Dict[str, Any]: """处理单个chunk的异步函数""" async with semaphore: text = chunk.get("text", "").strip() if not text: return chunk prompt = f""" {markdown_context} {text} Please provide a succinct context for this chunk within the document. Respond ONLY with the context text, no additional commentary. """ chinese_prompt = f""" <文档> {markdown_context} 以下是需要定位到整体文档中的片段 <片段> {text} 请简要说明该片段在整体文档中的上下文关系,以提升片段搜索效果。只需给出简洁的上下文说明,无需其他内容。 """ try: async with session.post( f"{base_url}/chat/completions", headers={"Authorization": f"Bearer {api_key}"}, json={ "model": model, "messages": [ {"role": "system", "content": "You are a helpful assistant."}, {"role": "user", "content": chinese_prompt} ], "temperature": 0.1, "extra_body": {"chat_template_kwargs": {"enable_thinking": False}} }, timeout=timeout ) as response: if response.status == 200: data = await response.json() if data.get("choices"): chunk["text"] = data["choices"][0]["message"]["content"].strip() else: print(f"API error for chunk: {await response.text()}") except Exception as e: print(f"Error processing chunk: {e}") return chunk # 创建信号量控制并发数 semaphore = asyncio.Semaphore(batch_size) # 异步处理所有chunks async with aiohttp.ClientSession() as session: tasks = [ process_single_chunk(session, chunk, semaphore) for chunk in chunks ] # 使用tqdm显示进度(可选) processed_chunks = [] for future in tqdm(asyncio.as_completed(tasks), total=len(tasks)): processed_chunks.append(await future) return processed_chunks def prepare_rag_prompt(query, documents): docs = "" for i, doc in enumerate(documents): docs += f"文本信息{i+1}:{doc}\n" return f"""你作为一个文档检索问答专家,请你从以下文本信息中选取有用的内容作为参考,仔细思考后用中文回答用户的问题,如果文本中没有关于问题的答案,则回答不知道: ----- {docs} ----- 用户提问:{query} 你的回答:""" def query_rewrite(query): prompt = f""" 你是一个专业的搜索查询优化助手。你的任务是根据用户输入的原始查询,生成更清晰、更易被搜索引擎理解的改写版本。请遵循以下规则: 1. **保持原意**:改写后的查询必须与用户意图一致,不能曲解原意。 2. **简洁清晰**:去掉冗余词,优化表达,但不要过度缩写。 3. **结构化**:如果查询适合拆解,可以适当结构化(如添加隐含的限定条件)。 4. **自然语言**:确保改写后的查询仍然符合自然语言习惯。 5. **多语言支持**:如果查询是中文,优先用中文改写;如果是英文,用英文优化。 请改写以下查询,直接返回优化后的内容,不要额外解释: 【用户查询开始】 {query} 【用户查询结束】 """ return OpenaiAPI.open_api_chat(prompt) def RAG_PROMPT(QUERY,CONTEXT): PROMPT = f""" ### Task: Respond to the user query using the provided context, incorporating inline citations in the format [source_id] **only when the tag is explicitly provided** in the context. ### Guidelines: - If you don't know the answer, clearly state that. - If uncertain, ask the user for clarification. - Respond in the same language as the user's query. - If the context is unreadable or of poor quality, inform the user and provide the best possible answer. - If the answer isn't present in the context but you possess the knowledge, explain this to the user and provide the answer using your own understanding. - When you find corresponding content in the provided sources, try to quote it verbatim as much as possible. - When quoting from sources, reproduce the original text verbatim—do not omit, paraphrase, or add content. - **Only include inline citations using [source_id] (e.g., [1], [2]) when a `` tag is explicitly provided in the context.** - If the source file has a .glb extension, directly mark the corresponding content with the source_id in the relevant position. - Do not cite if the or tag is not provided in the context. - Do not use XML tags in your response. - **If none of the provided sources contain relevant information to answer the question, respond only with: "没有找到相关来源" (No relevant sources found). Do not add any other content. - Ensure citations are concise and directly related to the information provided. ### Example of Citation: If the user asks about a specific topic and the information is found in "whitepaper.pdf" with a provided , the response should include the citation like so: * "According to the study, the proposed method increases efficiency by 20% [whitepaper.pdf]." If no is present, the response should omit the citation. ### Output: Provide a clear and direct response to the user's query, including inline citations in the format [source_id] only when the tag is present in the context. {CONTEXT} {QUERY} """ return PROMPT # for chunk in response: # content = chunk.choices[0].delta.content # if content: # print(content, end='', flush=True) # print('\n') # return response class LocalBgeM3Embeddings(Embedder): def __init__(self, base_url: str, api_key: str, model_name: str = "bge-m3"): self.base_url = base_url.rstrip("/") self.api_key = api_key self.model_name = model_name def embed_query(self, text: str) -> List[float]: return self.embed_documents([text])[0] def embed_documents(self, texts: List[str]) -> List[List[float]]: embedding_base_url = os.path.join(self.base_url, "embeddings") with httpx.Client(timeout=60) as client: response = client.post( embedding_base_url, json={"model": self.model_name, "input": texts}, headers={"Authorization": f"Bearer {self.api_key}"}, ) embedding_list = [item["embedding"] for item in response.json()["data"]] return embedding_list