refactor: 重构配置/知识库模块,拆分为独立app.py文件

This commit is contained in:
Wenjie Zhang 2025-09-18 15:20:39 +08:00
parent 747c5de88f
commit 66c550c18c
7 changed files with 280 additions and 263 deletions

1
.gitignore vendored
View File

@ -29,6 +29,7 @@ cache
.vscode
.idea
.vibe
.qoder
*.nogit*
*.private*
*.local*

View File

@ -3,14 +3,23 @@
💭 **Features Todo**
- [x] LangGraph 升级到 0.6+ 版本,并适配新特性,如 context 等,添加 MCP 工具的支持。
- [x] 支持动态工具配置的同时,将 Configuration替换为 Context 后能够正常使用
- [ ] 使用更好的知识图谱检索方法
- [x] 使用更好的知识图谱检索方法(二跳结果查询)
- [ ] 使用其他的聊天记录管理方法,解决两个问题,一个是上下文长度过长,一个是上下文的类型变得更加丰富,比如多模态等等。(现在是基于 LangGraph 的 Memory 实现的v0.2.3 版本实现),暂定使用 [mem0](github.com/mem0ai/mem0) 来实现。但是目前了解下来,还不是我想要的那种方案。可能会基于这个实现一个 ThreadConvManager 这个类。
- [ ] 添加对于上传文件的支持这里的复杂的地方就在于如何和历史记录结合在一起v0.2.3 版本实现,放在记忆管理后面)
- [x] 将现在的 graphview.vue 文件中 GraphContainer相关的代码分离到一个单独的组件中然后 actions 和 footer 都是作为 slot top/bottom 提供的
- [x] 然后应用到 web/src/components/ToolCallingResult/KnowledgeGraphResult.vue 中,替换现有的 GraphContainer。
- [x] 然后应用到 web/src/components/ToolCallingResult/KnowledgeGraphResult.vue 中,替换现有的 GraphContainer。
- [ ] 知识图谱的上传和可视化,支持属性,标签的展示
- [ ] 集成智能体评估,首先使用命令行来实现,然后考虑放在 UI 里面展示
- [ ] 开发与生产环境隔离
- [ ] 添加统计信息
- [ ] 添加内容审查功能
📝 **Base**
- [ ] 优化全局配置的管理模型,以及配置文件的管理,子配置的管理
- [ ] 新建 tasker 模块用来管理所有的后台任务UI 上使用侧边栏管理。
- [ ] 新增 files 模块,用来管理文件上传,下载等
🐛**BUGs**
- [x] LlightRAG 知识库中,点击边,没有显示,但是在全屏的时候却又能够显示出来。
@ -20,15 +29,17 @@
- [x] 目前只能获取默认的 tools单个智能体的tools没法展示
- [x] 生成的过程中切换对话,会出现消息渲染混乱的问题,消息完全记载完成之后不会出现混乱
- [ ] 当消息生成的时候有报错的时候,前端无显示
- [ ] 偶然会出现 embedding 报错的情况,但不好复现
- [x] GraphCanvas 在智能体消息页面有时候会出现渲染不出现的问题可以添加一个slot 在右上角(加载 button这样就可以强制重新渲染
💯 **More**:
下面的功能**可能**会放在后续版本实现,暂时未定
- [ ] 支持数据库查询功能
- [x] 支持数据库查询功能
- [x] 消息内容支持图片显示
- [ ] 添加 SQL 读取工具
- [ ] 添加绘图工具(这里的绘图是指绘制固定格式的图和表等,暂时没想好如何支持自定义绘图)
- [x] 添加绘图工具(这里的绘图是指绘制固定格式的图和表等,暂时没想好如何支持自定义绘图)
- [ ] 添加测试脚本,覆盖最常见的功能
- [ ] 优化对文档信息的检索展示(检索结果页、详情页)
- [ ] 集成 LangFuse (观望)添加用户日志与用户反馈模块,可以在 AgentView 中查看信息

View File

@ -5,30 +5,8 @@ from dotenv import load_dotenv
load_dotenv("src/.env", override=True)
from concurrent.futures import ThreadPoolExecutor # noqa: E402
from src.config import config # noqa: E402
from src.knowledge import knowledge_base, graph_base # noqa: E402
executor = ThreadPoolExecutor()
from src.config import Config # noqa: E402
config = Config()
# 导入知识库相关模块
from src.knowledge.chroma_kb import ChromaKB # noqa: E402
from src.knowledge.kb_factory import KnowledgeBaseFactory # noqa: E402
from src.knowledge.kb_manager import KnowledgeBaseManager # noqa: E402
from src.knowledge.lightrag_kb import LightRagKB # noqa: E402
from src.knowledge.milvus_kb import MilvusKB # noqa: E402
# 注册知识库类型
KnowledgeBaseFactory.register("chroma", ChromaKB, {"description": "基于 ChromaDB 的轻量级向量知识库,适合开发和小规模"})
KnowledgeBaseFactory.register("milvus", MilvusKB, {"description": "基于 Milvus 的生产级向量知识库,适合高性能部署"})
KnowledgeBaseFactory.register("lightrag", LightRagKB, {"description": "基于图检索的知识库,支持实体关系构建和复杂查询"})
# 创建知识库管理器
work_dir = os.path.join(config.save_dir, "knowledge_base_data")
knowledge_base = KnowledgeBaseManager(work_dir)
from src.knowledge import GraphDatabase # noqa: E402
graph_base = GraphDatabase()

View File

@ -1,228 +1,3 @@
import json
import os
from pathlib import Path
from .app import config
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):
return "" if key is None else key # 目前忘记了这里为什么要 lower 了,只能说配置项最好不要有大写的
def __str__(self):
return json.dumps(self)
def __setattr__(self, key, value):
self[self.__key(key)] = value
def __getattr__(self, key):
return self.get(self.__key(key))
def __getitem__(self, key):
return self.get(self.__key(key))
def __setitem__(self, key, value):
return super().__setitem__(self.__key(key), value)
def __dict__(self):
return {k: v for k, v in self.items()}
def update(self, other):
for key, value in other.items():
self[key] = value
class Config(SimpleConfig):
def __init__(self):
super().__init__()
self._config_items = {}
self.save_dir = os.getenv("SAVE_DIR", "saves")
self.filename = str(Path(f"{self.save_dir}/config/base.yaml"))
os.makedirs(os.path.dirname(self.filename), exist_ok=True)
self._update_models_from_file()
### >>> 默认配置
# 功能选项
self.add_item("enable_reranker", default=False, des="是否开启重排序")
self.add_item(
"enable_web_search",
default=False,
des=(
"是否开启网页搜索(注:现阶段会根据 TAVILY_API_KEY 自动开启,无法手动配置,将会在下个版本移除此配置项)"
),
)
# 默认智能体配置
self.add_item("default_agent_id", default="", des="默认智能体ID")
# 模型配置
## 注意这里是模型名,而不是具体的模型路径,默认使用 HuggingFace 的路径
## 如果需要自定义本地模型路径,则在 src/.env 中配置 MODEL_DIR
self.add_item("model_provider", default="siliconflow", des="模型提供商", choices=list(self.model_names.keys()))
self.add_item("model_name", default="zai-org/GLM-4.5", des="模型名称")
self.add_item(
"embed_model",
default="siliconflow/BAAI/bge-m3",
des="Embedding 模型",
choices=list(self.embed_model_names.keys()),
)
self.add_item(
"reranker",
default="siliconflow/BAAI/bge-reranker-v2-m3",
des="Re-Ranker 模型",
choices=list(self.reranker_names.keys()),
) # noqa: E501
### <<< 默认配置结束
self.load()
self.handle_self()
def add_item(self, key, default, des=None, choices=None):
self.__setattr__(key, default)
self._config_items[key] = {"default": default, "des": des, "choices": choices}
def __dict__(self):
blocklist = [
"_config_items",
"model_names",
"model_provider_status",
"embed_model_names",
"reranker_names",
]
return {k: v for k, v in self.items() if k not in blocklist}
def _update_models_from_file(self):
"""
models.yaml models.private.yml 中更新 MODEL_NAMES
"""
with open(Path("src/static/models.yaml"), encoding="utf-8") as f:
_models = yaml.safe_load(f)
# 尝试打开一个 models.private.yml 文件,用来覆盖 models.yaml 中的配置
try:
with open(Path("src/static/models.private.yml"), 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", {})}
def _save_models_to_file(self):
_models = {
"MODEL_NAMES": self.model_names,
"EMBED_MODEL_INFO": self.embed_model_names,
"RERANKER_LIST": self.reranker_names,
}
with open(Path("src/static/models.private.yml"), "w", encoding="utf-8") as f:
yaml.dump(_models, f, indent=2, allow_unicode=True)
def handle_self(self):
"""
处理配置
"""
self.model_dir = os.environ.get("MODEL_DIR", "")
if self.model_dir:
if os.path.exists(self.model_dir):
logger.debug(
f"The model directory {self.model_dir} "
f"contains the following folders: {os.listdir(self.model_dir)}"
)
else:
logger.warning(
f"Warning: The model directory {self.model_dir} does not exist. "
"If not configured, please ignore it. "
"If configured, please check if the configuration is correct; "
"For example, the mapping in the docker-compose file"
)
# 检查模型提供商的环境变量
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)
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}"
)
def load(self):
"""根据传入的文件覆盖掉默认配置"""
logger.info(f"Loading config from {self.filename}")
if self.filename is not None and os.path.exists(self.filename):
if self.filename.endswith(".json"):
with open(self.filename) as f:
content = f.read()
if content:
local_config = json.loads(content)
self.update(local_config)
else:
print(f"{self.filename} is empty.")
elif self.filename.endswith(".yaml"):
with open(self.filename) as f:
content = f.read()
if content:
local_config = yaml.safe_load(content)
self.update(local_config)
else:
print(f"{self.filename} is empty.")
else:
logger.warning(f"Unknown config file type {self.filename}")
def save(self):
logger.info(f"Saving config to {self.filename}")
if self.filename is None:
logger.warning("Config file is not specified, save to default config/base.yaml")
self.filename = os.path.join(self.save_dir, "config", "base.yaml")
os.makedirs(os.path.dirname(self.filename), exist_ok=True)
if self.filename.endswith(".json"):
with open(self.filename, "w+") as f:
json.dump(self.__dict__(), f, indent=4, ensure_ascii=False)
elif self.filename.endswith(".yaml"):
with open(self.filename, "w+") as f:
yaml.dump(self.__dict__(), f, indent=2, allow_unicode=True)
else:
logger.warning(f"Unknown config file type {self.filename}, save as json")
with open(self.filename, "w+") as f:
json.dump(self, f, indent=4)
logger.info(f"Config file {self.filename} saved")
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
__all__ = ["config"]

