mirror of
https://github.com/fastapiadmin/FastapiAdmin.git
synced 2026-09-22 13:05:18 +00:00
- 将 ApplicationQueryParams 改为 ApplicationQueryParam - 同步更新相关导入和函数参数类型注解 - 修改 PaginationQueryParams 为 PaginationQueryParam refactor(demo): 重命名查询参数类以统一命名风格 - 将 DemoQueryParams 改为 DemoQueryParam - 同步更新相关导入和函数参数类型注解 - 修改 PaginationQueryParams 为 PaginationQueryParam refactor(gencode): 优化代码生成模块的模型和服务层结构 - 统一模型名称后缀为 Schema,调整相关引用 - 规范 Pydantic schema 的命名和定义 - 删除无用的 Python DAO 模板文件 - 调整导入路径,统一使用 app 目录下的模块路径 - 改进服务层方法签名,添加返回类型注解 - 使用自定义异常 CustomException 替代旧异常 - 统一成功响应格式为 SuccessResponse - 优化代码生成服务中的数据库操作 DAO 调用参数传递 - 优化代码生成业务表和字段模型的字段定义,添加注释和默认值 - 优化生成代码路径处理逻辑和异常信息提示 - 整合分页查询参数定义,统一分页模型 - 修正多个服务方法的参数类型和返回类型 - 删除无用的导入和多余注释,提升代码整洁度
206 lines
6.9 KiB
Python
206 lines
6.9 KiB
Python
# -*- coding: utf-8 -*-
|
|
|
|
import asyncio
|
|
import httpx, aiofiles
|
|
import numpy as np
|
|
from openai import AsyncOpenAI
|
|
|
|
from app.core.logger import logger
|
|
|
|
|
|
class AIClient:
|
|
def __init__(self, kb_filepath=None, model="qwen3:4b", embedding_model="nomic-embed-text"):
|
|
# AI模型配置
|
|
self.model = model
|
|
self.embedding_model = embedding_model
|
|
|
|
# 创建HTTP客户端
|
|
self.http_client = httpx.AsyncClient(
|
|
timeout=30.0,
|
|
follow_redirects=True
|
|
)
|
|
|
|
# 初始化OpenAI客户端(用于与Ollama交互)
|
|
self.client = AsyncOpenAI(
|
|
api_key="ollama",
|
|
base_url="http://127.0.0.1:11434/v1",
|
|
http_client=self.http_client
|
|
)
|
|
|
|
# 知识库相关属性
|
|
self.docs = []
|
|
self.embeds = None
|
|
|
|
# 如果提供了知识库文件路径,则加载知识库
|
|
self.kb_loaded = False
|
|
self.kb_filepath = kb_filepath
|
|
|
|
# RAG提示词模板
|
|
self.prompt_template = """
|
|
基于以下知识回答用户的问题:
|
|
1: %s
|
|
2: %s
|
|
3: %s
|
|
4: %s
|
|
5: %s
|
|
|
|
用户的问题: %s
|
|
|
|
请根据提供的知识,用中文简洁准确地回答问题。如果提供的知识不足以回答,请说明这一点。
|
|
"""
|
|
|
|
# 知识库相关异步方法
|
|
async def load_kb(self):
|
|
"""异步加载知识库文件"""
|
|
if not self.kb_filepath or self.kb_loaded:
|
|
return
|
|
|
|
try:
|
|
# 异步读取文件
|
|
async with aiofiles.open(self.kb_filepath, 'r', encoding='utf-8') as f:
|
|
content = await f.read()
|
|
|
|
self.docs = self.split_content(content)
|
|
self.embeds = await self.encode(self.docs)
|
|
self.kb_loaded = True
|
|
logger.info(f"成功加载知识库,包含 {len(self.docs)} 个文档片段")
|
|
except Exception as e:
|
|
logger.error(f"加载知识库失败: {str(e)}")
|
|
raise
|
|
|
|
@staticmethod
|
|
def split_content(content):
|
|
"""将内容分割成文档块"""
|
|
chunks = []
|
|
# 按换行符分割成行
|
|
lines = content.splitlines()
|
|
for line in lines:
|
|
stripped_line = line.strip()
|
|
if stripped_line:
|
|
chunks.append(stripped_line)
|
|
return chunks
|
|
|
|
async def encode(self, texts):
|
|
"""异步使用Ollama生成嵌入向量"""
|
|
embeds = []
|
|
for text in texts:
|
|
try:
|
|
# 使用AsyncOpenAI客户端异步生成嵌入
|
|
response = await self.client.embeddings.create(
|
|
model=self.embedding_model,
|
|
input=text
|
|
)
|
|
embeds.append(response.data[0].embedding)
|
|
except Exception as e:
|
|
logger.error(f"生成嵌入向量失败 for text: {text[:30]}...: {str(e)}")
|
|
# 对于失败的嵌入,添加一个零向量
|
|
embeds.append([0.0] * 768) # 假设nomic-embed-text生成768维向量
|
|
return np.array(embeds)
|
|
|
|
@staticmethod
|
|
def similarity(e1, e2):
|
|
"""计算余弦相似度"""
|
|
dot_product = np.dot(e1, e2)
|
|
norm_e1 = np.linalg.norm(e1)
|
|
norm_e2 = np.linalg.norm(e2)
|
|
|
|
if norm_e1 == 0 or norm_e2 == 0:
|
|
return 0.0 # 避免除以零
|
|
|
|
return dot_product / (norm_e1 * norm_e2)
|
|
|
|
async def search(self, text, top_k=5):
|
|
"""异步在知识库中搜索相似文本"""
|
|
# 确保知识库已加载
|
|
if not self.kb_loaded:
|
|
await self.load_kb()
|
|
|
|
if not self.embeds.any():
|
|
logger.warning("知识库为空,无法进行搜索")
|
|
return []
|
|
|
|
# 生成查询文本的嵌入向量
|
|
query_embed = (await self.encode([text]))[0]
|
|
|
|
# 计算与所有文档的相似度
|
|
sims = [(idx, self.similarity(query_embed, doc_embed))
|
|
for idx, doc_embed in enumerate(self.embeds)]
|
|
|
|
# 按相似度排序
|
|
sims.sort(key=lambda x: x[1], reverse=True)
|
|
|
|
# 返回前top_k个匹配结果
|
|
top_matches = [self.docs[idx] for idx, _ in sims[:top_k]]
|
|
return top_matches
|
|
|
|
# RAG相关异步方法
|
|
async def build_rag_prompt(self, query):
|
|
"""异步构建RAG提示词"""
|
|
# 搜索知识库获取相关上下文
|
|
context = await self.search(query)
|
|
|
|
# 确保上下文有5个元素,不足的用空字符串填充
|
|
context += [""] * (5 - len(context))
|
|
|
|
# 构建提示词
|
|
return self.prompt_template % (
|
|
context[0], context[1], context[2], context[3], context[4], query
|
|
)
|
|
|
|
# AI处理相关方法
|
|
async def process(self, query: str, use_rag=True):
|
|
"""处理查询并返回流式响应,支持RAG模式"""
|
|
system_prompt = """你是一个有用的AI助手,可以帮助用户回答问题和提供帮助。请用中文回答用户的问题。"""
|
|
|
|
# 如果启用RAG,构建增强提示词
|
|
if use_rag and self.kb_filepath:
|
|
user_query = await self.build_rag_prompt(query)
|
|
else:
|
|
user_query = query
|
|
|
|
try:
|
|
# 使用 await 调用异步客户端
|
|
response = await self.client.chat.completions.create(
|
|
model=self.model,
|
|
messages=[
|
|
{"role": "system", "content": system_prompt},
|
|
{"role": "user", "content": user_query}
|
|
],
|
|
stream=True
|
|
)
|
|
|
|
# 流式返回响应
|
|
async for chunk in response:
|
|
if chunk.choices and chunk.choices[0].delta.content:
|
|
yield chunk.choices[0].delta.content
|
|
|
|
except Exception as e:
|
|
logger.error(f"AI处理查询失败: {str(e)}")
|
|
yield f"抱歉,处理您的请求时出现了错误: {str(e)}"
|
|
|
|
async def close(self):
|
|
"""关闭客户端连接"""
|
|
if hasattr(self, 'client'):
|
|
await self.client.close()
|
|
if hasattr(self, 'http_client'):
|
|
await self.http_client.aclose()
|
|
|
|
|
|
async def chat_query(message: str, kb_filepath=None):
|
|
"""处理聊天查询的异步函数"""
|
|
# 创建AI客户端实例,传入知识库文件路径
|
|
# message = message + "/no_think"
|
|
ai_client = AIClient(kb_filepath=kb_filepath)
|
|
try:
|
|
# 处理消息,启用RAG
|
|
async for response in ai_client.process(message, use_rag=True):
|
|
print(response, end='', flush=True)
|
|
finally:
|
|
# 确保关闭客户端连接
|
|
await ai_client.close()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
# 在异步事件循环中运行聊天查询
|
|
asyncio.run(chat_query("帕金森氏症介绍,怎么治疗", kb_filepath='帕金森氏症en.txt'))
|