From f1fe10f22c5ef96357d1465c2c3ffe573a6cca24 Mon Sep 17 00:00:00 2001 From: Wenjie Zhang Date: Mon, 22 Sep 2025 22:02:57 +0800 Subject: [PATCH] =?UTF-8?q?feat(models):=20=E6=B7=BB=E5=8A=A0=E5=B5=8C?= =?UTF-8?q?=E5=85=A5=E5=92=8C=E8=81=8A=E5=A4=A9=E6=A8=A1=E5=9E=8B=E7=8A=B6?= =?UTF-8?q?=E6=80=81=E6=A3=80=E6=9F=A5=E6=8E=A5=E5=8F=A3=20#282?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在知识路由中新增获取指定嵌入模型和聊天模型状态的接口 - 实现获取所有嵌入模型和聊天模型状态的功能 - 更新前端API以支持模型状态检查,增强用户体验 - 优化日志记录格式,提升调试信息的可读性 --- AGENTS.md | 25 ++- server/routers/knowledge_router.py | 33 +++ server/routers/system_router.py | 33 +++ src/models/chat.py | 65 ++++++ src/models/embed.py | 79 ++++++- src/utils/logging_config.py | 4 +- web/src/apis/knowledge_api.js | 23 ++ web/src/apis/system_api.js | 24 +++ web/src/components/ModelSelectorComponent.vue | 198 ++++++++++++++++-- web/src/views/SettingView.vue | 95 ++++++++- 10 files changed, 544 insertions(+), 35 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index f2f03852..533d72ca 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -11,13 +11,15 @@ Yuxi-Know/ ├── server/ # 服务端代码(部分) ├── src/ # 主要源代码目录 │ ├── agents/ # 智能体应用 -│ ├── config/ # 配置文件 +│ ├── config/ # 配置文件 │ ├── knowledge/ # 知识库相关 │ ├── models/ # 数据模型 │ ├── plugins/ # 插件(存放OCR) │ ├── static/ # 静态资源(配置文件) │ └── utils/ # 工具函数 -├── web/ # 前端代码 +├── web/src/ # 前端代码 +│ ├── apis/ # 前端的接口定义 +│ └── views/ # 主要 vue 文件 └── docker-compose.yml # Docker Compose 配置 ``` @@ -37,7 +39,20 @@ Yuxi-Know/ 核心原则: 由于 api-dev 和 web-dev 服务均配置了热重载 (hot-reloading),本地修改代码后无需重启容器,服务会自动更新。应该先检查项目是否已经在后台启动(`docker ps`),具体的可以阅读 [docker-compose.yml](docker-compose.yml). -## 风格说明 +前端开发规范: -- UI风格要简洁,同时要保持一致性,颜色要尽量参考 [base.css](web/src/assets/css/base.css) 中的颜色。不要悬停位移,不要过度使用阴影以及渐变色。 -- Python 代码要符合 Python 的规范,尽量使用较新的语法,避免使用旧版本的语法(版本兼容到 3.12+),使用 uvx ruff check 检查 lint。 +- API 接口规范:所有的 API 接口都应该定义在 web/src/apis 下面,并继承自 apiGet/apiPost/apiRequest +- Icon 应该从 @ant-design/icons-vue 或者 lucide-vue-next +- Vue 中的样式使用 less,并尽量使用[base.css](web/src/assets/css/base.css) 中的颜色。 +- UI风格要简洁,同时要保持一致性,颜色要尽量参考 不要悬停位移,不要过度使用阴影以及渐变色。 + + +后端开发规范: + +- 项目使用 uv 来管理依赖 +- Python 代码要符合 Python 的规范,尽量使用较新的语法,避免使用旧版本的语法(版本兼容到 3.12+),使用 make lint 检查 lint。使用 make format 来格式化代码。 + +其他: + +- 如果需要新建说明文档,则保存在 docs/vibe 文件夹下面 +- 测试脚本可以放在 test 文件夹下面,可以从 docker 中启动测试 diff --git a/server/routers/knowledge_router.py b/server/routers/knowledge_router.py index cd01dcfb..b58cd2b5 100644 --- a/server/routers/knowledge_router.py +++ b/server/routers/knowledge_router.py @@ -10,6 +10,7 @@ from server.models.user_model import User from server.utils.auth_middleware import get_admin_user from src import config, knowledge_base from src.knowledge.indexing import process_file_to_markdown +from src.models.embed import test_embedding_model_status, test_all_embedding_models_status from src.utils import hashstr, logger knowledge = APIRouter(prefix="/knowledge", tags=["knowledge"]) @@ -554,3 +555,35 @@ async def get_knowledge_base_statistics(current_user: User = Depends(get_admin_u except Exception as e: logger.error(f"获取知识库统计失败 {e}, {traceback.format_exc()}") return {"message": f"获取知识库统计失败 {e}", "stats": {}} + + +# ============================================================================= +# === Embedding模型状态检查分组 === +# ============================================================================= + + +@knowledge.get("/embedding-models/{model_id}/status") +async def get_embedding_model_status(model_id: str, current_user: User = Depends(get_admin_user)): + """获取指定embedding模型的状态""" + logger.debug(f"Checking embedding model status: {model_id}") + try: + status = await test_embedding_model_status(model_id) + return {"status": status, "message": "success"} + except Exception as e: + logger.error(f"获取embedding模型状态失败 {model_id}: {e}, {traceback.format_exc()}") + return { + "message": f"获取embedding模型状态失败: {e}", + "status": {"model_id": model_id, "status": "error", "message": str(e)}, + } + + +@knowledge.get("/embedding-models/status") +async def get_all_embedding_models_status(current_user: User = Depends(get_admin_user)): + """获取所有embedding模型的状态""" + logger.debug("Checking all embedding models status") + try: + status = await test_all_embedding_models_status() + return {"status": status, "message": "success"} + except Exception as e: + logger.error(f"获取所有embedding模型状态失败: {e}, {traceback.format_exc()}") + return {"message": f"获取所有embedding模型状态失败: {e}", "status": {"models": {}, "total": 0, "available": 0}} diff --git a/server/routers/system_router.py b/server/routers/system_router.py index 0322ad07..65efd484 100644 --- a/server/routers/system_router.py +++ b/server/routers/system_router.py @@ -9,6 +9,7 @@ from fastapi import APIRouter, Body, Depends, HTTPException from server.models.user_model import User from server.utils.auth_middleware import get_admin_user, get_superadmin_user from src import config, graph_base +from src.models.chat import test_chat_model_status, test_all_chat_models_status from src.utils.logging_config import logger system = APIRouter(prefix="/system", tags=["system"]) @@ -244,3 +245,35 @@ async def check_ocr_services_health(current_user: User = Depends(get_admin_user) overall_status = "healthy" if any(svc["status"] == "healthy" for svc in health_status.values()) else "unhealthy" return {"overall_status": overall_status, "services": health_status, "message": "OCR服务健康检查完成"} + + +# ============================================================================= +# === 聊天模型状态检查分组 === +# ============================================================================= + + +@system.get("/chat-models/status") +async def get_chat_model_status(provider: str, model_name: str, current_user: User = Depends(get_admin_user)): + """获取指定聊天模型的状态""" + logger.debug(f"Checking chat model status: {provider}/{model_name}") + try: + status = await test_chat_model_status(provider, model_name) + return {"status": status, "message": "success"} + except Exception as e: + logger.error(f"获取聊天模型状态失败 {provider}/{model_name}: {e}") + return { + "message": f"获取聊天模型状态失败: {e}", + "status": {"provider": provider, "model_name": model_name, "status": "error", "message": str(e)}, + } + + +@system.get("/chat-models/all/status") +async def get_all_chat_models_status(current_user: User = Depends(get_admin_user)): + """获取所有聊天模型的状态""" + logger.debug("Checking all chat models status") + try: + status = await test_all_chat_models_status() + return {"status": status, "message": "success"} + except Exception as e: + logger.error(f"获取所有聊天模型状态失败: {e}") + return {"message": f"获取所有聊天模型状态失败: {e}", "status": {"models": {}, "total": 0, "available": 0}} diff --git a/src/models/chat.py b/src/models/chat.py index cc65cd31..c79f8949 100644 --- a/src/models/chat.py +++ b/src/models/chat.py @@ -131,5 +131,70 @@ def get_custom_model(model_id): return modle_info +async def test_chat_model_status(provider: str, model_name: str) -> dict: + """ + 测试指定聊天模型的状态 + + Args: + provider: 模型提供商 + model_name: 模型名称 + + Returns: + dict: 包含状态信息的字典 + """ + try: + # 加载模型 + logger.debug(f"Selecting chat model {provider}/{model_name}") + model = select_model(provider, model_name) + + # 使用简单的测试消息 + test_messages = [{"role": "user", "content": "Say 1"}] + + # 发送测试请求 + response = model.call(test_messages, stream=False) + logger.debug(f"Test chat model status response: {response}") + + # 检查响应是否有效 + if response and response.content: + return {"provider": provider, "model_name": model_name, "status": "available", "message": "连接正常"} + else: + return {"provider": provider, "model_name": model_name, "status": "unavailable", "message": "响应无效"} + + except Exception as e: + logger.error(f"测试聊天模型状态失败 {provider}/{model_name}: {e}") + return {"provider": provider, "model_name": model_name, "status": "error", "message": str(e)} + + +async def test_all_chat_models_status() -> dict: + """ + 测试所有支持的聊天模型状态 + + Returns: + dict: 包含所有模型状态的字典 + """ + from src import config + + results = {} + + # 获取所有可用的模型 + for provider, provider_info in config.model_names.items(): + if provider == "custom": + # 处理自定义模型 + for custom_model in config.custom_models: + model_id = f"custom/{custom_model['custom_id']}" + status = await test_chat_model_status("custom", custom_model["custom_id"]) + results[model_id] = status + else: + # 处理普通模型 + for model_name in provider_info.models: + model_id = f"{provider}/{model_name}" + status = await test_chat_model_status(provider, model_name) + results[model_id] = status + + available_count = len([m for m in results.values() if m["status"] == "available"]) + + return {"models": results, "total": len(results), "available": available_count} + + if __name__ == "__main__": pass diff --git a/src/models/embed.py b/src/models/embed.py index 61efbd2e..bef6abad 100644 --- a/src/models/embed.py +++ b/src/models/embed.py @@ -168,12 +168,89 @@ class OtherEmbedding(BaseEmbeddingModel): logger.error(f"Other Embedding async request failed: {e}, {payload}") raise ValueError(f"Other Embedding async request failed: {e}") + async def test_connection(self) -> tuple[bool, str]: + """ + 测试embedding模型的连接性 + + Returns: + tuple: (success: bool, message: str) + """ + try: + # 使用简单的测试文本 + test_text = ["Hello world"] + await self.aencode(test_text) + return True, "连接正常" + except Exception as e: + error_msg = str(e) + return False, error_msg + + +async def test_embedding_model_status(model_id: str) -> dict: + """ + 测试指定embedding模型的状态 + + Args: + model_id: 模型ID,格式为 "provider/model_name" + + Returns: + dict: 包含状态信息的字典 + """ + try: + support_embed_models = config.embed_model_names.keys() + if model_id not in support_embed_models: + return {"model_id": model_id, "status": "unsupported", "message": f"不支持的模型: {model_id}"} + + # 选择并创建模型实例 + model = select_embedding_model(model_id) + + # 测试连接 + success, message = await model.test_connection() + + return { + "model_id": model_id, + "status": "available" if success else "unavailable", + "message": message if not success else "连接正常", + "dimension": model.dimension, + } + + except Exception as e: + logger.warning(f"测试embedding模型状态失败 {model_id}: {e}") + return {"model_id": model_id, "status": "error", "message": str(e)} + + +async def test_all_embedding_models_status() -> dict: + """ + 测试所有支持的embedding模型状态 + + Returns: + dict: 包含所有模型状态的字典 + """ + support_embed_models = list(config.embed_model_names.keys()) + results = {} + + # 并发测试所有模型 + tasks = [test_embedding_model_status(model_id) for model_id in support_embed_models] + model_statuses = await asyncio.gather(*tasks, return_exceptions=True) + + for i, status in enumerate(model_statuses): + if isinstance(status, Exception): + model_id = support_embed_models[i] + results[model_id] = {"model_id": model_id, "status": "error", "message": str(status)} + else: + results[status["model_id"]] = status + + return { + "models": results, + "total": len(support_embed_models), + "available": len([m for m in results.values() if m["status"] == "available"]), + } + def select_embedding_model(model_id): provider, model_name = model_id.split("/", 1) if model_id else ("", "") support_embed_models = config.embed_model_names.keys() assert model_id in support_embed_models, f"Unsupported embed model: {model_id}, only support {support_embed_models}" - logger.debug(f"Loading embedding model {model_id}") + logger.info(f"Loading embedding model {model_id}") if provider == "local": raise ValueError("Local embedding model is not supported, please use other embedding models") diff --git a/src/utils/logging_config.py b/src/utils/logging_config.py index 3aea635f..22182af6 100644 --- a/src/utils/logging_config.py +++ b/src/utils/logging_config.py @@ -20,7 +20,7 @@ def setup_logger(name, level="DEBUG", console=True): loguru_logger.add( LOG_FILE, level=level, - format="{time:YYYY-MM-DD HH:mm:ss} - {level} - {name}:{line} - {message}", + format="{time:YYYY-MM-DD HH:mm:ss} - {level} - {file}:{line} - {message}", encoding="utf-8", rotation="10 MB", # 文件大小达到 10MB 时轮转 retention="30 days", # 保留30天的日志 @@ -35,7 +35,7 @@ def setup_logger(name, level="DEBUG", console=True): format=( "{time:MM-DD HH:mm:ss} " "{level} " - "{name}:{line}: " + "{file}:{line}: " "{message}" ), colorize=True, diff --git a/web/src/apis/knowledge_api.js b/web/src/apis/knowledge_api.js index e438e3b4..45b71c7c 100644 --- a/web/src/apis/knowledge_api.js +++ b/web/src/apis/knowledge_api.js @@ -198,3 +198,26 @@ export const typeApi = { } } +// ============================================================================= +// === Embedding模型状态检查分组 === +// ============================================================================= + +export const embeddingApi = { + /** + * 获取指定embedding模型的状态 + * @param {string} modelId - 模型ID + * @returns {Promise} - 模型状态 + */ + getModelStatus: async (modelId) => { + return apiAdminGet(`/api/knowledge/embedding-models/${modelId}/status`) + }, + + /** + * 获取所有embedding模型的状态 + * @returns {Promise} - 所有模型状态 + */ + getAllModelsStatus: async () => { + return apiAdminGet('/api/knowledge/embedding-models/status') + } +} + diff --git a/web/src/apis/system_api.js b/web/src/apis/system_api.js index a9d191cd..7cf079fa 100644 --- a/web/src/apis/system_api.js +++ b/web/src/apis/system_api.js @@ -100,5 +100,29 @@ export const ocrApi = { getHealth: async () => apiAdminGet('/api/system/ocr/health') } +// ============================================================================= +// === 聊天模型状态检查分组 === +// ============================================================================= + +export const chatModelApi = { + /** + * 获取指定聊天模型的状态 + * @param {string} provider - 模型提供商 + * @param {string} modelName - 模型名称 + * @returns {Promise} - 模型状态 + */ + getModelStatus: async (provider, modelName) => { + return apiAdminGet(`/api/system/chat-models/status?provider=${encodeURIComponent(provider)}&model_name=${encodeURIComponent(modelName)}`) + }, + + /** + * 获取所有聊天模型的状态 + * @returns {Promise} - 所有模型状态 + */ + getAllModelsStatus: async () => { + return apiAdminGet('/api/system/chat-models/all/status') + } +} + diff --git a/web/src/components/ModelSelectorComponent.vue b/web/src/components/ModelSelectorComponent.vue index 7bf67a0c..225551ac 100644 --- a/web/src/components/ModelSelectorComponent.vue +++ b/web/src/components/ModelSelectorComponent.vue @@ -1,12 +1,35 @@