231
src/config/app.py Normal file
View File

@ -0,0 +1,231 @@
import json
import os
from pathlib import Path
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):
return "" if key is None else key # 目前忘记了这里为什么要 lower 了,只能说配置项最好不要有大写的
def __str__(self):
return json.dumps(self)
def __setattr__(self, key, value):
self[self.__key(key)] = value
def __getattr__(self, key):
return self.get(self.__key(key))
def __getitem__(self, key):
return self.get(self.__key(key))
def __setitem__(self, key, value):
return super().__setitem__(self.__key(key), value)
def __dict__(self):
return {k: v for k, v in self.items()}
def update(self, other):
for key, value in other.items():
self[key] = value
class Config(SimpleConfig):
def __init__(self):
super().__init__()
self._config_items = {}
self.save_dir = os.getenv("SAVE_DIR", "saves")
self.filename = str(Path(f"{self.save_dir}/config/base.yaml"))
os.makedirs(os.path.dirname(self.filename), exist_ok=True)
self._update_models_from_file()
### >>> 默认配置
# 功能选项
self.add_item("enable_reranker", default=False, des="是否开启重排序")
self.add_item(
"enable_web_search",
default=False,
des=(
"是否开启网页搜索(注:现阶段会根据 TAVILY_API_KEY 自动开启,无法手动配置,将会在下个版本移除此配置项)"
),
)
# 默认智能体配置
self.add_item("default_agent_id", default="", des="默认智能体ID")
# 模型配置
## 注意这里是模型名,而不是具体的模型路径,默认使用 HuggingFace 的路径
## 如果需要自定义本地模型路径,则在 src/.env 中配置 MODEL_DIR
self.add_item("model_provider", default="siliconflow", des="模型提供商", choices=list(self.model_names.keys()))
self.add_item("model_name", default="zai-org/GLM-4.5", des="模型名称")
self.add_item(
"embed_model",
default="siliconflow/BAAI/bge-m3",
des="Embedding 模型",
choices=list(self.embed_model_names.keys()),
)
self.add_item(
"reranker",
default="siliconflow/BAAI/bge-reranker-v2-m3",
des="Re-Ranker 模型",
choices=list(self.reranker_names.keys()),
) # noqa: E501
### <<< 默认配置结束
self.load()
self.handle_self()
def add_item(self, key, default, des=None, choices=None):
self.__setattr__(key, default)
self._config_items[key] = {"default": default, "des": des, "choices": choices}
def __dict__(self):
blocklist = [
"_config_items",
"model_names",
"model_provider_status",
"embed_model_names",
"reranker_names",
]
return {k: v for k, v in self.items() if k not in blocklist}
def _update_models_from_file(self):
"""
models.yaml models.private.yml 中更新 MODEL_NAMES
"""
with open(Path("src/static/models.yaml"), encoding="utf-8") as f:
_models = yaml.safe_load(f)
# 尝试打开一个 models.private.yml 文件,用来覆盖 models.yaml 中的配置
try:
with open(Path("src/static/models.private.yml"), 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", {})}
def _save_models_to_file(self):
_models = {
"MODEL_NAMES": self.model_names,
"EMBED_MODEL_INFO": self.embed_model_names,
"RERANKER_LIST": self.reranker_names,
}
with open(Path("src/static/models.private.yml"), "w", encoding="utf-8") as f:
yaml.dump(_models, f, indent=2, allow_unicode=True)
def handle_self(self):
"""
处理配置
"""
self.model_dir = os.environ.get("MODEL_DIR", "")
if self.model_dir:
if os.path.exists(self.model_dir):
logger.debug(
f"The model directory {self.model_dir} "
f"contains the following folders: {os.listdir(self.model_dir)}"
)
else:
logger.warning(
f"Warning: The model directory {self.model_dir} does not exist. "
"If not configured, please ignore it. "
"If configured, please check if the configuration is correct; "
"For example, the mapping in the docker-compose file"
)
# 检查模型提供商的环境变量
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)
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}"
)
def load(self):
"""根据传入的文件覆盖掉默认配置"""
logger.info(f"Loading config from {self.filename}")
if self.filename is not None and os.path.exists(self.filename):
if self.filename.endswith(".json"):
with open(self.filename) as f:
content = f.read()
if content:
local_config = json.loads(content)
self.update(local_config)
else:
print(f"{self.filename} is empty.")
elif self.filename.endswith(".yaml"):
with open(self.filename) as f:
content = f.read()
if content:
local_config = yaml.safe_load(content)
self.update(local_config)
else:
print(f"{self.filename} is empty.")
else:
logger.warning(f"Unknown config file type {self.filename}")
def save(self):
logger.info(f"Saving config to {self.filename}")
if self.filename is None:
logger.warning("Config file is not specified, save to default config/base.yaml")
self.filename = os.path.join(self.save_dir, "config", "base.yaml")
os.makedirs(os.path.dirname(self.filename), exist_ok=True)
if self.filename.endswith(".json"):
with open(self.filename, "w+") as f:
json.dump(self.__dict__(), f, indent=4, ensure_ascii=False)
elif self.filename.endswith(".yaml"):
with open(self.filename, "w+") as f:
yaml.dump(self.__dict__(), f, indent=2, allow_unicode=True)
else:
logger.warning(f"Unknown config file type {self.filename}, save as json")
with open(self.filename, "w+") as f:
json.dump(self, f, indent=4)
logger.info(f"Config file {self.filename} saved")
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()

