diff --git a/.gitignore b/.gitignore index f14507b1..c1fbc2ef 100644 --- a/.gitignore +++ b/.gitignore @@ -49,6 +49,7 @@ saves_dev notebooks graphrag docker/volumes +docs/vibe .cursorrules /models diff --git a/AGENTS.md b/AGENTS.md index 533d72ca..e50f30b5 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -3,25 +3,8 @@ Yuxi-Know 是一个基于知识图谱和向量数据库的智能知识库系统。它通过 FastAPI 提供后端 API,使用 Vue.js 构建前端界面,并利用 Docker Compose 进行整体服务的编排和管理。 +文档中心在 `docs` 文件夹下面。 -``` -Yuxi-Know/ -├── docker/ # Docker 配置文件 -├── scripts/ # 脚本文件,如批量上传等 -├── server/ # 服务端代码(部分) -├── src/ # 主要源代码目录 -│ ├── agents/ # 智能体应用 -│ ├── config/ # 配置文件 -│ ├── knowledge/ # 知识库相关 -│ ├── models/ # 数据模型 -│ ├── plugins/ # 插件(存放OCR) -│ ├── static/ # 静态资源(配置文件) -│ └── utils/ # 工具函数 -├── web/src/ # 前端代码 -│ ├── apis/ # 前端的接口定义 -│ └── views/ # 主要 vue 文件 -└── docker-compose.yml # Docker Compose 配置 -``` # 核心服务与容器 (Core Services & Containers) @@ -54,5 +37,6 @@ Yuxi-Know/ 其他: -- 如果需要新建说明文档,则保存在 docs/vibe 文件夹下面 +- 如果需要新建说明文档(仅开发者可见,非必要不创建),则保存在 `docs/vibe` 文件夹下面 - 测试脚本可以放在 test 文件夹下面,可以从 docker 中启动测试 +- 代码更新后要检查文档部分是否有需要更新的地方,文档的目录定义在 `docs/.vitepress/config.mts` 中 diff --git a/README.md b/README.md index ce98266f..57bfb623 100644 --- a/README.md +++ b/README.md @@ -141,7 +141,7 @@ export HTTPS_PROXY=http://IP:PORT
自定义模型供应商 -在 [src/config/static/models.yaml](src/config/static/models.yaml) 中添加新的模型配置,然后在 `.env` 文件中添加对应的环境变量,最后重新部署项目即可。 +在 [src/config/static/models.yaml](src/config/static/models.yaml) 中添加新的模型配置,然后在 `.env` 文件中添加对应的环境变量,最后重新部署项目即可。如需使用自定义配置文件,可设置环境变量 `OVERRIDE_DEFAULT_MODELS_CONFIG_WITH` 指向该文件。 **示例**: @@ -150,8 +150,7 @@ custom-provider-name-here: name: custom-provider-name-here default: custom-model-name-here base_url: "https://api.your-provider.com/v1" - env: - - CUSTOM_API_KEY_ENV_NAME # e.g. OPENAI_API_KEY + env: CUSTOM_API_KEY_ENV_NAME # e.g. OPENAI_API_KEY models: - supported-model-name-here ``` @@ -159,7 +158,7 @@ custom-provider-name-here: ### 自定义嵌入模型和重排序模型 -需要注意的是,从 v0.2 版本开始,项目将模型部署和项目本身做了完全解耦,因此无论是 Embedding 还是 Reranker,如果需要使用本地模型,都需要使用 vllm /ollama 转换为 api 服务后,在 src/config/static/models.yaml 或者 src/config/static/models.private.yaml (优先)中 添加类似下面的信息: +需要注意的是,从 v0.2 版本开始,项目将模型部署和项目本身做了完全解耦。因此无论是 Embedding 还是 Reranker,如果需要使用本地模型,都需要使用 vllm /ollama 转换为 API 服务后,在 [src/config/static/models.yaml](src/config/static/models.yaml)(或环境变量 `OVERRIDE_DEFAULT_MODELS_CONFIG_WITH` 指向的文件)中添加类似下面的信息: ```yaml EMBED_MODEL_INFO: diff --git a/docs/changelog/roadmap.md b/docs/changelog/roadmap.md index d6e03836..b97c2909 100644 --- a/docs/changelog/roadmap.md +++ b/docs/changelog/roadmap.md @@ -22,7 +22,7 @@ - [x] 添加统计信息 - [ ] 支持 MinerU 2.5 的解析方法 - [ ] Options 中添加网络搜索和绘制图片的选项,分别是用来调用工具 -- [ ] 移除自定义模型,同时将模型的 env 修改为单环境变量的形式,移除 models.yaml 的私有共有双配置模式,除非配置了环境变量 `OVERRIDE_DEFAULT_MODELS_CONFIG_WITH` 然后指向一个变量。 +- [x] 移除自定义模型,同时将模型的 env 修改为单环境变量的形式,移除 models.yaml 的私有共有双配置模式,除非配置了环境变量 `OVERRIDE_DEFAULT_MODELS_CONFIG_WITH` 然后指向一个变量。 📝 **Base** diff --git a/docs/intro/model-config.md b/docs/intro/model-config.md index 3324c964..a0ca7156 100644 --- a/docs/intro/model-config.md +++ b/docs/intro/model-config.md @@ -42,13 +42,17 @@ ### 1. 编辑模型配置文件 -**方式一:修改公共配置** +**方式一:修改默认配置** 编辑 `src/config/static/models.yaml` 文件 -**方式二:创建私有配置(推荐)** -复制模板并创建私有配置: +**方式二:使用覆盖配置** +创建自定义配置文件并通过环境变量指定: ```bash -cp src/config/static/models.yaml src/config/static/models.private.yaml +# 创建自定义配置文件 +cp src/config/static/models.yaml /path/to/your/custom-models.yaml + +# 设置环境变量 +export OVERRIDE_DEFAULT_MODELS_CONFIG_WITH=/path/to/your/custom-models.yaml ``` ### 2. 添加模型配置 @@ -60,20 +64,17 @@ custom-provider-name: name: custom-provider-name default: custom-model-name base_url: "https://api.your-provider.com/v1" - env: - - CUSTOM_API_KEY_ENV_NAME + env: CUSTOM_API_KEY_ENV_NAME # 注意:现在是单个环境变量 models: - supported-model-name - another-model-name - # 本地 Ollama 服务 local-ollama: name: Local Ollama base_url: "http://localhost:11434/v1" default: llama3.2 - env: - - NO_API_KEY + env: NO_API_KEY # 对于不需要API Key的服务,使用NO_API_KEY models: - llama3.2 - qwen2.5 @@ -83,8 +84,7 @@ local-vllm: name: Local vLLM base_url: "http://localhost:8000/v1" default: Qwen/Qwen2.5-7B-Instruct - env: - - NO_API_KEY + env: NO_API_KEY models: - Qwen/Qwen2.5-7B-Instruct - Qwen/Qwen2.5-14B-Instruct @@ -113,7 +113,7 @@ docker compose restart api-dev #### 1. 配置模型信息 -在 `src/config/static/models.yaml` 或 `src/config/static/models.private.yaml` 中添加配置: +在 `src/config/static/models.yaml` 中或通过覆盖配置文件添加配置: ```yaml EMBED_MODEL_INFO: diff --git a/server/routers/dashboard_router.py b/server/routers/dashboard_router.py index a48b88cf..74e27a35 100644 --- a/server/routers/dashboard_router.py +++ b/server/routers/dashboard_router.py @@ -665,10 +665,7 @@ async def get_all_feedbacks( db.query(MessageFeedback, Message, Conversation, User) .join(Message, MessageFeedback.message_id == Message.id) .join(Conversation, Message.conversation_id == Conversation.id) - .outerjoin(User, - (MessageFeedback.user_id == User.id) | - (MessageFeedback.user_id == User.user_id) - ) + .outerjoin(User, (MessageFeedback.user_id == User.id) | (MessageFeedback.user_id == User.user_id)) ) # Apply filters diff --git a/src/agents/common/models.py b/src/agents/common/models.py index bbe078f2..99622516 100644 --- a/src/agents/common/models.py +++ b/src/agents/common/models.py @@ -5,7 +5,6 @@ from langchain_core.language_models import BaseChatModel from pydantic import SecretStr from src import config -from src.models import get_custom_model from src.utils import get_docker_safe_url @@ -15,22 +14,13 @@ def load_chat_model(fully_specified_name: str, **kwargs) -> BaseChatModel: """ provider, model = fully_specified_name.split("/", maxsplit=1) - if provider == "custom": - from langchain_openai import ChatOpenAI - - model_info = get_custom_model(model) - api_key = model_info.get("api_key") or "custom_model" - base_url = get_docker_safe_url(model_info["api_base"]) - model_name = model_info.get("name") or "custom_model" - return ChatOpenAI( - model=model_name, - api_key=SecretStr(api_key), - base_url=base_url, - stream_usage=True, - ) + assert provider != "custom", "[弃用] 自定义模型已移除,请在 src/config/static/models.yaml 中配置" model_info = config.model_names.get(provider, {}) - api_key = os.getenv(model_info["env"][0], model_info["env"][0]) + env_var = model_info["env"] + + api_key = os.getenv(env_var, env_var) + base_url = get_docker_safe_url(model_info["base_url"]) if provider in ["deepseek", "dashscope"]: diff --git a/src/config/app.py b/src/config/app.py index 3c8781c3..b612dacd 100644 --- a/src/config/app.py +++ b/src/config/app.py @@ -6,8 +6,6 @@ import yaml from src.utils.logging_config import logger -DEFAULT_MOCK_API = "this_is_mock_api_key_in_frontend" - class SimpleConfig(dict): def __key(self, key): @@ -44,6 +42,7 @@ class Config(SimpleConfig): self.filename = str(Path(f"{self.save_dir}/config/base.yaml")) os.makedirs(os.path.dirname(self.filename), exist_ok=True) + self._models_config_path: Path | None = None self._update_models_from_file() ### >>> 默认配置 @@ -100,40 +99,42 @@ class Config(SimpleConfig): def _update_models_from_file(self): """ - 从 models.yaml 和 models.private.yaml 中更新 MODEL_NAMES + 从 models.yaml 或覆盖配置文件中更新 MODEL_NAMES """ + # 检查是否设置了覆盖配置文件的环境变量 + override_config_path = os.getenv("OVERRIDE_DEFAULT_MODELS_CONFIG_WITH") - with open(Path("src/config/static/models.yaml"), encoding="utf-8") as f: + if override_config_path and os.path.exists(override_config_path): + config_file = Path(override_config_path) + logger.info(f"Using override models config from: {override_config_path}") + else: + config_file = Path("src/config/static/models.yaml") + logger.info("Using default models config") + + self._models_config_path = config_file + + with open(self._models_config_path, encoding="utf-8") as f: _models = yaml.safe_load(f) - # 尝试打开一个 models.private.yaml 文件,用来覆盖 models.yaml 中的配置 - # 兼容性测试(历史遗留问题) - exist_yml = Path("src/config/static/models.private.yml").exists() - exist_yaml = Path("src/config/static/models.private.yaml").exists() - if exist_yml and not exist_yaml: - os.rename("src/config/static/models.private.yml", "src/config/static/models.private.yaml") - - try: - with open(Path("src/config/static/models.private.yaml"), encoding="utf-8") as f: - _models_private = yaml.safe_load(f) - except FileNotFoundError: - _models_private = {} - - # 修改为按照子元素合并 - # _models = {**_models, **_models_private} - - self.model_names = {**_models["MODEL_NAMES"], **_models_private.get("MODEL_NAMES", {})} - self.embed_model_names = {**_models["EMBED_MODEL_INFO"], **_models_private.get("EMBED_MODEL_INFO", {})} - self.reranker_names = {**_models["RERANKER_LIST"], **_models_private.get("RERANKER_LIST", {})} + self.model_names = _models["MODEL_NAMES"] + self.embed_model_names = _models["EMBED_MODEL_INFO"] + self.reranker_names = _models["RERANKER_LIST"] def _save_models_to_file(self): - _models = { + """ + 将当前模型配置写回模型配置文件 + """ + if self._models_config_path is None: + self._models_config_path = Path("src/config/static/models.yaml") + + models_payload = { "MODEL_NAMES": self.model_names, "EMBED_MODEL_INFO": self.embed_model_names, "RERANKER_LIST": self.reranker_names, } - with open(Path("src/config/static/models.private.yaml"), "w", encoding="utf-8") as f: - yaml.dump(_models, f, indent=2, allow_unicode=True) + + with open(self._models_config_path, "w", encoding="utf-8") as f: + yaml.safe_dump(models_payload, f, indent=2, allow_unicode=True, sort_keys=False) def handle_self(self): """ @@ -156,19 +157,21 @@ class Config(SimpleConfig): ) # 检查模型提供商的环境变量 - conds = {} self.model_provider_status = {} for provider in self.model_names: - conds[provider] = self.model_names[provider]["env"] - conds_bool = [bool(os.getenv(_k)) for _k in conds[provider]] - self.model_provider_status[provider] = all(conds_bool) + env_var = self.model_names[provider]["env"] + # 如果环境变量名为 NO_API_KEY,则认为总是可用 + if env_var == "NO_API_KEY": + self.model_provider_status[provider] = True + else: + self.model_provider_status[provider] = bool(os.getenv(env_var)) if os.getenv("TAVILY_API_KEY"): self.enable_web_search = True self.valuable_model_provider = [k for k, v in self.model_provider_status.items() if v] assert len(self.valuable_model_provider) > 0, ( - f"No model provider available, please check your `.env` file. API_KEY_LIST: {conds}" + "No model provider available, please check your `.env` file." ) def load(self): @@ -218,23 +221,6 @@ class Config(SimpleConfig): def dump_config(self): return json.loads(str(self)) - def compare_custom_models(self, value): - """ - 比较 custom_models 中的 api_key,如果输入的 api_key 与当前的 api_key 相同,则不修改 - 如果输入的 api_key 为 DEFAULT_MOCK_API,则使用当前的 api_key - """ - current_models_dict = {model["custom_id"]: model.get("api_key") for model in self.get("custom_models", [])} - - for i, model in enumerate(value): - input_custom_id = model.get("custom_id") - input_api_key = model.get("api_key") - - if input_custom_id in current_models_dict: - current_api_key = current_models_dict[input_custom_id] - if input_api_key == DEFAULT_MOCK_API or input_api_key == current_api_key: - value[i]["api_key"] = current_api_key - - return value config = Config() diff --git a/src/config/static/models.yaml b/src/config/static/models.yaml index 541857c2..4212e5d4 100644 --- a/src/config/static/models.yaml +++ b/src/config/static/models.yaml @@ -1,8 +1,8 @@ #################################################### # -# 不要直接修改这个里面的文件,可能会有被覆盖的风险, -# 建议 复制一份 在 models.private.yaml 中修改, -# 会自动加载 +# 默认模型配置文件。可以直接修改该文件更新模型。 +# 如需自定义路径,可通过环境变量 +# OVERRIDE_DEFAULT_MODELS_CONFIG_WITH 指向新的 YAML。 # ##################################################### @@ -12,8 +12,7 @@ MODEL_NAMES: url: https://platform.openai.com/docs/models base_url: https://api.openai.com/v1 default: gpt-4o-mini - env: - - OPENAI_API_KEY + env: OPENAI_API_KEY models: - gpt-4 - gpt-4o @@ -24,8 +23,7 @@ MODEL_NAMES: url: https://platform.deepseek.com/api-docs/zh-cn/pricing base_url: https://api.deepseek.com/v1 default: deepseek-chat - env: - - DEEPSEEK_API_KEY + env: DEEPSEEK_API_KEY models: - deepseek-chat - deepseek-reasoner @@ -35,8 +33,7 @@ MODEL_NAMES: url: https://open.bigmodel.cn/dev/api base_url: https://open.bigmodel.cn/api/paas/v4/ default: glm-4.5-flash - env: - - ZHIPUAI_API_KEY + env: ZHIPUAI_API_KEY models: - glm-4.5 - glm-4.5-air @@ -47,8 +44,7 @@ MODEL_NAMES: url: https://cloud.siliconflow.cn/models base_url: https://api.siliconflow.cn/v1 default: zai-org/GLM-4.5 - env: - - SILICONFLOW_API_KEY + env: SILICONFLOW_API_KEY models: - Qwen/Qwen3-235B-A22B-Thinking-2507 - Qwen/Qwen3-235B-A22B-Instruct-2507 @@ -60,8 +56,7 @@ MODEL_NAMES: url: https://api.together.ai/models base_url: https://api.together.xyz/v1/ default: meta-llama/Llama-3.3-70B-Instruct-Turbo-Free - env: - - TOGETHER_API_KEY + env: TOGETHER_API_KEY models: - meta-llama/Llama-3.3-70B-Instruct-Turbo-Free @@ -70,8 +65,7 @@ MODEL_NAMES: url: https://bailian.console.aliyun.com/?switchAgent=10226727&productCode=p_efm#/model-market base_url: https://dashscope.aliyuncs.com/compatible-mode/v1 default: qwen-max-latest - env: - - DASHSCOPE_API_KEY + env: DASHSCOPE_API_KEY models: - qwen-max-latest - qwen-plus-latest @@ -84,8 +78,7 @@ MODEL_NAMES: url: https://console.volcengine.com/ark/region:ark+cn-beijing/model base_url: https://ark.cn-beijing.volces.com/api/v3 default: doubao-seed-1-6-250615 - env: - - ARK_API_KEY + env: ARK_API_KEY models: - doubao-seed-1-6-250615 - doubao-seed-1-6-thinking-250715 @@ -96,8 +89,7 @@ MODEL_NAMES: url: https://openrouter.ai/models base_url: https://openrouter.ai/api/v1 default: openai/gpt-4o - env: - - OPENROUTER_API_KEY + env: OPENROUTER_API_KEY models: - openai/gpt-4o - x-ai/grok-4 diff --git a/src/models/__init__.py b/src/models/__init__.py index d530a04f..bd131e96 100644 --- a/src/models/__init__.py +++ b/src/models/__init__.py @@ -1,4 +1,4 @@ -from src.models.chat import get_custom_model, select_model +from src.models.chat import select_model from src.models.embed import select_embedding_model -__all__ = ["select_model", "select_embedding_model", "get_custom_model"] +__all__ = ["select_model", "select_embedding_model"] diff --git a/src/models/chat.py b/src/models/chat.py index c79f8949..2bf3478e 100644 --- a/src/models/chat.py +++ b/src/models/chat.py @@ -79,14 +79,6 @@ class OpenModel(OpenAIBase): super().__init__(api_key=api_key, base_url=base_url, model_name=model_name) -class CustomModel(OpenAIBase): - def __init__(self, model_info): - model_name = model_info["name"] - api_key = model_info.get("api_key") or "custom_model" - base_url = get_docker_safe_url(model_info["api_base"]) - logger.info(f"> Custom model: {model_name}, base_url: {base_url}") - - super().__init__(api_key=api_key, base_url=base_url, model_name=model_name) class GeneralResponse: @@ -106,14 +98,10 @@ def select_model(model_provider, model_name=None): if model_provider == "openai": return OpenModel(model_name) - if model_provider == "custom": - model_info = get_custom_model(model_name) - return CustomModel(model_info) - # 其他模型,默认使用OpenAIBase try: model = OpenAIBase( - api_key=os.getenv(model_info["env"][0]), + api_key=os.getenv(model_info["env"]), base_url=model_info["base_url"], model_name=model_name, ) @@ -122,13 +110,6 @@ def select_model(model_provider, model_name=None): raise ValueError(f"Model provider {model_provider} load failed, {e} \n {traceback.format_exc()}") -def get_custom_model(model_id): - """return model_info""" - assert config.custom_models is not None, "custom_models is not set" - modle_info = next((x for x in config.custom_models if x["custom_id"] == model_id), None) - if modle_info is None: - raise ValueError(f"Model {model_id} not found in custom models") - return modle_info async def test_chat_model_status(provider: str, model_name: str) -> dict: @@ -178,18 +159,11 @@ async def test_all_chat_models_status() -> dict: # 获取所有可用的模型 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 + # 处理普通模型 + 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"]) diff --git a/web/src/components/ModelProvidersComponent.vue b/web/src/components/ModelProvidersComponent.vue index 75d026ac..97fed2f4 100644 --- a/web/src/components/ModelProvidersComponent.vue +++ b/web/src/components/ModelProvidersComponent.vue @@ -1,38 +1,7 @@ @@ -75,7 +70,6 @@ const state = reactive({ // 从configStore中获取所需数据 const modelNames = computed(() => configStore.config?.model_names) const modelStatus = computed(() => configStore.config?.model_provider_status) -const customModels = computed(() => configStore.config?.custom_models || []) // 筛选 modelStatus 中为真的key const modelKeys = computed(() => {