mirror of
https://github.com/fastapiadmin/FastapiAdmin.git
synced 2026-09-22 05:02:57 +00:00
refactor: 重构服务层方法命名规范
fix: 修复文件下载和删除功能 fix: 修复Redis哈希获取方法返回值类型 fix: 修复权限检查逻辑 perf: 优化部门和服务详情查询性能 perf: 优化角色数据范围显示 style: 清理无用导入和注释 style: 统一CRUD方法命名 docs: 更新main.py中的命令说明 chore: 移动IP定位工具类位置 chore: 更新.gitignore忽略迁移版本文件
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from fastapi import APIRouter, Depends, Path, Query, Body, WebSocket, Request
|
||||
from fastapi import APIRouter, Depends, Path, Body, WebSocket
|
||||
from fastapi.responses import JSONResponse, StreamingResponse
|
||||
|
||||
from app.common.response import StreamResponse, SuccessResponse
|
||||
@@ -41,54 +41,54 @@ async def chat_controller(
|
||||
|
||||
|
||||
@MCPRouter.get("/detail/{id}", summary="获取 MCP 服务器详情", description="获取 MCP 服务器详情")
|
||||
async def get_mcp_detail_controller(
|
||||
async def detail_controller(
|
||||
id: int = Path(..., description="MCP ID"),
|
||||
auth: AuthSchema = Depends(AuthPermission(permissions=["ai:mcp:query"]))
|
||||
) -> JSONResponse:
|
||||
result_dict = await McpService.get_mcp_detail_service(auth=auth, id=id)
|
||||
result_dict = await McpService.detail_service(auth=auth, id=id)
|
||||
logger.info(f"获取 MCP 服务器详情成功 {id}")
|
||||
return SuccessResponse(data=result_dict, msg="获取 MCP 服务器详情成功")
|
||||
|
||||
|
||||
@MCPRouter.get("/list", summary="查询 MCP 服务器列表", description="查询 MCP 服务器列表")
|
||||
async def get_mcp_list_controller(
|
||||
async def list_controller(
|
||||
page: PaginationQueryParam = Depends(),
|
||||
search: McpQueryParam = Depends(),
|
||||
auth: AuthSchema = Depends(AuthPermission(permissions=["ai:mcp:query"]))
|
||||
) -> JSONResponse:
|
||||
result_dict_list = await McpService.get_mcp_list_service(auth=auth, search=search, order_by=page.order_by)
|
||||
result_dict_list = await McpService.list_service(auth=auth, search=search, order_by=page.order_by)
|
||||
result_dict = await PaginationService.paginate(data_list=result_dict_list, page_no=page.page_no, page_size=page.page_size)
|
||||
logger.info(f"查询 MCP 服务器列表成功")
|
||||
return SuccessResponse(data=result_dict, msg="查询 MCP 服务器列表成功")
|
||||
|
||||
|
||||
@MCPRouter.post("/create", summary="创建 MCP 服务器", description="创建 MCP 服务器")
|
||||
async def create_mcp_controller(
|
||||
async def create_controller(
|
||||
data: McpCreateSchema,
|
||||
auth: AuthSchema = Depends(AuthPermission(permissions=["ai:mcp:create"]))
|
||||
) -> JSONResponse:
|
||||
result_dict = await McpService.create_mcp_service(auth=auth, data=data)
|
||||
result_dict = await McpService.create_service(auth=auth, data=data)
|
||||
logger.info(f"创建 MCP 服务器成功: {result_dict}")
|
||||
return SuccessResponse(data=result_dict, msg="创建 MCP 服务器成功")
|
||||
|
||||
|
||||
@MCPRouter.put("/update/{id}", summary="修改 MCP 服务器", description="修改 MCP 服务器")
|
||||
async def update_mcp_controller(
|
||||
async def update_controller(
|
||||
data: McpUpdateSchema,
|
||||
id: int = Path(..., description="MCP ID"),
|
||||
auth: AuthSchema = Depends(AuthPermission(permissions=["ai:mcp:update"]))
|
||||
) -> JSONResponse:
|
||||
result_dict = await McpService.update_mcp_service(auth=auth, id=id, data=data)
|
||||
result_dict = await McpService.update_service(auth=auth, id=id, data=data)
|
||||
logger.info(f"修改 MCP 服务器成功: {result_dict}")
|
||||
return SuccessResponse(data=result_dict, msg="修改 MCP 服务器成功")
|
||||
|
||||
|
||||
@MCPRouter.delete("/delete", summary="删除 MCP 服务器", description="删除 MCP 服务器")
|
||||
async def delete_mcp_controller(
|
||||
async def delete_controller(
|
||||
ids: list[int] = Body(..., description="ID列表"),
|
||||
auth: AuthSchema = Depends(AuthPermission(permissions=["ai:mcp:delete"]))
|
||||
) -> JSONResponse:
|
||||
await McpService.delete_mcp_service(auth=auth, ids=ids)
|
||||
await McpService.delete_service(auth=auth, ids=ids)
|
||||
logger.info(f"删除 MCP 服务器成功: {ids}")
|
||||
return SuccessResponse(msg="删除 MCP 服务器成功")
|
||||
|
||||
|
||||
@@ -1,205 +0,0 @@
|
||||
# -*- 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'))
|
||||
@@ -1,22 +1,20 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from typing import AsyncGenerator, List, Dict, Optional, Any
|
||||
from typing import List, Dict, Optional, Any
|
||||
|
||||
from app.core.exceptions import CustomException
|
||||
from app.core.logger import logger
|
||||
from app.api.v1.module_system.auth.schema import AuthSchema
|
||||
from app.utils.ai_util import AIClient
|
||||
from .schema import McpCreateSchema, McpUpdateSchema, McpOutSchema, ChatQuerySchema
|
||||
from .param import McpQueryParam
|
||||
from .crud import McpCRUD
|
||||
from .model import McpModel
|
||||
|
||||
|
||||
class McpService:
|
||||
"""MCP服务层"""
|
||||
|
||||
@classmethod
|
||||
async def get_mcp_detail_service(cls, auth: AuthSchema, id: int) -> Dict[str, Any]:
|
||||
async def detail_service(cls, auth: AuthSchema, id: int) -> Dict[str, Any]:
|
||||
"""详情"""
|
||||
obj = await McpCRUD(auth).get_by_id_crud(id=id)
|
||||
if not obj:
|
||||
@@ -24,7 +22,7 @@ class McpService:
|
||||
return McpOutSchema.model_validate(obj).model_dump()
|
||||
|
||||
@classmethod
|
||||
async def get_mcp_list_service(cls, auth: AuthSchema, search: Optional[McpQueryParam] = None, order_by: Optional[List[Dict[str, str]]] = None) -> List[Dict[str, Any]]:
|
||||
async def list_service(cls, auth: AuthSchema, search: Optional[McpQueryParam] = None, order_by: Optional[List[Dict[str, str]]] = None) -> List[Dict[str, Any]]:
|
||||
"""列表查询"""
|
||||
if order_by:
|
||||
order_by = eval(str(order_by))
|
||||
@@ -32,7 +30,7 @@ class McpService:
|
||||
return [McpOutSchema.model_validate(obj).model_dump() for obj in obj_list]
|
||||
|
||||
@classmethod
|
||||
async def create_mcp_service(cls, auth: AuthSchema, data: McpCreateSchema) -> Dict[str, Any]:
|
||||
async def create_service(cls, auth: AuthSchema, data: McpCreateSchema) -> Dict[str, Any]:
|
||||
"""创建"""
|
||||
obj = await McpCRUD(auth).get_by_name_crud(name=data.name)
|
||||
if obj:
|
||||
@@ -41,7 +39,7 @@ class McpService:
|
||||
return McpOutSchema.model_validate(obj).model_dump()
|
||||
|
||||
@classmethod
|
||||
async def update_mcp_service(cls, auth: AuthSchema, id: int, data: McpUpdateSchema) -> Dict[str, Any]:
|
||||
async def update_service(cls, auth: AuthSchema, id: int, data: McpUpdateSchema) -> Dict[str, Any]:
|
||||
"""更新"""
|
||||
obj = await McpCRUD(auth).get_by_id_crud(id=id)
|
||||
if not obj:
|
||||
@@ -53,7 +51,7 @@ class McpService:
|
||||
return McpOutSchema.model_validate(obj).model_dump()
|
||||
|
||||
@classmethod
|
||||
async def delete_mcp_service(cls, auth: AuthSchema, ids: List[int]) -> None:
|
||||
async def delete_service(cls, auth: AuthSchema, ids: List[int]) -> None:
|
||||
"""删除"""
|
||||
if len(ids) < 1:
|
||||
raise CustomException(msg='删除失败,删除对象不能为空')
|
||||
|
||||
Reference in New Issue
Block a user