feat: 重构智能体元数据系统,从集中式 YAML 迁移到分布式 TOML 配置

This commit is contained in:
Wenjie Zhang 2025-11-07 12:28:58 +08:00
parent bf3e8cdedf
commit 99aae1c00a
11 changed files with 181 additions and 86 deletions

View File

@ -1 +1 @@
See AGENTS.md You MUST read `./AGENTS.md`

View File

@ -6,6 +6,20 @@
仓库预置了若干可直接运行的智能体:`chatbot` 聚焦对话与动态工具调度,`mini_agent` 提供精简模板,`reporter` 演示报告类链路。这些目录展示了上下文类、Graph 构造方式、子智能体引用以及中间件组合的范例,新增功能时可以直接复用。 仓库预置了若干可直接运行的智能体:`chatbot` 聚焦对话与动态工具调度,`mini_agent` 提供精简模板,`reporter` 演示报告类链路。这些目录展示了上下文类、Graph 构造方式、子智能体引用以及中间件组合的范例,新增功能时可以直接复用。
### 智能体元数据配置
每个智能体可以通过在智能体目录下创建 `metadata.toml` 文件来配置元数据信息。这个文件使用 TOML 格式,包含以下字段:
- `name`: 智能体显示名称
- `description`: 智能体功能描述
- `examples`: 示例问题列表(数组格式)
例如,`src/agents/chatbot/metadata.toml`
<<< @/../src/agents/chatbot/metadata.toml
**注意**`metadata.toml` 文件是可选的,如果没有提供,系统将使用智能体类的基本属性。
### 创建新的智能体 ### 创建新的智能体
`src/agents` 下新建一个包,保持与现有目录一致的结构:放置 Graph 构造逻辑(通常命名为 `graph.py`),并在包内的 `__init__.py` 中暴露主类。 `src/agents` 下新建一个包,保持与现有目录一致的结构:放置 Graph 构造逻辑(通常命名为 `graph.py`),并在包内的 `__init__.py` 中暴露主类。

View File

