Skip to content

Commit 99aae1c

Browse files
committed
feat: 重构智能体元数据系统,从集中式 YAML 迁移到分布式 TOML 配置
1 parent bf3e8cd commit 99aae1c

File tree

11 files changed

+178
-83
lines changed

11 files changed

+178
-83
lines changed

CLAUDE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
See AGENTS.md
1+
You MUST read `./AGENTS.md`

docs/advanced/agents-config.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,20 @@
66

77
仓库预置了若干可直接运行的智能体:`chatbot` 聚焦对话与动态工具调度,`mini_agent` 提供精简模板,`reporter` 演示报告类链路。这些目录展示了上下文类、Graph 构造方式、子智能体引用以及中间件组合的范例,新增功能时可以直接复用。
88

9+
### 智能体元数据配置
10+
11+
每个智能体可以通过在智能体目录下创建 `metadata.toml` 文件来配置元数据信息。这个文件使用 TOML 格式,包含以下字段:
12+
13+
- `name`: 智能体显示名称
14+
- `description`: 智能体功能描述
15+
- `examples`: 示例问题列表(数组格式)
16+
17+
例如,`src/agents/chatbot/metadata.toml`
18+
19+
<<< @/../src/agents/chatbot/metadata.toml
20+
21+
**注意**`metadata.toml` 文件是可选的,如果没有提供,系统将使用智能体类的基本属性。
22+
923
### 创建新的智能体
1024

1125
`src/agents` 下新建一个包,保持与现有目录一致的结构:放置 Graph 构造逻辑(通常命名为 `graph.py`),并在包内的 `__init__.py` 中暴露主类。

docs/changelog/roadmap.md

Lines changed: 34 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -2,46 +2,48 @@
22