View File

@ -1,3 +1,24 @@
from .graphbase import GraphDatabase
import os
from ..config import config
from .graphbase import GraphDatabase
from .chroma_kb import ChromaKB
from .kb_factory import KnowledgeBaseFactory
from .kb_manager import KnowledgeBaseManager
from .lightrag_kb import LightRagKB
from .milvus_kb import MilvusKB
# 注册知识库类型
KnowledgeBaseFactory.register("chroma", ChromaKB, {"description": "基于 ChromaDB 的轻量级向量知识库,适合开发和小规模"})
KnowledgeBaseFactory.register("milvus", MilvusKB, {"description": "基于 Milvus 的生产级向量知识库,适合高性能部署"})
KnowledgeBaseFactory.register("lightrag", LightRagKB, {"description": "基于图检索的知识库,支持实体关系构建和复杂查询"})
# 创建知识库管理器
work_dir = os.path.join(config.save_dir, "knowledge_base_data")
knowledge_base = KnowledgeBaseManager(work_dir)
# 创建图数据库实例
graph_base = GraphDatabase()
__all__ = ["GraphDatabase", "knowledge_base", "graph_base"]
__all__ = ["GraphDatabase"]

View File

@ -119,7 +119,7 @@ class OllamaEmbedding(BaseEmbeddingModel):
raise ValueError(f"Ollama Embedding failed: Invalid response format {result}")
return result["embeddings"]
except (requests.RequestException, json.JSONDecodeError) as e:
logger.error(f"Ollama Embedding request failed: {e}")
logger.error(f"Ollama Embedding request failed: {e}, {payload}")
raise ValueError(f"Ollama Embedding request failed: {e}")
async def apredict(self, message: list[str] | str) -> list[list[float]]:
@ -136,7 +136,7 @@ class OllamaEmbedding(BaseEmbeddingModel):
raise ValueError(f"Ollama Embedding failed: Invalid response format {result}")
return result["embeddings"]
except (httpx.RequestError, json.JSONDecodeError) as e:
logger.error(f"Ollama Embedding async request failed: {e}")
logger.error(f"Ollama Embedding async request failed: {e}, {payload}")
raise ValueError(f"Ollama Embedding async request failed: {e}")
@ -158,7 +158,7 @@ class OtherEmbedding(BaseEmbeddingModel):
raise ValueError(f"Other Embedding failed: Invalid response format {result}")
return [item["embedding"] for item in result["data"]]
except (requests.RequestException, json.JSONDecodeError) as e:
logger.error(f"Other Embedding request failed: {e}")
logger.error(f"Other Embedding request failed: {e}, {payload}")
raise ValueError(f"Other Embedding request failed: {e}")
async def apredict(self, message: list[str] | str) -> list[list[float]]:
@ -172,5 +172,5 @@ class OtherEmbedding(BaseEmbeddingModel):
raise ValueError(f"Other Embedding failed: Invalid response format {result}")
return [item["embedding"] for item in result["data"]]
except (httpx.RequestError, json.JSONDecodeError) as e:
logger.error(f"Other Embedding async request failed: {e}")
logger.error(f"Other Embedding async request failed: {e}, {payload}")
raise ValueError(f"Other Embedding async request failed: {e}")