diff --git a/CLAUDE.md b/CLAUDE.md index 5c11c218..b69821bf 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1 +1 @@ -See AGENTS.md +You MUST read `./AGENTS.md` \ No newline at end of file diff --git a/docs/advanced/agents-config.md b/docs/advanced/agents-config.md index 55ad8099..73c34583 100644 --- a/docs/advanced/agents-config.md +++ b/docs/advanced/agents-config.md @@ -6,6 +6,20 @@ 仓库预置了若干可直接运行的智能体:`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` 中暴露主类。 diff --git a/docs/changelog/roadmap.md b/docs/changelog/roadmap.md index 274fd58e..951cce09 100644 --- a/docs/changelog/roadmap.md +++ b/docs/changelog/roadmap.md @@ -2,46 +2,48 @@ 路线图可能会经常变更,如果有强烈的建议,可以在 [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) +- 集成智能体评估,首先使用命令行来实现,然后考虑放在 UI 里面展示 +- 开发与生产环境隔离,构建生产镜像 +- 集成 LangFuse (观望) 添加用户日志与用户反馈模块,可以在 AgentView 中查看信息 -- [ ] 新建 DeepAgents 智能体 -- [ ] 添加对于上传文件的支持 -- [ ] 统一图谱数据结构,优化可视化方式 [#298](https://github.com/xerrors/Yuxi-Know/issues/298) -- [ ] 集成智能体评估,首先使用命令行来实现,然后考虑放在 UI 里面展示 -- [ ] 开发与生产环境隔离,构建生产镜像 +### Bugs +- 部分异常状态下,智能体的模型名称出现重叠[#279](https://github.com/xerrors/Yuxi-Know/issues/279) + +### 新增 +- 优化知识库详情页面,更加简洁清晰 + +### 修复 +- 修复重排序模型实际未生效的问题 -## Later +## v0.3 +### Added +- 添加测试脚本,覆盖最常见的功能(已覆盖API) +- 新建 tasker 模块,用来管理所有的后台任务,UI 上使用侧边栏管理。Tasker 中获取历史任务的时候,仅获取 top100 个 task。 +- 优化对文档信息的检索展示(检索结果页、详情页) +- 优化全局配置的管理模型,优化配置管理 +- 支持 MinerU 2.5 的解析方法 +- 修改现有的智能体Demo,并尽量将默认助手的特性兼容到 LangGraph 的 [`create_agent`](https://docs.langchain.com/oss/python/langchain/agents) 中 +- 基于 create_agent 创建 SQL Viewer 智能体 +- 优化 MCP 逻辑,支持 common + special 创建方式 +- LightRAG 知识库应该可以支持修改 LLM -下面的功能**可能**会放在后续版本实现,暂时未定 - -- [ ] 集成 LangFuse (观望) 添加用户日志与用户反馈模块,可以在 AgentView 中查看信息 - -## Done - - -- [x] 添加测试脚本,覆盖最常见的功能(已覆盖API) -- [x] 新建 tasker 模块,用来管理所有的后台任务,UI 上使用侧边栏管理。 -- [x] 优化对文档信息的检索展示(检索结果页、详情页) -- [x] 当前 ReAct 智能体有消息顺序错乱的 bug,且不会默认调用工具 -- [x] 优化全局配置的管理模型,优化配置管理 -- [x] 支持 MinerU 2.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 智能体 -- [x] 优化 MCP 逻辑,支持 common + special 创建方式 -- [x] 修复本地知识库的 metadata 和 向量数据库中不一致的情况。 -- [x] v1 版本的 LangGraph 的工具渲染有问题 -- [x] upload 接口会阻塞主进程 -- [x] LightRAG 知识库查看不了解析后的文本,偶然出现,未复现 -- [x] LightRAG 知识库应该可以支持修改 LLM -- [x] 智能体的加载状态有问题:(1)智能体加载没有动画;(2)切换对话和加载中,使用同一个loading状态。 -- [x] 前端工具调用渲染出现问题 \ No newline at end of file +### Fixed +- 修复本地知识库的 metadata 和 向量数据库中不一致的情况。 +- v1 版本的 LangGraph 的工具渲染有问题 +- upload 接口会阻塞主进程 +- LightRAG 知识库查看不了解析后的文本,偶然出现,未复现 +- 智能体的加载状态有问题:(1)智能体加载没有动画;(2)切换对话和加载中,使用同一个loading状态。 +- 前端工具调用渲染出现问题 +- 当前 ReAct 智能体有消息顺序错乱的 bug,且不会默认调用工具 +- 修复文件管理:(1)文件选择的时候会跨数据库;(2)文件校验会算上失败的文件; \ No newline at end of file diff --git a/server/routers/chat_router.py b/server/routers/chat_router.py index 26e5b279..106bdc29 100644 --- a/server/routers/chat_router.py +++ b/server/routers/chat_router.py @@ -2,7 +2,6 @@ import asyncio import json import traceback import uuid -import yaml from pathlib import Path 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") async def get_agent(current_user: User = Depends(get_required_user)): """获取所有可用智能体(需要登录)""" - agents = await agent_manager.get_agents_info() - # logger.debug(f"agents: {agents}") - metadata = {} - if Path("src/config/static/agents_meta.yaml").exists(): - with open("src/config/static/agents_meta.yaml") as f: - metadata = yaml.safe_load(f) - return {"agents": agents, "metadata": metadata} + agents_info = await agent_manager.get_agents_info() + + # Return agents with complete information + agents = [ + { + "id": agent_info["id"], + "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 diff --git a/src/agents/chatbot/metadata.toml b/src/agents/chatbot/metadata.toml new file mode 100644 index 00000000..dbb2ed9c --- /dev/null +++ b/src/agents/chatbot/metadata.toml @@ -0,0 +1,9 @@ +name = "智能聊天助手" +description = "基础的AI对话助手,支持工具调用,MCP,并具备丰富的知识库。" +examples = [ + "你好,请介绍一下你自己", + "帮我写一封商务邮件", + "解释一下什么是机器学习", + "推荐几本好书", + "如何提高工作效率?" +] \ No newline at end of file diff --git a/src/agents/common/base.py b/src/agents/common/base.py index 702368d0..e0247ed2 100644 --- a/src/agents/common/base.py +++ b/src/agents/common/base.py @@ -1,5 +1,6 @@ from __future__ import annotations +import importlib.util import os from abc import abstractmethod from pathlib import Path @@ -8,6 +9,8 @@ from langgraph.checkpoint.memory import InMemorySaver from langgraph.checkpoint.sqlite.aio import AsyncSqliteSaver, aiosqlite from langgraph.graph.state import CompiledStateGraph +import tomllib as tomli + from src import config as sys_config from src.agents.common.context import BaseContext from src.utils import logger @@ -27,6 +30,7 @@ class BaseAgent: self.context_schema = BaseContext self.workdir = Path(sys_config.save_dir) / "agents" / self.module_name self.workdir.mkdir(parents=True, exist_ok=True) + self._metadata_cache = None # Cache for metadata to avoid repeated file reads @property def module_name(self) -> str: @@ -39,10 +43,15 @@ class BaseAgent: return self.__class__.__name__ async def get_info(self): + # Load metadata from file + metadata = self.load_metadata() + + # Merge metadata with class attributes, metadata takes precedence return { "id": self.id, - "name": self.name if hasattr(self, "name") else "Unknown", - "description": self.description if hasattr(self, "description") else "Unknown", + "name": metadata.get("name", getattr(self, "name", "Unknown")), + "description": metadata.get("description", getattr(self, "description", "Unknown")), + "examples": metadata.get("examples", []), "configurable_items": self.context_schema.get_configurable_items(), "has_checkpointer": await self.check_checkpointer(), } @@ -138,3 +147,43 @@ class BaseAgent: async def get_aio_memory(self) -> AsyncSqliteSaver: """获取异步存储实例""" 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 {} \ No newline at end of file diff --git a/src/config/static/agents_meta.yaml b/src/config/static/agents_meta.yaml deleted file mode 100644 index e69de29b..00000000 diff --git a/web/src/components/AgentChatComponent.vue b/web/src/components/AgentChatComponent.vue index d9a04db2..f5ec1b31 100644 --- a/web/src/components/AgentChatComponent.vue +++ b/web/src/components/AgentChatComponent.vue @@ -56,9 +56,8 @@
- 智能体图标 -
-

您好,我是{{ currentAgentName }}!有什么可以帮您?

+
+

您好,我是{{ currentAgentName }}!

@@ -190,7 +189,12 @@ const userInput = ref(''); // 从智能体元数据获取示例问题 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) => ({ id: index + 1, text: text @@ -234,12 +238,14 @@ const currentAgentId = computed(() => { } }); -const currentAgentMetadata = computed(() => { +const currentAgentName = computed(() => { const agentId = currentAgentId.value; - const metadata = agentStore?.metadata || {}; - return agentId && metadata[agentId] ? metadata[agentId] : {}; + if (agentId && agents.value && agents.value.length > 0) { + 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 chatsList = computed(() => threads.value || []); @@ -947,10 +953,17 @@ const handleExampleClick = (questionText) => { }; 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 = { chatTitle: currentThread.value?.title || '新对话', 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)) : [], onGoingMessages: onGoingConvMessages.value ? JSON.parse(JSON.stringify(onGoingConvMessages.value)) : [] }; diff --git a/web/src/components/ChatSidebarComponent.vue b/web/src/components/ChatSidebarComponent.vue index 54b2b178..59ef21f4 100644 --- a/web/src/components/ChatSidebarComponent.vue +++ b/web/src/components/ChatSidebarComponent.vue @@ -100,8 +100,8 @@ const props = defineProps({ default: true }, agents: { - type: Object, - default: () => ({}) + type: Array, + default: () => [] }, selectedAgentId: { 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 selectedAgentName = computed(() => { - if (props.selectedAgentId && props.agents && props.agents[props.selectedAgentId]) { - return props.agents[props.selectedAgentId].name; + if (props.selectedAgentId && props.agents && props.agents.length > 0) { + const agent = props.agents.find(a => a.id === props.selectedAgentId); + return agent ? agent.name : ''; } return ''; }); diff --git a/web/src/stores/agent.js b/web/src/stores/agent.js index 6f882155..6ceb8fe9 100644 --- a/web/src/stores/agent.js +++ b/web/src/stores/agent.js @@ -6,8 +6,7 @@ import { handleChatError } from '@/utils/errorHandler'; export const useAgentStore = defineStore('agent', { state: () => ({ // 智能体相关状态 - agents: {}, // 以ID为键的智能体对象 - metadata: {}, // 智能体元数据 + agents: [], // 智能体数组,每个元素包含完整信息 selectedAgentId: null, // 当前选中的智能体ID defaultAgentId: null, // 默认智能体ID @@ -34,12 +33,12 @@ export const useAgentStore = defineStore('agent', { getters: { // --- 智能体相关 Getters --- - selectedAgent: (state) => state.selectedAgentId ? state.agents[state.selectedAgentId] : null, - defaultAgent: (state) => state.defaultAgentId ? state.agents[state.defaultAgentId] : state.agents[Object.keys(state.agents)[0]], - agentsList: (state) => Object.values(state.agents), + selectedAgent: (state) => state.selectedAgentId ? state.agents.find(a => a.id === state.selectedAgentId) : null, + defaultAgent: (state) => state.defaultAgentId ? state.agents.find(a => a.id === state.defaultAgentId) : state.agents[0], + agentsList: (state) => state.agents, isDefaultAgent: (state) => state.selectedAgentId === state.defaultAgentId, 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 {}; const agentConfigurableItems = agent.configurable_items; @@ -67,11 +66,11 @@ export const useAgentStore = defineStore('agent', { await this.fetchAgents(); await this.fetchDefaultAgent(); - if (!this.selectedAgentId || !this.agents[this.selectedAgentId]) { - if (this.defaultAgentId && this.agents[this.defaultAgentId]) { + if (!this.selectedAgentId || !this.agents.find(a => a.id === this.selectedAgentId)) { + if (this.defaultAgentId && this.agents.find(a => a.id === this.defaultAgentId)) { this.selectAgent(this.defaultAgentId); - } else if (Object.keys(this.agents).length > 0) { - const firstAgentId = Object.keys(this.agents)[0]; + } else if (this.agents.length > 0) { + const firstAgentId = this.agents[0].id; this.selectAgent(firstAgentId); } } else { @@ -98,12 +97,8 @@ export const useAgentStore = defineStore('agent', { try { const response = await agentApi.getAgents(); - // 将数组转换为以ID为键的对象 - this.agents = response.agents.reduce((acc, agent) => { - acc[agent.id] = agent; - return acc; - }, {}); - this.metadata = response.metadata; + // 直接使用返回的 agents 数组 + this.agents = response.agents; } catch (error) { console.error('Failed to fetch agents:', error); handleChatError(error, 'fetch'); @@ -141,7 +136,7 @@ export const useAgentStore = defineStore('agent', { // 选择智能体 selectAgent(agentId) { - if (this.agents[agentId]) { + if (this.agents.find(a => a.id === agentId)) { this.selectedAgentId = agentId; // 清空之前的配置 this.agentConfig = {}; @@ -229,7 +224,7 @@ export const useAgentStore = defineStore('agent', { // 重置store状态 reset() { - this.agents = {}; + this.agents = []; this.selectedAgentId = null; this.defaultAgentId = null; this.agentConfig = {}; diff --git a/web/src/views/AgentView.vue b/web/src/views/AgentView.vue index 3a60f642..e9f6dda7 100644 --- a/web/src/views/AgentView.vue +++ b/web/src/views/AgentView.vue @@ -13,20 +13,23 @@
- {{ agent.name }} - - + {{ agent.name || 'Unknown' }} + +
-
{{ agent.description }}
+ +
+ {{ agent.description || '' }} +
@@ -97,7 +100,7 @@