From 67ddbe6f40ce6e32a794d55a56c870d8ebe018fe Mon Sep 17 00:00:00 2001 From: Wenjie Zhang Date: Wed, 12 Nov 2025 11:00:39 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=20CLAUDE.md=20?= =?UTF-8?q?=E6=96=87=E4=BB=B6=E4=BB=A5=E6=8F=90=E4=BE=9B=E9=A1=B9=E7=9B=AE?= =?UTF-8?q?=E5=BC=80=E5=8F=91=E6=8C=87=E5=AF=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 CLAUDE.md 文件,包含项目概述、开发环境管理、项目架构、开发规范、特色功能模块及调试和故障排除指南。 - 详细描述了使用 Docker Compose 管理开发环境的步骤和常用命令。 - 介绍了项目的技术栈、核心目录结构及主要服务,确保开发者能够快速上手。 --- CLAUDE.md | 158 ++++++++++++++++++ server/routers/chat_router.py | 149 +++++++++++------ server/routers/knowledge_router.py | 36 ++-- server/routers/system_router.py | 2 - src/agents/common/base.py | 4 +- src/agents/common/middlewares/__init__.py | 2 +- .../middlewares/attachment_middleware.py | 3 +- .../common/middlewares/context_middlewares.py | 4 +- src/agents/common/subagents/calc_agent.py | 1 + src/agents/common/toolkits/mysql/tools.py | 2 +- src/agents/common/tools.py | 5 +- src/agents/deep_agent/context.py | 1 - src/agents/deep_agent/graph.py | 9 +- src/agents/reporter/graph.py | 1 - src/knowledge/implementations/chroma.py | 4 +- src/knowledge/implementations/milvus.py | 4 +- test/api/test_knowledge_router.py | 4 +- test/api/test_settings_router.py | 4 +- web/src/components/AgentChatComponent.vue | 23 ++- .../components/ModelProvidersComponent.vue | 7 +- 20 files changed, 315 insertions(+), 108 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index b69821bf..bb719d3f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1 +1,159 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## 项目概述 + +Yuxi-Know 是一个基于大模型的智能知识库与知识图谱智能体开发平台,融合了 RAG 技术与知识图谱技术,基于 LangGraph v1 + Vue.js + FastAPI + LightRAG 架构构建。项目完全通过 Docker Compose 进行管理,支持热重载开发。 + +## 开发环境管理 + +### 核心原则 +- 所有开发调试都应在运行的 Docker 容器环境中进行 +- 使用 `docker compose up -d` 启动完整开发环境 +- api-dev 和 web-dev 服务均配置热重载,本地修改代码后无需重启容器 +- 先检查 `docker ps` 确认服务状态,使用 `docker logs api-dev --tail 100` 查看日志 + +### 常用命令 + +#### 项目启动和停止 +```bash +# 启动所有服务 +make start +# 或 +docker compose up -d + +# 停止所有服务 +make stop +# 或 +docker compose down + +# 查看日志 +make logs +# 或 +docker logs --tail=50 api-dev +``` + +#### 后端开发(在 Docker 容器内) +```bash +# 代码检查和格式化 +make lint # 检查代码规范 +make format # 格式化代码 +make format_diff # 查看格式化差异 + +# 运行测试 +make router-tests # 运行 API 路由测试 + +# 直接在容器内执行命令 +docker compose exec api uv run python your_script.py +``` + +#### 前端开发 +```bash +# 在 web 目录下执行 +pnpm run dev # 开发模式(已通过 Docker 配置) +pnpm run build # 构建生产版本 +pnpm run lint # ESLint 检查 +pnpm run format # Prettier 格式化 +``` + +#### 文档开发 +```bash +# 在 docs 目录下执行 +pnpm run docs:dev # 开发文档 +pnpm run docs:build # 构建文档 +``` + +## 项目架构 + +### 技术栈 +- **后端**: FastAPI + Python 3.11+,使用 uv 管理依赖 +- **前端**: Vue 3.5 + Vite 7 + Ant Design Vue + Pinia +- **AI框架**: LangChain v1 + LangGraph v1 + LightRAG +- **数据库**: Neo4j (图数据库) + Milvus (向量数据库) + MinIO (对象存储) +- **容器化**: Docker Compose 多服务编排 + +### 核心目录结构 +``` +├── server/ # FastAPI 后端服务 +├── web/ # Vue.js 前端应用 +├── src/ # 核心业务逻辑代码 +├── docs/ # 文档中心 (VitePress) +├── test/ # 测试代码 +├── docker/ # Docker 配置文件 +├── scripts/ # 脚本工具 +├── saves/ # 数据保存目录 +├── models/ # 模型文件目录 +└── docker-compose.yml +``` + +### 主要服务 +- `api-dev`: FastAPI 后端服务 (端口 5050) +- `web-dev`: Vue.js 前端服务 (端口 5173) +- `graph`: Neo4j 图数据库 (端口 7474, 7687) +- `milvus`: Milvus 向量数据库 (端口 19530) +- `minio`: MinIO 对象存储 (端口 9000, 9001) +- `mineru`: MinerU 文档解析服务 (可选) +- `paddlex`: PaddleX OCR 服务 (可选) + +## 开发规范 + +### 前端开发规范 +- **API 接口**: 所有 API 接口定义在 `web/src/apis` 下,继承自 `apiGet/apiPost/apiRequest` +- **图标**: 从 `@ant-design/icons-vue` 或 `lucide-vue-next` 选取 +- **样式**: 使用 Less,优先采用 `web/src/assets/css/base.css` 中的颜色 +- **UI风格**: 简洁一致,避免悬停位移、过度阴影和渐变色 +- **组件库**: 基于 Ant Design Vue,保持组件一致性 + +### 后端开发规范 +- **包管理**: 使用 uv 管理依赖,调试时使用 `uv run` +- **代码规范**: 符合 Pythonic 风格,支持 Python 3.12+ 语法 +- **代码质量**: 使用 `make lint` 检查,`make format` 格式化 +- **API设计**: 遵循 RESTful 规范,使用 Pydantic 进行数据验证 + +### 通用开发规范 +- **文档更新**: 代码更新后同步更新 `docs/latest` 中的相关文档 +- **测试**: 测试脚本放在 `test/` 目录,从 Docker 容器中运行 +- **环境变量**: 配置通过 `.env` 文件管理 +- **数据安全**: 敏感数据不提交到版本控制 + +## 特色功能模块 + +### 智能体系统 +- 基于 LangGraph v1 的智能体框架 +- 支持多代理协作和工具调用 +- 提供完整的智能体开发套件 + +### 知识管理 +- 多模态文档解析 (PDF、Word、图片等) +- 知识图谱自动构建和可视化 +- 向量化存储和检索 + +### 数据处理 +- MinerU 文档解析集成 +- PaddleX OCR 文字识别 +- 支持多种模型提供商 (OpenAI、DeepSeek、阿里云等) + +## 调试和故障排除 + +### 常见问题排查 +1. **服务启动失败**: 检查端口占用和 Docker 服务状态 +2. **API 连接问题**: 确认 `VITE_API_URL` 环境变量配置 +3. **模型加载问题**: 检查 `models/` 目录和权限设置 +4. **数据库连接**: 确认 Neo4j 和 Milvus 服务健康状态 + +### 日志查看 +```bash +# 查看各服务日志 +docker logs api-dev --tail 100 +docker logs web-dev --tail 50 +docker logs graph --tail 50 +docker logs milvus --tail 50 +``` + +### 性能监控 +- 使用健康检查端点 `/api/system/health` +- 监控 GPU 使用情况 (如果使用 GPU 服务) +- 检查内存和磁盘使用情况 + You MUST read `./AGENTS.md` \ No newline at end of file diff --git a/server/routers/chat_router.py b/server/routers/chat_router.py index e2a3f54d..11154103 100644 --- a/server/routers/chat_router.py +++ b/server/routers/chat_router.py @@ -180,31 +180,41 @@ def _serialize_attachment(record: dict) -> dict: } -async def save_partial_message(conv_mgr, thread_id, full_msg, error_type="interrupted"): +async def save_partial_message(conv_mgr, thread_id, full_msg=None, error_message=None, error_type="interrupted"): """ - 保存部分生成的AI消息到数据库 - 当streaming被中断时,用于保存已生成的部分内容 - """ - if not full_msg: - return None + 统一保存AI消息到数据库的函数 + Args: + conv_mgr: 对话管理器 + thread_id: 线程ID + full_msg: 完整的AI消息对象(可选) + error_message: 纯错误消息文本(当full_msg为空时使用) + error_type: 错误类型标识 + """ try: - msg_dict = full_msg.model_dump() if hasattr(full_msg, "model_dump") else {} - content = full_msg.content if hasattr(full_msg, "content") else str(full_msg) + if full_msg: + # 保存部分生成的AI消息 + msg_dict = full_msg.model_dump() if hasattr(full_msg, "model_dump") else {} + content = full_msg.content if hasattr(full_msg, "content") else str(full_msg) + extra_metadata = msg_dict | {"error_type": error_type} + else: + # 保存纯错误消息 + content = error_message or f"发生错误: {error_type}" + extra_metadata = {"error_type": error_type, "is_error": True} saved_msg = conv_mgr.add_message_by_thread_id( thread_id=thread_id, role="assistant", content=content, message_type="text", - extra_metadata=msg_dict | {"error_type": error_type}, + extra_metadata=extra_metadata, ) - logger.info(f"Saved partial message due to {error_type}: {saved_msg.id}") + logger.info(f"Saved message due to {error_type}: {saved_msg.id}") return saved_msg except Exception as e: - logger.error(f"Error saving partial message: {e}") + logger.error(f"Error saving message: {e}") logger.error(traceback.format_exc()) return None @@ -367,7 +377,7 @@ async def get_agent(current_user: User = Depends(get_required_user)): "examples": agent_info.get("examples", []), "configurable_items": agent_info.get("configurable_items", []), "has_checkpointer": agent_info.get("has_checkpointer", False), - "capabilities": agent_info.get("capabilities", []) # 智能体能力列表 + "capabilities": agent_info.get("capabilities", []), # 智能体能力列表 } for agent_info in agents_info ] @@ -513,16 +523,19 @@ async def chat_agent( # 客户端主动中断连接,检查中断并保存已生成的部分内容 logger.warning(f"Client disconnected, cancelling stream: {e}") - - # 如果有手动维护的 full_msg,直接保存到数据库 - if full_msg: - # 创建新的 db session,因为原 session 可能已关闭 - new_db = db_manager.get_session() - try: - new_conv_manager = ConversationManager(new_db) - await save_partial_message(new_conv_manager, thread_id, full_msg, "interrupted") - finally: - new_db.close() + # 保存中断消息到数据库 + new_db = db_manager.get_session() + try: + new_conv_manager = ConversationManager(new_db) + await save_partial_message( + new_conv_manager, + thread_id, + full_msg=full_msg, + error_message="对话已中断" if not full_msg else None, + error_type="interrupted", + ) + finally: + new_db.close() # 通知前端中断(可能发送不到,但用于一致性) yield make_chunk(status="interrupted", message="对话已中断", meta=meta) @@ -530,16 +543,19 @@ async def chat_agent( except Exception as e: logger.error(f"Error streaming messages: {e}, {traceback.format_exc()}") - - # 如果有手动维护的 full_msg,直接保存到数据库 - if full_msg: - # 创建新的 db session,因为原 session 可能已关闭 - new_db = db_manager.get_session() - try: - new_conv_manager = ConversationManager(new_db) - await save_partial_message(new_conv_manager, thread_id, full_msg, "unexpected_error") - finally: - new_db.close() + # 保存错误消息到数据库 + new_db = db_manager.get_session() + try: + new_conv_manager = ConversationManager(new_db) + await save_partial_message( + new_conv_manager, + thread_id, + full_msg=full_msg, + error_message=f"Error streaming messages: {e}" if not full_msg else None, + error_type="unexpected_error", + ) + finally: + new_db.close() yield make_chunk(message=f"Error streaming messages: {e}", status="error") @@ -644,28 +660,61 @@ async def resume_agent_chat( resume_command, context=context, config={"configurable": input_context}, stream_mode="messages" ) - async for msg, metadata in stream_source: - # 确保msg有正确的ID结构 - msg_dict = msg.model_dump() - if "id" not in msg_dict: - msg_dict["id"] = str(uuid.uuid4()) + try: + async for msg, metadata in stream_source: + # 确保msg有正确的ID结构 + msg_dict = msg.model_dump() + if "id" not in msg_dict: + msg_dict["id"] = str(uuid.uuid4()) - yield make_resume_chunk( - content=getattr(msg, "content", ""), msg=msg_dict, metadata=metadata, status="loading" + yield make_resume_chunk( + content=getattr(msg, "content", ""), msg=msg_dict, metadata=metadata, status="loading" + ) + + meta["time_cost"] = asyncio.get_event_loop().time() - start_time + yield make_resume_chunk(status="finished", meta=meta) + + # 保存消息到数据库 + langgraph_config = {"configurable": input_context} + conv_manager = ConversationManager(db) + await save_messages_from_langgraph_state( + agent_instance=agent, + thread_id=thread_id, + conv_mgr=conv_manager, + config_dict=langgraph_config, ) - meta["time_cost"] = asyncio.get_event_loop().time() - start_time - yield make_resume_chunk(status="finished", meta=meta) + except (asyncio.CancelledError, ConnectionError) as e: + # 客户端主动中断连接 + logger.warning(f"Client disconnected during resume: {e}") - # 保存消息到数据库 - langgraph_config = {"configurable": input_context} - conv_manager = ConversationManager(db) - await save_messages_from_langgraph_state( - agent_instance=agent, - thread_id=thread_id, - conv_mgr=conv_manager, - config_dict=langgraph_config, - ) + # 保存中断消息到数据库 + new_db = db_manager.get_session() + try: + new_conv_manager = ConversationManager(new_db) + await save_partial_message( + new_conv_manager, thread_id, error_message="对话恢复已中断", error_type="resume_interrupted" + ) + finally: + new_db.close() + + yield make_resume_chunk(status="interrupted", message="对话恢复已中断", meta=meta) + + except Exception as e: + # 处理其他异常 + logger.error(f"Error during resume: {e}, {traceback.format_exc()}") + + # 保存错误消息到数据库 + new_db = db_manager.get_session() + try: + new_conv_manager = ConversationManager(new_db) + await save_partial_message( + new_conv_manager, thread_id, error_message=f"Error during resume: {e}", error_type="resume_error" + ) + finally: + new_db.close() + + yield make_resume_chunk(message=f"Error during resume: {e}", status="error") return StreamingResponse(stream_resume(), media_type="application/json") diff --git a/server/routers/knowledge_router.py b/server/routers/knowledge_router.py index 285bb4fd..06e93dcc 100644 --- a/server/routers/knowledge_router.py +++ b/server/routers/knowledge_router.py @@ -247,12 +247,14 @@ async def add_documents( error_type = "timeout" if isinstance(doc_error, TimeoutError) else "processing_error" error_msg = "处理超时" if isinstance(doc_error, TimeoutError) else "处理失败" - processed_items.append({ - "item": item, - "status": "failed", - "error": f"{error_msg}: {str(doc_error)}", - "error_type": error_type - }) + processed_items.append( + { + "item": item, + "status": "failed", + "error": f"{error_msg}: {str(doc_error)}", + "error_type": error_type, + } + ) except asyncio.CancelledError: await context.set_progress(100.0, "任务已取消") @@ -262,13 +264,15 @@ async def add_documents( logger.exception(f"Task processing failed: {task_error}") await context.set_progress(100.0, f"任务处理失败: {str(task_error)}") # 将所有未处理的文档标记为失败 - for item in items[len(processed_items):]: - processed_items.append({ - "item": item, - "status": "failed", - "error": f"任务失败: {str(task_error)}", - "error_type": "task_failed" - }) + for item in items[len(processed_items) :]: + processed_items.append( + { + "item": item, + "status": "failed", + "error": f"任务失败: {str(task_error)}", + "error_type": "task_failed", + } + ) raise item_type = "URL" if content_type == "url" else "文件" @@ -600,8 +604,7 @@ async def get_knowledge_base_query_params(db_id: str, current_user: User = Depen "type": "select", "default": reranker_config.get("model", ""), "options": [ - {"label": info.name, "value": model_id} - for model_id, info in config.reranker_names.items() + {"label": info.name, "value": model_id} for model_id, info in config.reranker_names.items() ], "description": "覆盖默认配置,选择用于本次查询的重排序模型", } @@ -675,8 +678,7 @@ async def get_knowledge_base_query_params(db_id: str, current_user: User = Depen "type": "select", "default": reranker_config.get("model", ""), "options": [ - {"label": info.name, "value": model_id} - for model_id, info in config.reranker_names.items() + {"label": info.name, "value": model_id} for model_id, info in config.reranker_names.items() ], "description": "覆盖默认配置,选择用于本次查询的重排序模型", } diff --git a/server/routers/system_router.py b/server/routers/system_router.py index 658d4b12..3b73bc43 100644 --- a/server/routers/system_router.py +++ b/server/routers/system_router.py @@ -51,8 +51,6 @@ async def update_config_batch(items: dict = Body(...), current_user: User = Depe return config.dump_config() - - @system.get("/logs") def get_system_logs(current_user: User = Depends(get_admin_user)): """获取系统日志""" diff --git a/src/agents/common/base.py b/src/agents/common/base.py index 1e889b3a..b7038523 100644 --- a/src/agents/common/base.py +++ b/src/agents/common/base.py @@ -2,6 +2,7 @@ from __future__ import annotations import importlib.util import os +import tomllib as tomli from abc import abstractmethod from pathlib import Path @@ -9,8 +10,6 @@ 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 @@ -163,7 +162,6 @@ class BaseAgent: """获取异步存储实例""" 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: diff --git a/src/agents/common/middlewares/__init__.py b/src/agents/common/middlewares/__init__.py index 5436dbcc..51c3cf2f 100644 --- a/src/agents/common/middlewares/__init__.py +++ b/src/agents/common/middlewares/__init__.py @@ -1,6 +1,6 @@ +from .attachment_middleware import inject_attachment_context from .context_middlewares import context_aware_prompt, context_based_model from .dynamic_tool_middleware import DynamicToolMiddleware -from .attachment_middleware import inject_attachment_context __all__ = [ "DynamicToolMiddleware", diff --git a/src/agents/common/middlewares/attachment_middleware.py b/src/agents/common/middlewares/attachment_middleware.py index de150860..520a7a0b 100644 --- a/src/agents/common/middlewares/attachment_middleware.py +++ b/src/agents/common/middlewares/attachment_middleware.py @@ -40,8 +40,7 @@ def _build_attachment_prompt(attachments: Sequence[dict]) -> str | None: return None instructions = ( - "以下为用户提供的附件内容,请综合这些文件与用户的新问题进行回答。" - "如附件与问题无关,可忽略附件内容:\n\n" + "以下为用户提供的附件内容,请综合这些文件与用户的新问题进行回答。如附件与问题无关,可忽略附件内容:\n\n" ) return instructions + "\n\n".join(chunks) diff --git a/src/agents/common/middlewares/context_middlewares.py b/src/agents/common/middlewares/context_middlewares.py index 056ee996..c9623601 100644 --- a/src/agents/common/middlewares/context_middlewares.py +++ b/src/agents/common/middlewares/context_middlewares.py @@ -14,9 +14,7 @@ def context_aware_prompt(request: ModelRequest) -> str: @wrap_model_call -async def context_based_model( - request: ModelRequest, handler: Callable[[ModelRequest], ModelResponse] -) -> ModelResponse: +async def context_based_model(request: ModelRequest, handler: Callable[[ModelRequest], ModelResponse]) -> ModelResponse: """从 runtime context 动态选择模型""" model_spec = request.runtime.context.model model = load_chat_model(model_spec) diff --git a/src/agents/common/subagents/calc_agent.py b/src/agents/common/subagents/calc_agent.py index 131f636a..7c9a11b5 100644 --- a/src/agents/common/subagents/calc_agent.py +++ b/src/agents/common/subagents/calc_agent.py @@ -11,6 +11,7 @@ calc_agent = create_agent( system_prompt="你可以使用计算器工具,处理各种数学计算任务。", ) + @tool(name_or_callable="calc_agent_tool", description="进行计算任务,输入是数学表达式或描述,输出计算结果。") async def calc_agent_tool(description: str) -> str: """CalcAgent 工具 - 使用子智能体 CalcAgent 进行计算任务""" diff --git a/src/agents/common/toolkits/mysql/tools.py b/src/agents/common/toolkits/mysql/tools.py index cf1816f4..d41baf23 100644 --- a/src/agents/common/toolkits/mysql/tools.py +++ b/src/agents/common/toolkits/mysql/tools.py @@ -88,7 +88,7 @@ def mysql_list_tables() -> str: all_table_names = "\n".join(table_names) result = f"数据库中的表:\n{all_table_names}" - if db_note := conn_manager.config.get('description'): + if db_note := conn_manager.config.get("description"): result = f"数据库说明: {db_note}\n\n" + result logger.info(f"Retrieved {len(table_names)} tables from database") return result diff --git a/src/agents/common/tools.py b/src/agents/common/tools.py index 8c5f49b4..79bfb52b 100644 --- a/src/agents/common/tools.py +++ b/src/agents/common/tools.py @@ -11,10 +11,10 @@ from pydantic import BaseModel, Field from src import config, graph_base, knowledge_base from src.utils import logger - search = TavilySearch(max_results=10) search.metadata = {"name": "Tavily 网页搜索"} + @tool(name_or_callable="计算器", description="可以对给定的2个数字选择进行 add, subtract, multiply, divide 运算") def calculator(a: float, b: float, operation: str) -> float: try: @@ -38,7 +38,7 @@ def calculator(a: float, b: float, operation: str) -> float: @tool(name_or_callable="人工审批工具(Debug)", description="请求人工审批工具,用于在执行重要操作前获得人类确认。") def get_approved_user_goal( operation_description: str, -)->dict: +) -> dict: """ 请求人工审批,在执行重要操作前获得人类确认。 @@ -78,6 +78,7 @@ KG_QUERY_DESCRIPTION = """ 关键词(query),使用可能帮助回答这个问题的关键词进行查询,不要直接使用用户的原始输入去查询。 """ + @tool(name_or_callable="查询知识图谱", description=KG_QUERY_DESCRIPTION) def query_knowledge_graph(query: Annotated[str, "The keyword to query knowledge graph."]) -> Any: """使用这个工具可以查询知识图谱中包含的三元组信息。关键词(query),使用可能帮助回答这个问题的关键词进行查询,不要直接使用用户的原始输入去查询。""" diff --git a/src/agents/deep_agent/context.py b/src/agents/deep_agent/context.py index bc7df151..9adc850a 100644 --- a/src/agents/deep_agent/context.py +++ b/src/agents/deep_agent/context.py @@ -4,7 +4,6 @@ from dataclasses import field from src.agents.common.context import BaseContext - DEEP_PROMPT = """你是一个能够处理复杂、多步骤任务的深度分析代理。 你拥有以下资源: diff --git a/src/agents/deep_agent/graph.py b/src/agents/deep_agent/graph.py index 586bb014..c6a23467 100644 --- a/src/agents/deep_agent/graph.py +++ b/src/agents/deep_agent/graph.py @@ -1,17 +1,20 @@ """Deep Agent - 基于create_deep_agent的深度分析智能体""" + import os from typing import Literal + from deepagents import create_deep_agent +from tavily import TavilyClient from src.agents.common import BaseAgent, load_chat_model -from src.agents.common.tools import search from src.agents.common.middlewares import ( context_aware_prompt, context_based_model, inject_attachment_context, ) +from src.agents.common.tools import search + from .context import DeepContext -from tavily import TavilyClient # 最佳实践是初始化客户端一次并复用它。 tavily_client = TavilyClient(api_key=os.environ["TAVILY_API_KEY"]) @@ -169,8 +172,8 @@ research_instructions = """你是一位专家级研究员。你的工作是进 使用此工具对给定的查询进行网络搜索。你可以指定结果数量、主题以及是否包含原始内容。 """ -class DeepAgent(BaseAgent): +class DeepAgent(BaseAgent): name = "深度分析智能体" description = "具备规划、深度分析和子智能体协作能力的智能体,可以处理复杂的多步骤任务" context_schema = DeepContext diff --git a/src/agents/reporter/graph.py b/src/agents/reporter/graph.py index 474151a0..a455adb4 100644 --- a/src/agents/reporter/graph.py +++ b/src/agents/reporter/graph.py @@ -1,4 +1,3 @@ - from langchain.agents import create_agent from src import config diff --git a/src/knowledge/implementations/chroma.py b/src/knowledge/implementations/chroma.py index 736a8404..10359fc8 100644 --- a/src/knowledge/implementations/chroma.py +++ b/src/knowledge/implementations/chroma.py @@ -323,9 +323,7 @@ class ChromaKB(KnowledgeBase): try: rerank_start = time.time() documents_text = [chunk["content"] for chunk in retrieved_chunks] - rerank_scores = await reranker.acompute_score( - [query_text, documents_text], normalize=True - ) + rerank_scores = await reranker.acompute_score([query_text, documents_text], normalize=True) for chunk, rerank_score in zip(retrieved_chunks, rerank_scores): chunk["rerank_score"] = float(rerank_score) diff --git a/src/knowledge/implementations/milvus.py b/src/knowledge/implementations/milvus.py index 5f485f8b..fc0fed4c 100644 --- a/src/knowledge/implementations/milvus.py +++ b/src/knowledge/implementations/milvus.py @@ -369,9 +369,7 @@ class MilvusKB(KnowledgeBase): try: rerank_start = time.time() documents_text = [chunk["content"] for chunk in retrieved_chunks] - rerank_scores = await reranker.acompute_score( - [query_text, documents_text], normalize=True - ) + rerank_scores = await reranker.acompute_score([query_text, documents_text], normalize=True) for chunk, rerank_score in zip(retrieved_chunks, rerank_scores): chunk["rerank_score"] = float(rerank_score) diff --git a/test/api/test_knowledge_router.py b/test/api/test_knowledge_router.py index 57e7460a..25329c9a 100644 --- a/test/api/test_knowledge_router.py +++ b/test/api/test_knowledge_router.py @@ -85,9 +85,7 @@ async def test_admin_can_create_vector_db_with_reranker(test_client, admin_heade assert reranker_config.get("enabled") is True assert reranker_config.get("model") == "siliconflow/BAAI/bge-reranker-v2-m3" - params_response = await test_client.get( - f"/api/knowledge/databases/{db_id}/query-params", headers=admin_headers - ) + params_response = await test_client.get(f"/api/knowledge/databases/{db_id}/query-params", headers=admin_headers) assert params_response.status_code == 200, params_response.text params_payload = params_response.json() diff --git a/test/api/test_settings_router.py b/test/api/test_settings_router.py index 4a41fe1f..e249d56c 100644 --- a/test/api/test_settings_router.py +++ b/test/api/test_settings_router.py @@ -13,9 +13,7 @@ async def test_reranker_list_requires_admin(test_client, standard_user): public_response = await test_client.get("/api/settings/rerankers") assert public_response.status_code == 401 - forbidden_response = await test_client.get( - "/api/settings/rerankers", headers=standard_user["headers"] - ) + forbidden_response = await test_client.get("/api/settings/rerankers", headers=standard_user["headers"]) assert forbidden_response.status_code == 403 diff --git a/web/src/components/AgentChatComponent.vue b/web/src/components/AgentChatComponent.vue index 4010e9d3..e4369924 100644 --- a/web/src/components/AgentChatComponent.vue +++ b/web/src/components/AgentChatComponent.vue @@ -436,6 +436,7 @@ const cleanupThreadState = (threadId) => { // ==================== STREAM HANDLING LOGIC ==================== const resetOnGoingConv = (threadId = null, preserveMessages = false) => { + console.log('🔄 [RESET] Resetting on going conversation:', threadId, preserveMessages); if (threadId) { // 清理指定线程的状态 const threadState = getThreadState(threadId); @@ -449,8 +450,8 @@ const resetOnGoingConv = (threadId = null, preserveMessages = false) => { // 延迟清空消息,给历史记录加载足够时间 setTimeout(() => { if (threadState.onGoingConv) { - threadState.onGoingConv = createOnGoingConvState(); - } + threadState.onGoingConv = createOnGoingConvState(); + } }, 100); } else { threadState.onGoingConv = createOnGoingConvState(); @@ -469,8 +470,8 @@ const resetOnGoingConv = (threadId = null, preserveMessages = false) => { if (preserveMessages) { setTimeout(() => { if (threadState.onGoingConv) { - threadState.onGoingConv = createOnGoingConvState(); - } + threadState.onGoingConv = createOnGoingConvState(); + } }, 100); } else { threadState.onGoingConv = createOnGoingConvState(); @@ -547,8 +548,8 @@ const _processStreamChunk = (chunk, threadId) => { } fetchThreadMessages({ agentId: currentAgentId.value, threadId: threadId }) .finally(() => { - resetOnGoingConv(threadId, true); - }); + resetOnGoingConv(threadId, true); + }); return true; } @@ -646,11 +647,17 @@ const updateThread = async (threadId, title) => { }; // 获取线程消息 -const fetchThreadMessages = async ({ agentId, threadId }) => { +const fetchThreadMessages = async ({ agentId, threadId, delay = 0 }) => { if (!threadId || !agentId) return; + // 如果指定了延迟,等待指定时间(用于确保后端数据库事务提交) + if (delay > 0) { + await new Promise(resolve => setTimeout(resolve, delay)); + } + try { const response = await agentApi.getAgentHistory(agentId, threadId); + console.log('🔄 [FETCH] Thread messages:', response); threadMessages.value[threadId] = response.history || []; } catch (error) { handleChatError(error, 'load'); @@ -963,7 +970,7 @@ const handleSendOrStop = async () => { // 中断后刷新消息历史,确保显示最新的状态 try { - await fetchThreadMessages({ agentId: currentAgentId.value, threadId: threadId }); + await fetchThreadMessages({ agentId: currentAgentId.value, threadId: threadId, delay: 100 }); message.info('已中断对话生成'); } catch (error) { console.error('刷新消息历史失败:', error); diff --git a/web/src/components/ModelProvidersComponent.vue b/web/src/components/ModelProvidersComponent.vue index 3ce3e770..4d919055 100644 --- a/web/src/components/ModelProvidersComponent.vue +++ b/web/src/components/ModelProvidersComponent.vue @@ -118,8 +118,11 @@