@ -2,46 +2,48 @@
路线图可能会经常变更,如果有强烈的建议,可以在 [issue](https://github.com/xerrors/Yuxi-Know/issues) 中提。 路线图可能会经常变更,如果有强烈的建议,可以在 [issue](https://github.com/xerrors/Yuxi-Know/issues) 中提。
- **[2025/10/13]** v0.3 进入 beta 测试环节,不会再封装新的特性,仅作 bug 层面的修复
## v0.4
## Bugs
- [ ] 无法展示图谱 ### 看板
## Next - 新建 DeepAgents 智能体(暂时没有场景)
- 添加对于上传文件的支持
- 统一图谱数据结构,优化可视化方式 [#298](https://github.com/xerrors/Yuxi-Know/issues/298) [#273](https://github.com/xerrors/Yuxi-Know/issues/273) <Badge type="info" text="0.4" />
- 集成智能体评估,首先使用命令行来实现,然后考虑放在 UI 里面展示
- 开发与生产环境隔离,构建生产镜像 <Badge type="info" text="0.4" />
- 集成 LangFuse (观望) 添加用户日志与用户反馈模块,可以在 AgentView 中查看信息
- [ ] 新建 DeepAgents 智能体 ### Bugs
- [ ] 添加对于上传文件的支持 - 部分异常状态下,智能体的模型名称出现重叠[#279](https://github.com/xerrors/Yuxi-Know/issues/279)
- [ ] 统一图谱数据结构,优化可视化方式 [#298](https://github.com/xerrors/Yuxi-Know/issues/298) <Badge type="info" text="0.4" />
- [ ] 集成智能体评估,首先使用命令行来实现,然后考虑放在 UI 里面展示 ### 新增
- [ ] 开发与生产环境隔离,构建生产镜像 <Badge type="info" text="0.4" /> - 优化知识库详情页面,更加简洁清晰
### 修复
- 修复重排序模型实际未生效的问题
## Later ## v0.3
### Added
- 添加测试脚本覆盖最常见的功能已覆盖API
- 新建 tasker 模块用来管理所有的后台任务UI 上使用侧边栏管理。Tasker 中获取历史任务的时候,仅获取 top100 个 task。
- 优化对文档信息的检索展示(检索结果页、详情页)
- 优化全局配置的管理模型,优化配置管理
- 支持 MinerU 2.5 的解析方法 <Badge type="info" text="0.3.5" />
- 修改现有的智能体Demo并尽量将默认助手的特性兼容到 LangGraph 的 [`create_agent`](https://docs.langchain.com/oss/python/langchain/agents) 中
- 基于 create_agent 创建 SQL Viewer 智能体 <Badge type="info" text="0.3.5" />
- 优化 MCP 逻辑,支持 common + special 创建方式 <Badge type="info" text="0.3.5" />
- LightRAG 知识库应该可以支持修改 LLM
下面的功能**可能**会放在后续版本实现,暂时未定 ### Fixed
- 修复本地知识库的 metadata 和 向量数据库中不一致的情况。
- [ ] 集成 LangFuse (观望) 添加用户日志与用户反馈模块,可以在 AgentView 中查看信息 - v1 版本的 LangGraph 的工具渲染有问题
- upload 接口会阻塞主进程
## Done - LightRAG 知识库查看不了解析后的文本,偶然出现,未复现
- 智能体的加载状态有问题1智能体加载没有动画2切换对话和加载中使用同一个loading状态。
- 前端工具调用渲染出现问题
- [x] 添加测试脚本覆盖最常见的功能已覆盖API - 当前 ReAct 智能体有消息顺序错乱的 bug且不会默认调用工具
- [x] 新建 tasker 模块用来管理所有的后台任务UI 上使用侧边栏管理。 - 修复文件管理1文件选择的时候会跨数据库2文件校验会算上失败的文件
- [x] 优化对文档信息的检索展示(检索结果页、详情页)
- [x] 当前 ReAct 智能体有消息顺序错乱的 bug且不会默认调用工具
- [x] 优化全局配置的管理模型,优化配置管理
- [x] 支持 MinerU 2.5 的解析方法 <Badge type="info" text="0.3.5" />
- [x] 文件管理1文件选择的时候会跨数据库2文件校验会算上失败的文件
- [x] Tasker 中获取历史任务的时候,仅获取 top100 个 task。
- [x] 修改现有的智能体Demo并尽量将默认助手的特性兼容到 LangGraph 的 [`create_agent`](https://docs.langchain.com/oss/python/langchain/agents) 中
- [x] 基于 create_agent 创建 SQL Viewer 智能体 <Badge type="info" text="0.3.5" />
- [x] 优化 MCP 逻辑,支持 common + special 创建方式 <Badge type="info" text="0.3.5" />
- [x] 修复本地知识库的 metadata 和 向量数据库中不一致的情况。
- [x] v1 版本的 LangGraph 的工具渲染有问题
- [x] upload 接口会阻塞主进程
- [x] LightRAG 知识库查看不了解析后的文本,偶然出现,未复现
- [x] LightRAG 知识库应该可以支持修改 LLM
- [x] 智能体的加载状态有问题1智能体加载没有动画2切换对话和加载中使用同一个loading状态。
- [x] 前端工具调用渲染出现问题

View File

@ -2,7 +2,6 @@ import asyncio
import json import json
import traceback import traceback
import uuid import uuid
import yaml
from pathlib import Path from pathlib import Path
from fastapi import APIRouter, Body, Depends, HTTPException from fastapi import APIRouter, Body, Depends, HTTPException
@ -304,13 +303,22 @@ async def call(query: str = Body(...), meta: dict = Body(None), current_user: Us
@chat.get("/agent") @chat.get("/agent")
async def get_agent(current_user: User = Depends(get_required_user)): async def get_agent(current_user: User = Depends(get_required_user)):
"""获取所有可用智能体(需要登录)""" """获取所有可用智能体(需要登录)"""
agents = await agent_manager.get_agents_info() agents_info = await agent_manager.get_agents_info()
# logger.debug(f"agents: {agents}")
metadata = {} # Return agents with complete information
if Path("src/config/static/agents_meta.yaml").exists(): agents = [
with open("src/config/static/agents_meta.yaml") as f: {
metadata = yaml.safe_load(f) "id": agent_info["id"],
return {"agents": agents, "metadata": metadata} "name": agent_info.get("name", "Unknown"),
"description": agent_info.get("description", ""),
"examples": agent_info.get("examples", []),
"configurable_items": agent_info.get("configurable_items", []),
"has_checkpointer": agent_info.get("has_checkpointer", False)
}
for agent_info in agents_info
]
return {"agents": agents}
# TODO:[未完成]这个thread_id在前端是直接生成的1234最好传入thread_id时做校验只允许uuid4 # TODO:[未完成]这个thread_id在前端是直接生成的1234最好传入thread_id时做校验只允许uuid4

View File

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

View File

@ -1,5 +1,6 @@
from __future__ import annotations from __future__ import annotations
import importlib.util
import os import os
from abc import abstractmethod from abc import abstractmethod
from pathlib import Path from pathlib import Path
@ -8,6 +9,8 @@ from langgraph.checkpoint.memory import InMemorySaver
from langgraph.checkpoint.sqlite.aio import AsyncSqliteSaver, aiosqlite from langgraph.checkpoint.sqlite.aio import AsyncSqliteSaver, aiosqlite
from langgraph.graph.state import CompiledStateGraph from langgraph.graph.state import CompiledStateGraph
import tomllib as tomli
from src import config as sys_config from src import config as sys_config
from src.agents.common.context import BaseContext from src.agents.common.context import BaseContext
from src.utils import logger from src.utils import logger
@ -27,6 +30,7 @@ class BaseAgent:
self.context_schema = BaseContext self.context_schema = BaseContext
self.workdir = Path(sys_config.save_dir) / "agents" / self.module_name self.workdir = Path(sys_config.save_dir) / "agents" / self.module_name
self.workdir.mkdir(parents=True, exist_ok=True) self.workdir.mkdir(parents=True, exist_ok=True)
self._metadata_cache = None # Cache for metadata to avoid repeated file reads
@property @property
def module_name(self) -> str: def module_name(self) -> str:
@ -39,10 +43,15 @@ class BaseAgent:
return self.__class__.__name__ return self.__class__.__name__
async def get_info(self): async def get_info(self):
# Load metadata from file
metadata = self.load_metadata()
# Merge metadata with class attributes, metadata takes precedence
return { return {
"id": self.id, "id": self.id,
"name": self.name if hasattr(self, "name") else "Unknown", "name": metadata.get("name", getattr(self, "name", "Unknown")),
"description": self.description if hasattr(self, "description") else "Unknown", "description": metadata.get("description", getattr(self, "description", "Unknown")),
"examples": metadata.get("examples", []),
"configurable_items": self.context_schema.get_configurable_items(), "configurable_items": self.context_schema.get_configurable_items(),
"has_checkpointer": await self.check_checkpointer(), "has_checkpointer": await self.check_checkpointer(),
} }
@ -138,3 +147,43 @@ class BaseAgent:
async def get_aio_memory(self) -> AsyncSqliteSaver: async def get_aio_memory(self) -> AsyncSqliteSaver:
"""获取异步存储实例""" """获取异步存储实例"""
return AsyncSqliteSaver(await self.get_async_conn()) return AsyncSqliteSaver(await self.get_async_conn())
def load_metadata(self) -> dict:
"""Load metadata from metadata.toml file in the agent's source directory."""
if self._metadata_cache is not None:
return self._metadata_cache
# Try to find metadata.toml in the agent's source directory
try:
# Get the agent's source file directory
agent_module = self.__class__.__module__
# Use importlib to get the module's file path
spec = importlib.util.find_spec(agent_module)
if spec and spec.origin:
agent_file = Path(spec.origin)
agent_dir = agent_file.parent
else:
# Fallback: construct path from module name
module_path = agent_module.replace(".", "/")
agent_file = Path(f"src/{module_path}.py")
agent_dir = agent_file.parent
metadata_file = agent_dir / "metadata.toml"
if metadata_file.exists():
with open(metadata_file, "rb") as f:
metadata = tomli.load(f)
self._metadata_cache = metadata
logger.debug(f"Loaded metadata from {metadata_file}")
return metadata
else:
logger.debug(f"No metadata.toml found for {self.module_name} at {metadata_file}")
self._metadata_cache = {}
return {}
except Exception as e:
logger.error(f"Error loading metadata for {self.module_name}: {e}")
self._metadata_cache = {}
return {}

View File

@ -56,9 +56,8 @@
</div> </div>
<div v-else-if="!conversations.length" class="chat-examples"> <div v-else-if="!conversations.length" class="chat-examples">
<img v-if="currentAgentMetadata?.icon" class="agent-icons" :src="currentAgentMetadata?.icon" alt="智能体图标" /> <div style="margin-bottom: 150px"></div>
<div v-else style="margin-bottom: 150px"></div> <h1>您好我是{{ currentAgentName }}</h1>
<h1>您好我是{{ currentAgentName }}有什么可以帮您</h1>
<!-- <h1>{{ currentAgent ? currentAgent.name : '请选择一个智能体开始对话' }}</h1> <!-- <h1>{{ currentAgent ? currentAgent.name : '请选择一个智能体开始对话' }}</h1>
<p>{{ currentAgent ? currentAgent.description : '不同的智能体有不同的专长和能力' }}</p> --> <p>{{ currentAgent ? currentAgent.description : '不同的智能体有不同的专长和能力' }}</p> -->
@ -190,7 +189,12 @@ const userInput = ref('');
// //
const exampleQuestions = computed(() => { const exampleQuestions = computed(() => {
const examples = currentAgentMetadata.value?.examples || []; const agentId = currentAgentId.value;
let examples = [];
if (agentId && agents.value && agents.value.length > 0) {
const agent = agents.value.find(a => a.id === agentId);
examples = agent ? (agent.examples || []) : [];
}
return examples.map((text, index) => ({ return examples.map((text, index) => ({
id: index + 1, id: index + 1,
text: text text: text
@ -234,12 +238,14 @@ const currentAgentId = computed(() => {
} }
}); });
const currentAgentMetadata = computed(() => { const currentAgentName = computed(() => {
const agentId = currentAgentId.value; const agentId = currentAgentId.value;
const metadata = agentStore?.metadata || {}; if (agentId && agents.value && agents.value.length > 0) {
return agentId && metadata[agentId] ? metadata[agentId] : {}; const agent = agents.value.find(a => a.id === agentId);
return agent ? agent.name : '智能体';
}
return '智能体';
}); });
const currentAgentName = computed(() => currentAgentMetadata.value?.name || currentAgent.value?.name || '智能体');
const currentAgent = computed(() => agents.value[currentAgentId.value] || null); const currentAgent = computed(() => agents.value[currentAgentId.value] || null);
const chatsList = computed(() => threads.value || []); const chatsList = computed(() => threads.value || []);
@ -947,10 +953,17 @@ const handleExampleClick = (questionText) => {
}; };
const buildExportPayload = () => { const buildExportPayload = () => {
const agentId = currentAgentId.value;
let agentDescription = '';
if (agentId && agents.value && agents.value.length > 0) {
const agent = agents.value.find(a => a.id === agentId);
agentDescription = agent ? (agent.description || '') : '';
}
const payload = { const payload = {
chatTitle: currentThread.value?.title || '新对话', chatTitle: currentThread.value?.title || '新对话',
agentName: currentAgentName.value || currentAgent.value?.name || '智能助手', agentName: currentAgentName.value || currentAgent.value?.name || '智能助手',
agentDescription: currentAgentMetadata.value?.description || currentAgent.value?.description || '', agentDescription: agentDescription || currentAgent.value?.description || '',
messages: conversations.value ? JSON.parse(JSON.stringify(conversations.value)) : [], messages: conversations.value ? JSON.parse(JSON.stringify(conversations.value)) : [],
onGoingMessages: onGoingConvMessages.value ? JSON.parse(JSON.stringify(onGoingConvMessages.value)) : [] onGoingMessages: onGoingConvMessages.value ? JSON.parse(JSON.stringify(onGoingConvMessages.value)) : []
}; };

View File

@ -100,8 +100,8 @@ const props = defineProps({
default: true default: true
}, },
agents: { agents: {
type: Object, type: Array,
default: () => ({}) default: () => []
}, },
selectedAgentId: { selectedAgentId: {
type: String, type: String,
@ -112,8 +112,9 @@ const props = defineProps({
const emit = defineEmits(['create-chat', 'select-chat', 'delete-chat', 'rename-chat', 'toggle-sidebar', 'open-agent-modal']); const emit = defineEmits(['create-chat', 'select-chat', 'delete-chat', 'rename-chat', 'toggle-sidebar', 'open-agent-modal']);
const selectedAgentName = computed(() => { const selectedAgentName = computed(() => {
if (props.selectedAgentId && props.agents && props.agents[props.selectedAgentId]) { if (props.selectedAgentId && props.agents && props.agents.length > 0) {
return props.agents[props.selectedAgentId].name; const agent = props.agents.find(a => a.id === props.selectedAgentId);
return agent ? agent.name : '';
} }
return ''; return '';
}); });

View File

@ -6,8 +6,7 @@ import { handleChatError } from '@/utils/errorHandler';
export const useAgentStore = defineStore('agent', { export const useAgentStore = defineStore('agent', {
state: () => ({ state: () => ({
// 智能体相关状态 // 智能体相关状态
agents: {}, // 以ID为键的智能体对象 agents: [], // 智能体数组,每个元素包含完整信息
metadata: {}, // 智能体元数据
selectedAgentId: null, // 当前选中的智能体ID selectedAgentId: null, // 当前选中的智能体ID
defaultAgentId: null, // 默认智能体ID defaultAgentId: null, // 默认智能体ID
@ -34,12 +33,12 @@ export const useAgentStore = defineStore('agent', {
getters: { getters: {
// --- 智能体相关 Getters --- // --- 智能体相关 Getters ---
selectedAgent: (state) => state.selectedAgentId ? state.agents[state.selectedAgentId] : null, selectedAgent: (state) => state.selectedAgentId ? state.agents.find(a => a.id === state.selectedAgentId) : null,
defaultAgent: (state) => state.defaultAgentId ? state.agents[state.defaultAgentId] : state.agents[Object.keys(state.agents)[0]], defaultAgent: (state) => state.defaultAgentId ? state.agents.find(a => a.id === state.defaultAgentId) : state.agents[0],
agentsList: (state) => Object.values(state.agents), agentsList: (state) => state.agents,
isDefaultAgent: (state) => state.selectedAgentId === state.defaultAgentId, isDefaultAgent: (state) => state.selectedAgentId === state.defaultAgentId,
configurableItems: (state) => { configurableItems: (state) => {
const agent = state.selectedAgentId ? state.agents[state.selectedAgentId] : null; const agent = state.selectedAgentId ? state.agents.find(a => a.id === state.selectedAgentId) : null;
if (!agent || !agent.configurable_items) return {}; if (!agent || !agent.configurable_items) return {};
const agentConfigurableItems = agent.configurable_items; const agentConfigurableItems = agent.configurable_items;
@ -67,11 +66,11 @@ export const useAgentStore = defineStore('agent', {
await this.fetchAgents(); await this.fetchAgents();
await this.fetchDefaultAgent(); await this.fetchDefaultAgent();
if (!this.selectedAgentId || !this.agents[this.selectedAgentId]) { if (!this.selectedAgentId || !this.agents.find(a => a.id === this.selectedAgentId)) {
if (this.defaultAgentId && this.agents[this.defaultAgentId]) { if (this.defaultAgentId && this.agents.find(a => a.id === this.defaultAgentId)) {
this.selectAgent(this.defaultAgentId); this.selectAgent(this.defaultAgentId);
} else if (Object.keys(this.agents).length > 0) { } else if (this.agents.length > 0) {
const firstAgentId = Object.keys(this.agents)[0]; const firstAgentId = this.agents[0].id;
this.selectAgent(firstAgentId); this.selectAgent(firstAgentId);
} }
} else { } else {
@ -98,12 +97,8 @@ export const useAgentStore = defineStore('agent', {
try { try {
const response = await agentApi.getAgents(); const response = await agentApi.getAgents();
// 将数组转换为以ID为键的对象 // 直接使用返回的 agents 数组
this.agents = response.agents.reduce((acc, agent) => { this.agents = response.agents;
acc[agent.id] = agent;
return acc;
}, {});
this.metadata = response.metadata;
} catch (error) { } catch (error) {
console.error('Failed to fetch agents:', error); console.error('Failed to fetch agents:', error);
handleChatError(error, 'fetch'); handleChatError(error, 'fetch');
@ -141,7 +136,7 @@ export const useAgentStore = defineStore('agent', {
// 选择智能体 // 选择智能体
selectAgent(agentId) { selectAgent(agentId) {
if (this.agents[agentId]) { if (this.agents.find(a => a.id === agentId)) {
this.selectedAgentId = agentId; this.selectedAgentId = agentId;
// 清空之前的配置 // 清空之前的配置
this.agentConfig = {}; this.agentConfig = {};
@ -229,7 +224,7 @@ export const useAgentStore = defineStore('agent', {
// 重置store状态 // 重置store状态
reset() { reset() {
this.agents = {}; this.agents = [];
this.selectedAgentId = null; this.selectedAgentId = null;
this.defaultAgentId = null; this.defaultAgentId = null;
this.agentConfig = {}; this.agentConfig = {};

View File

@ -13,20 +13,23 @@
<div class="agent-modal-content"> <div class="agent-modal-content">
<div class="agents-grid"> <div class="agents-grid">
<div <div
v-for="(agent, id) in agents" v-for="agent in agents"
:key="id" :key="agent.id"
class="agent-card" class="agent-card"
:class="{ 'selected': id === selectedAgentId }" :class="{ 'selected': agent.id === selectedAgentId }"
@click="selectAgentFromModal(id)" @click="selectAgentFromModal(agent.id)"
> >
<div class="agent-card-header"> <div class="agent-card-header">
<div class="agent-card-title"> <div class="agent-card-title">
<span class="agent-card-name">{{ agent.name }}</span> <span class="agent-card-name">{{ agent.name || 'Unknown' }}</span>
<StarFilled v-if="id === defaultAgentId" class="default-icon" /> <StarFilled v-if="agent.id === defaultAgentId" class="default-icon" />
<StarOutlined v-else @click.prevent="setAsDefaultAgent(id)" class="default-icon" /> <StarOutlined v-else @click.prevent="setAsDefaultAgent(agent.id)" class="default-icon" />
</div> </div>
</div> </div>
<div class="agent-card-description">{{ agent.description }}</div>
<div class="agent-card-description">
{{ agent.description || '' }}
</div>
</div> </div>
</div> </div>
</div> </div>
@ -97,7 +100,7 @@
</template> </template>
<script setup> <script setup>
import { ref, reactive, watch } from 'vue'; import { ref, reactive, watch, computed } from 'vue';
import { import {
StarOutlined, StarOutlined,
StarFilled, StarFilled,
@ -130,6 +133,7 @@ const {
selectedAgentId, selectedAgentId,
defaultAgentId, defaultAgentId,
} = storeToRefs(agentStore); } = storeToRefs(agentStore);
const state = reactive({ const state = reactive({
agentModalOpen: false, agentModalOpen: false,
isConfigSidebarOpen: false, isConfigSidebarOpen: false,