33
路线图可能会经常变更,如果有强烈的建议,可以在 [issue](https://github.com/xerrors/Yuxi-Know/issues) 中提。
44

5-
- **[2025/10/13]** v0.3 进入 beta 测试环节,不会再封装新的特性,仅作 bug 层面的修复
65

6+
## v0.4
77

8-
## Bugs
98

10-
- [ ] 无法展示图谱
119

12-
## Next
10+
### 看板
1311

14-
- [ ] 新建 DeepAgents 智能体
15-
- [ ] 添加对于上传文件的支持
16-
- [ ] 统一图谱数据结构,优化可视化方式 [#298](https://github.com/xerrors/Yuxi-Know/issues/298) <Badge type="info" text="0.4" />
17-
- [ ] 集成智能体评估,首先使用命令行来实现,然后考虑放在 UI 里面展示
18-
- [ ] 开发与生产环境隔离,构建生产镜像 <Badge type="info" text="0.4" />
12+
- 新建 DeepAgents 智能体(暂时没有场景)
13+
- 添加对于上传文件的支持
14+
- 统一图谱数据结构,优化可视化方式 [#298](https://github.com/xerrors/Yuxi-Know/issues/298) [#273](https://github.com/xerrors/Yuxi-Know/issues/273) <Badge type="info" text="0.4" />
15+
- 集成智能体评估,首先使用命令行来实现,然后考虑放在 UI 里面展示
16+
- 开发与生产环境隔离,构建生产镜像 <Badge type="info" text="0.4" />
17+
- 集成 LangFuse (观望) 添加用户日志与用户反馈模块,可以在 AgentView 中查看信息
1918

19+
### Bugs
20+
- 部分异常状态下,智能体的模型名称出现重叠[#279](https://github.com/xerrors/Yuxi-Know/issues/279)
2021

21-
## Later
22+
### 新增
23+
- 优化知识库详情页面,更加简洁清晰
2224

23-
下面的功能**可能**会放在后续版本实现,暂时未定
25+
### 修复
26+
- 修复重排序模型实际未生效的问题
2427

25-
- [ ] 集成 LangFuse (观望) 添加用户日志与用户反馈模块,可以在 AgentView 中查看信息
2628

27-
## Done
29+
## v0.3
30+
### Added
31+
- 添加测试脚本,覆盖最常见的功能(已覆盖API)
32+
- 新建 tasker 模块,用来管理所有的后台任务,UI 上使用侧边栏管理。Tasker 中获取历史任务的时候,仅获取 top100 个 task。
33+
- 优化对文档信息的检索展示(检索结果页、详情页)
34+
- 优化全局配置的管理模型,优化配置管理
35+
- 支持 MinerU 2.5 的解析方法 <Badge type="info" text="0.3.5" />
36+
- 修改现有的智能体Demo,并尽量将默认助手的特性兼容到 LangGraph 的 [`create_agent`](https://docs.langchain.com/oss/python/langchain/agents)
37+
- 基于 create_agent 创建 SQL Viewer 智能体 <Badge type="info" text="0.3.5" />
38+
- 优化 MCP 逻辑,支持 common + special 创建方式 <Badge type="info" text="0.3.5" />
39+
- LightRAG 知识库应该可以支持修改 LLM
2840

29-
30-
- [x] 添加测试脚本,覆盖最常见的功能(已覆盖API)
31-
- [x] 新建 tasker 模块,用来管理所有的后台任务,UI 上使用侧边栏管理。
32-
- [x] 优化对文档信息的检索展示(检索结果页、详情页)
33-
- [x] 当前 ReAct 智能体有消息顺序错乱的 bug,且不会默认调用工具
34-
- [x] 优化全局配置的管理模型,优化配置管理
35-
- [x] 支持 MinerU 2.5 的解析方法 <Badge type="info" text="0.3.5" />
36-
- [x] 文件管理:(1)文件选择的时候会跨数据库;(2)文件校验会算上失败的文件;
37-
- [x] Tasker 中获取历史任务的时候,仅获取 top100 个 task。
38-
- [x] 修改现有的智能体Demo,并尽量将默认助手的特性兼容到 LangGraph 的 [`create_agent`](https://docs.langchain.com/oss/python/langchain/agents)
39-
- [x] 基于 create_agent 创建 SQL Viewer 智能体 <Badge type="info" text="0.3.5" />
40-
- [x] 优化 MCP 逻辑,支持 common + special 创建方式 <Badge type="info" text="0.3.5" />
41-
- [x] 修复本地知识库的 metadata 和 向量数据库中不一致的情况。
42-
- [x] v1 版本的 LangGraph 的工具渲染有问题
43-
- [x] upload 接口会阻塞主进程
44-
- [x] LightRAG 知识库查看不了解析后的文本,偶然出现,未复现
45-
- [x] LightRAG 知识库应该可以支持修改 LLM
46-
- [x] 智能体的加载状态有问题:(1)智能体加载没有动画;(2)切换对话和加载中,使用同一个loading状态。
47-
- [x] 前端工具调用渲染出现问题
41+
### Fixed
42+
- 修复本地知识库的 metadata 和 向量数据库中不一致的情况。
43+
- v1 版本的 LangGraph 的工具渲染有问题
44+
- upload 接口会阻塞主进程
45+
- LightRAG 知识库查看不了解析后的文本,偶然出现,未复现
46+
- 智能体的加载状态有问题:(1)智能体加载没有动画;(2)切换对话和加载中,使用同一个loading状态。
47+
- 前端工具调用渲染出现问题
48+
- 当前 ReAct 智能体有消息顺序错乱的 bug,且不会默认调用工具
49+
- 修复文件管理:(1)文件选择的时候会跨数据库;(2)文件校验会算上失败的文件;

server/routers/chat_router.py

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
import json
33
import traceback
44
import uuid
5-
import yaml
65
from pathlib import Path
76

87
from fastapi import APIRouter, Body, Depends, HTTPException
@@ -304,13 +303,22 @@ async def call_async(query):
304303
@chat.get("/agent")
305304
async def get_agent(current_user: User = Depends(get_required_user)):
306305
"""获取所有可用智能体(需要登录)"""
307-
agents = await agent_manager.get_agents_info()
308-
# logger.debug(f"agents: {agents}")
309-
metadata = {}
310-
if Path("src/config/static/agents_meta.yaml").exists():
311-
with open("src/config/static/agents_meta.yaml") as f:
312-
metadata = yaml.safe_load(f)
313-
return {"agents": agents, "metadata": metadata}
306+
agents_info = await agent_manager.get_agents_info()
307+
308+
# Return agents with complete information
309+
agents = [
310+
{
311+
"id": agent_info["id"],
312+
"name": agent_info.get("name", "Unknown"),
313+
"description": agent_info.get("description", ""),
314+
"examples": agent_info.get("examples", []),
315+
"configurable_items": agent_info.get("configurable_items", []),
316+
"has_checkpointer": agent_info.get("has_checkpointer", False)
317+
}
318+
for agent_info in agents_info
319+
]
320+
321+
return {"agents": agents}
314322

315323

316324
# TODO:[未完成]这个thread_id在前端是直接生成的1234,最好传入thread_id时做校验只允许uuid4

src/agents/chatbot/metadata.toml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
name = "智能聊天助手"
2+
description = "基础的AI对话助手,支持工具调用,MCP,并具备丰富的知识库。"
3+
examples = [
4+
"你好,请介绍一下你自己",
5+
"帮我写一封商务邮件",
6+
"解释一下什么是机器学习",
7+
"推荐几本好书",
8+
"如何提高工作效率?"
9+
]

src/agents/common/base.py

Lines changed: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from __future__ import annotations
22

3+
import importlib.util
34
import os
45
from abc import abstractmethod
56
from pathlib import Path
@@ -8,6 +9,8 @@
89
from langgraph.checkpoint.sqlite.aio import AsyncSqliteSaver, aiosqlite
910
from langgraph.graph.state import CompiledStateGraph
1011

12+
import tomllib as tomli
13+
1114
from src import config as sys_config
1215
from src.agents.common.context import BaseContext
1316
from src.utils import logger
@@ -27,6 +30,7 @@ def __init__(self, **kwargs):
2730
self.context_schema = BaseContext
2831
self.workdir = Path(sys_config.save_dir) / "agents" / self.module_name
2932
self.workdir.mkdir(parents=True, exist_ok=True)
33+
self._metadata_cache = None # Cache for metadata to avoid repeated file reads
3034

3135
@property
3236
def module_name(self) -> str:
@@ -39,10 +43,15 @@ def id(self) -> str:
3943
return self.__class__.__name__
4044

4145
async def get_info(self):
46+
# Load metadata from file
47+
metadata = self.load_metadata()
48+
49+
# Merge metadata with class attributes, metadata takes precedence
4250
return {
4351
"id": self.id,
44-
"name": self.name if hasattr(self, "name") else "Unknown",
45-
"description": self.description if hasattr(self, "description") else "Unknown",
52+
"name": metadata.get("name", getattr(self, "name", "Unknown")),
53+
"description": metadata.get("description", getattr(self, "description", "Unknown")),
54+
"examples": metadata.get("examples", []),
4655
"configurable_items": self.context_schema.get_configurable_items(),
4756
"has_checkpointer": await self.check_checkpointer(),
4857
}
@@ -138,3 +147,43 @@ async def get_async_conn(self) -> aiosqlite.Connection:
138147
async def get_aio_memory(self) -> AsyncSqliteSaver:
139148
"""获取异步存储实例"""
140149
return AsyncSqliteSaver(await self.get_async_conn())
150+
151+
152+
def load_metadata(self) -> dict:
153+
"""Load metadata from metadata.toml file in the agent's source directory."""
154+
if self._metadata_cache is not None:
155+
return self._metadata_cache
156+
157+
# Try to find metadata.toml in the agent's source directory
158+
try:
159+
# Get the agent's source file directory
160+
agent_module = self.__class__.__module__
161+
162+
# Use importlib to get the module's file path
163+
spec = importlib.util.find_spec(agent_module)
164+
if spec and spec.origin:
165+
agent_file = Path(spec.origin)
166+
agent_dir = agent_file.parent
167+
else:
168+
# Fallback: construct path from module name
169+
module_path = agent_module.replace(".", "/")
170+
agent_file = Path(f"src/{module_path}.py")
171+
agent_dir = agent_file.parent
172+
173+
metadata_file = agent_dir / "metadata.toml"
174+
175+
if metadata_file.exists():
176+
with open(metadata_file, "rb") as f:
177+
metadata = tomli.load(f)
178+
self._metadata_cache = metadata
179+
logger.debug(f"Loaded metadata from {metadata_file}")
180+
return metadata
181+
else:
182+
logger.debug(f"No metadata.toml found for {self.module_name} at {metadata_file}")
183+
self._metadata_cache = {}
184+
return {}
185+
186+
except Exception as e:
187+
logger.error(f"Error loading metadata for {self.module_name}: {e}")
188+
self._metadata_cache = {}
189+
return {}

src/config/static/agents_meta.yaml

Whitespace-only changes.

web/src/components/AgentChatComponent.vue

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -56,9 +56,8 @@
5656
</div>
5757

5858
<div v-else-if="!conversations.length" class="chat-examples">
59-
<img v-if="currentAgentMetadata?.icon" class="agent-icons" :src="currentAgentMetadata?.icon" alt="智能体图标" />
60-
<div v-else style="margin-bottom: 150px"></div>
61-
<h1>您好,我是{{ currentAgentName }}!有什么可以帮您?</h1>
59+
<div style="margin-bottom: 150px"></div>
60+
<h1>您好,我是{{ currentAgentName }}!</h1>
6261
<!-- <h1>{{ currentAgent ? currentAgent.name : '请选择一个智能体开始对话' }}</h1>
6362
<p>{{ currentAgent ? currentAgent.description : '不同的智能体有不同的专长和能力' }}</p> -->
6463

@@ -190,7 +189,12 @@ const userInput = ref('');
190189
191190
// 从智能体元数据获取示例问题
192191
const exampleQuestions = computed(() => {
193-
const examples = currentAgentMetadata.value?.examples || [];
192+
const agentId = currentAgentId.value;
193+
let examples = [];
194+
if (agentId && agents.value && agents.value.length > 0) {
195+
const agent = agents.value.find(a => a.id === agentId);
196+
examples = agent ? (agent.examples || []) : [];
197+
}
194198
return examples.map((text, index) => ({
195199
id: index + 1,
196200
text: text
@@ -234,12 +238,14 @@ const currentAgentId = computed(() => {
234238
}
235239
});
236240
237-
const currentAgentMetadata = computed(() => {
241+
const currentAgentName = computed(() => {
238242
const agentId = currentAgentId.value;
239-
const metadata = agentStore?.metadata || {};
240-
return agentId && metadata[agentId] ? metadata[agentId] : {};
243+
if (agentId && agents.value && agents.value.length > 0) {
244+
const agent = agents.value.find(a => a.id === agentId);
245+
return agent ? agent.name : '智能体';
246+
}
247+
return '智能体';
241248
});
242-
const currentAgentName = computed(() => currentAgentMetadata.value?.name || currentAgent.value?.name || '智能体');
243249
244250
const currentAgent = computed(() => agents.value[currentAgentId.value] || null);
245251
const chatsList = computed(() => threads.value || []);
@@ -947,10 +953,17 @@ const handleExampleClick = (questionText) => {
947953
};
948954
949955
const buildExportPayload = () => {
956+
const agentId = currentAgentId.value;
957+
let agentDescription = '';
958+
if (agentId && agents.value && agents.value.length > 0) {
959+
const agent = agents.value.find(a => a.id === agentId);
960+
agentDescription = agent ? (agent.description || '') : '';
961+
}
962+
950963
const payload = {
951964
chatTitle: currentThread.value?.title || '新对话',
952965
agentName: currentAgentName.value || currentAgent.value?.name || '智能助手',
953-
agentDescription: currentAgentMetadata.value?.description || currentAgent.value?.description || '',
966+
agentDescription: agentDescription || currentAgent.value?.description || '',
954967
messages: conversations.value ? JSON.parse(JSON.stringify(conversations.value)) : [],
955968
onGoingMessages: onGoingConvMessages.value ? JSON.parse(JSON.stringify(onGoingConvMessages.value)) : []
956969
};

web/src/components/ChatSidebarComponent.vue

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -100,8 +100,8 @@ const props = defineProps({
100100
default: true
101101
},
102102
agents: {
103-
type: Object,
104-
default: () => ({})
103+
type: Array,
104+
default: () => []
105105
},
106106
selectedAgentId: {
107107
type: String,
@@ -112,8 +112,9 @@ const props = defineProps({
112112
const emit = defineEmits(['create-chat', 'select-chat', 'delete-chat', 'rename-chat', 'toggle-sidebar', 'open-agent-modal']);
113113
114114
const selectedAgentName = computed(() => {
115-
if (props.selectedAgentId && props.agents && props.agents[props.selectedAgentId]) {
116-
return props.agents[props.selectedAgentId].name;
115+
if (props.selectedAgentId && props.agents && props.agents.length > 0) {
116+
const agent = props.agents.find(a => a.id === props.selectedAgentId);
117+
return agent ? agent.name : '';
117118
}
118119
return '';
119120
});

0 commit comments

Comments
 (0)