kgrag/modelsAPI/model_api.py

600 lines
24 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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. 绝对禁止输出 <think>, <think>, <reasoning> 等任何思考过程标签。\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"""
<document>{markdown_context}</document>
<chunk>{text}</chunk>
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 <source_id> 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 `<source_id>` 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 <source_id> 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 <source_id> , the response should include the citation like so:
* "According to the study, the proposed method increases efficiency by 20% [whitepaper.pdf]."
If no <source_id> 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 <source_id> tag is present in the context.
<context>
{CONTEXT}
</context>
<user_query>
{QUERY}
</user_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