diff --git a/pyproject.toml b/pyproject.toml index 38deff1b..0d3d7ddb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,6 +6,8 @@ readme = "README.md" requires-python = ">=3.11" dependencies = [ "asyncpg>=0.30.0", + "aiosqlite>=0.20.0", + "sqlalchemy[asyncio]>=2.0.0", "langchain>=1.0.2", "chromadb>=1.3", "colorlog>=6.9.0", diff --git a/server/routers/chat_router.py b/server/routers/chat_router.py index 8dbe0b01..af165897 100644 --- a/server/routers/chat_router.py +++ b/server/routers/chat_router.py @@ -8,7 +8,8 @@ from fastapi.responses import StreamingResponse from langchain.messages import AIMessageChunk, HumanMessage from langgraph.types import Command from pydantic import BaseModel -from sqlalchemy.orm import Session +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy import select from src.storage.db.models import User, MessageFeedback, Message, Conversation from src.storage.conversation import ConversationManager @@ -193,8 +194,8 @@ def _save_tool_message(conv_mgr, msg_dict): logger.warning(f"Tool call {tool_call_id} not found for update") -def _require_user_conversation(conv_mgr: ConversationManager, thread_id: str, user_id: str) -> Conversation: - conversation = conv_mgr.get_conversation_by_thread_id(thread_id) +async def _require_user_conversation(conv_mgr: ConversationManager, thread_id: str, user_id: str) -> Conversation: + conversation = await conv_mgr.get_conversation_by_thread_id(thread_id) if not conversation or conversation.user_id != str(user_id) or conversation.status == "deleted": raise HTTPException(status_code=404, detail="对话线程不存在") return conversation @@ -455,7 +456,7 @@ async def chat_agent( meta: dict = Body({}), image_content: str | None = Body(None), current_user: User = Depends(get_required_user), - db: Session = Depends(get_db), + db: AsyncSession = Depends(get_db), ): """使用特定智能体进行对话(需要登录)""" start_time = asyncio.get_event_loop().time() @@ -556,7 +557,7 @@ async def chat_agent( # Save user message try: - conv_manager.add_message_by_thread_id( + await conv_manager.add_message_by_thread_id( thread_id=thread_id, role="user", content=query, @@ -569,7 +570,7 @@ async def chat_agent( try: assert thread_id, "thread_id is required" - attachments = conv_manager.get_attachments_by_thread_id(thread_id) + attachments = await conv_manager.get_attachments_by_thread_id(thread_id) input_context["attachments"] = attachments logger.debug(f"Loaded {len(attachments)} attachments for thread_id={thread_id}") except Exception as e: @@ -648,8 +649,7 @@ async def chat_agent( logger.warning(f"Client disconnected, cancelling stream: {e}") # 保存中断消息到数据库 - new_db = db_manager.get_session() - try: + async with db_manager.get_async_session_context() as new_db: new_conv_manager = ConversationManager(new_db) await save_partial_message( new_conv_manager, @@ -658,8 +658,6 @@ async def chat_agent( error_message="对话已中断" if not full_msg else None, error_type="interrupted", ) - finally: - new_db.close() # 通知前端中断(可能发送不到,但用于一致性) yield make_chunk(status="interrupted", message="对话已中断", meta=meta) @@ -671,8 +669,7 @@ async def chat_agent( error_type = "unexpected_error" # 保存错误消息到数据库 - new_db = db_manager.get_session() - try: + async with db_manager.get_async_session_context() as new_db: new_conv_manager = ConversationManager(new_db) await save_partial_message( new_conv_manager, @@ -681,8 +678,6 @@ async def chat_agent( error_message=error_msg, error_type=error_type, ) - finally: - new_db.close() yield make_chunk( status="error", @@ -739,7 +734,7 @@ async def resume_agent_chat( thread_id: str = Body(...), approved: bool = Body(...), current_user: User = Depends(get_required_user), - db: Session = Depends(get_db), + db: AsyncSession = Depends(get_db), ): """恢复被人工审批中断的对话(需要登录)""" start_time = asyncio.get_event_loop().time() @@ -874,7 +869,7 @@ async def save_agent_config(agent_id: str, config: dict = Body(...), current_use @chat.get("/agent/{agent_id}/history") async def get_agent_history( - agent_id: str, thread_id: str, current_user: User = Depends(get_required_user), db: Session = Depends(get_db) + agent_id: str, thread_id: str, current_user: User = Depends(get_required_user), db: AsyncSession = Depends(get_db) ): """获取智能体历史消息(需要登录)- NEW STORAGE ONLY""" try: @@ -884,7 +879,7 @@ async def get_agent_history( # Use new storage system ONLY conv_manager = ConversationManager(db) - messages = conv_manager.get_messages_by_thread_id(thread_id) + messages = await conv_manager.get_messages_by_thread_id(thread_id) # Convert to frontend-compatible format history = [] @@ -934,14 +929,14 @@ async def get_agent_state( agent_id: str, thread_id: str, current_user: User = Depends(get_required_user), - db: Session = Depends(get_db), + db: AsyncSession = Depends(get_db), ): try: if not agent_manager.get_agent(agent_id): raise HTTPException(status_code=404, detail=f"智能体 {agent_id} 不存在") conv_manager = ConversationManager(db) - _require_user_conversation(conv_manager, thread_id, str(current_user.id)) + await _require_user_conversation(conv_manager, thread_id, str(current_user.id)) agent = agent_manager.get_agent(agent_id) graph = await agent.get_graph() @@ -1019,7 +1014,7 @@ class AttachmentListResponse(BaseModel): @chat.post("/thread", response_model=ThreadResponse) async def create_thread( - thread: ThreadCreate, db: Session = Depends(get_db), current_user: User = Depends(get_required_user) + thread: ThreadCreate, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_required_user) ): """创建新对话线程 (使用新存储系统)""" thread_id = str(uuid.uuid4()) @@ -1027,7 +1022,7 @@ async def create_thread( # Create conversation using new storage system conv_manager = ConversationManager(db) - conversation = conv_manager.create_conversation( + conversation = await conv_manager.create_conversation( user_id=str(current_user.id), agent_id=thread.agent_id, title=thread.title or "新的对话", @@ -1048,7 +1043,7 @@ async def create_thread( @chat.get("/threads", response_model=list[ThreadResponse]) -async def list_threads(agent_id: str, db: Session = Depends(get_db), current_user: User = Depends(get_required_user)): +async def list_threads(agent_id: str, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_required_user)): """获取用户的所有对话线程 (使用新存储系统)""" assert agent_id, "agent_id 不能为空" @@ -1056,7 +1051,7 @@ async def list_threads(agent_id: str, db: Session = Depends(get_db), current_use # Use new storage system conv_manager = ConversationManager(db) - conversations = conv_manager.list_conversations( + conversations = await conv_manager.list_conversations( user_id=str(current_user.id), agent_id=agent_id, status="active", @@ -1076,17 +1071,17 @@ async def list_threads(agent_id: str, db: Session = Depends(get_db), current_use @chat.delete("/thread/{thread_id}") -async def delete_thread(thread_id: str, db: Session = Depends(get_db), current_user: User = Depends(get_required_user)): +async def delete_thread(thread_id: str, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_required_user)): """删除对话线程 (使用新存储系统)""" # Use new storage system conv_manager = ConversationManager(db) - conversation = conv_manager.get_conversation_by_thread_id(thread_id) + conversation = await conv_manager.get_conversation_by_thread_id(thread_id) if not conversation or conversation.user_id != str(current_user.id): raise HTTPException(status_code=404, detail="对话线程不存在") # Soft delete - success = conv_manager.delete_conversation(thread_id, soft_delete=True) + success = await conv_manager.delete_conversation(thread_id, soft_delete=True) if not success: raise HTTPException(status_code=500, detail="删除失败") @@ -1102,19 +1097,19 @@ class ThreadUpdate(BaseModel): async def update_thread( thread_id: str, thread_update: ThreadUpdate, - db: Session = Depends(get_db), + db: AsyncSession = Depends(get_db), current_user: User = Depends(get_required_user), ): """更新对话线程信息 (使用新存储系统)""" # Use new storage system conv_manager = ConversationManager(db) - conversation = conv_manager.get_conversation_by_thread_id(thread_id) + conversation = await conv_manager.get_conversation_by_thread_id(thread_id) if not conversation or conversation.user_id != str(current_user.id) or conversation.status == "deleted": raise HTTPException(status_code=404, detail="对话线程不存在") # Update conversation - updated_conv = conv_manager.update_conversation( + updated_conv = await conv_manager.update_conversation( thread_id=thread_id, title=thread_update.title, ) @@ -1136,12 +1131,12 @@ async def update_thread( async def upload_thread_attachment( thread_id: str, file: UploadFile = File(...), - db: Session = Depends(get_db), + db: AsyncSession = Depends(get_db), current_user: User = Depends(get_required_user), ): """上传并解析附件为 Markdown,附加到指定对话线程。""" conv_manager = ConversationManager(db) - conversation = _require_user_conversation(conv_manager, thread_id, str(current_user.id)) + conversation = await _require_user_conversation(conv_manager, thread_id, str(current_user.id)) try: conversion = await convert_upload_to_markdown(file) @@ -1161,7 +1156,7 @@ async def upload_thread_attachment( "uploaded_at": utc_isoformat(), "truncated": conversion.truncated, } - conv_manager.add_attachment(conversation.id, attachment_record) + await conv_manager.add_attachment(conversation.id, attachment_record) return _serialize_attachment(attachment_record) @@ -1169,13 +1164,13 @@ async def upload_thread_attachment( @chat.get("/thread/{thread_id}/attachments", response_model=AttachmentListResponse) async def list_thread_attachments( thread_id: str, - db: Session = Depends(get_db), + db: AsyncSession = Depends(get_db), current_user: User = Depends(get_required_user), ): """列出当前对话线程的所有附件元信息。""" conv_manager = ConversationManager(db) - conversation = _require_user_conversation(conv_manager, thread_id, str(current_user.id)) - attachments = conv_manager.get_attachments(conversation.id) + conversation = await _require_user_conversation(conv_manager, thread_id, str(current_user.id)) + attachments = await conv_manager.get_attachments(conversation.id) return { "attachments": [_serialize_attachment(item) for item in attachments], "limits": { @@ -1189,13 +1184,13 @@ async def list_thread_attachments( async def delete_thread_attachment( thread_id: str, file_id: str, - db: Session = Depends(get_db), + db: AsyncSession = Depends(get_db), current_user: User = Depends(get_required_user), ): """移除指定附件。""" conv_manager = ConversationManager(db) - conversation = _require_user_conversation(conv_manager, thread_id, str(current_user.id)) - removed = conv_manager.remove_attachment(conversation.id, file_id) + conversation = await _require_user_conversation(conv_manager, thread_id, str(current_user.id)) + removed = await conv_manager.remove_attachment(conversation.id, file_id) if not removed: raise HTTPException(status_code=404, detail="附件不存在或已被删除") return {"message": "附件已删除"} @@ -1223,7 +1218,7 @@ class MessageFeedbackResponse(BaseModel): async def submit_message_feedback( message_id: int, feedback_data: MessageFeedbackRequest, - db: Session = Depends(get_db), + db: AsyncSession = Depends(get_db), current_user: User = Depends(get_required_user), ): """Submit user feedback for a specific message""" @@ -1233,20 +1228,23 @@ async def submit_message_feedback( raise HTTPException(status_code=422, detail="Rating must be 'like' or 'dislike'") # Verify message exists and get conversation to check permissions - message = db.query(Message).filter_by(id=message_id).first() + message_result = await db.execute(select(Message).filter_by(id=message_id)) + message = message_result.scalar_one_or_none() if not message: raise HTTPException(status_code=404, detail="Message not found") # Verify user has access to this message (through conversation) - conversation = db.query(Conversation).filter_by(id=message.conversation_id).first() + conversation_result = await db.execute(select(Conversation).filter_by(id=message.conversation_id)) + conversation = conversation_result.scalar_one_or_none() if not conversation or conversation.user_id != str(current_user.id): raise HTTPException(status_code=403, detail="Access denied") # Check if feedback already exists (user can only submit once) - existing_feedback = ( - db.query(MessageFeedback).filter_by(message_id=message_id, user_id=str(current_user.id)).first() + existing_feedback_result = await db.execute( + select(MessageFeedback).filter_by(message_id=message_id, user_id=str(current_user.id)) ) + existing_feedback = existing_feedback_result.scalar_one_or_none() if existing_feedback: raise HTTPException(status_code=409, detail="Feedback already submitted for this message") @@ -1284,13 +1282,14 @@ async def submit_message_feedback( @chat.get("/message/{message_id}/feedback") async def get_message_feedback( message_id: int, - db: Session = Depends(get_db), + db: AsyncSession = Depends(get_db), current_user: User = Depends(get_required_user), ): """Get feedback status for a specific message (for current user)""" try: # Get user's feedback for this message - feedback = db.query(MessageFeedback).filter_by(message_id=message_id, user_id=str(current_user.id)).first() + feedback_result = await db.execute(select(MessageFeedback).filter_by(message_id=message_id, user_id=str(current_user.id))) + feedback = feedback_result.scalar_one_or_none() if not feedback: return {"has_feedback": False, "feedback": None} diff --git a/server/routers/dashboard_router.py b/server/routers/dashboard_router.py index e931bcbc..ae9613bc 100644 --- a/server/routers/dashboard_router.py +++ b/server/routers/dashboard_router.py @@ -9,8 +9,8 @@ from datetime import datetime, timedelta from fastapi import APIRouter, Depends, HTTPException from pydantic import BaseModel -from sqlalchemy import String, cast, distinct, func, or_ -from sqlalchemy.orm import Session +from sqlalchemy import String, cast, distinct, func, or_, select +from sqlalchemy.ext.asyncio import AsyncSession from server.routers.auth_router import get_admin_user from server.utils.auth_middleware import get_db @@ -110,7 +110,7 @@ async def get_all_conversations( status: str = "active", limit: int = 100, offset: int = 0, - db: Session = Depends(get_db), + db: AsyncSession = Depends(get_db), current_user: User = Depends(get_admin_user), ): """Get all conversations (Admin only)""" @@ -118,8 +118,9 @@ async def get_all_conversations( try: # Build query - query = db.query(Conversation, ConversationStats).outerjoin( - ConversationStats, Conversation.id == ConversationStats.conversation_id + query = ( + select(Conversation, ConversationStats) + .outerjoin(ConversationStats, Conversation.id == ConversationStats.conversation_id) ) # Apply filters @@ -133,7 +134,8 @@ async def get_all_conversations( # Order and paginate query = query.order_by(Conversation.updated_at.desc()).limit(limit).offset(offset) - results = query.all() + result = await db.execute(query) + results = result.all() return [ { @@ -157,7 +159,7 @@ async def get_all_conversations( @dashboard.get("/conversations/{thread_id}", response_model=ConversationDetailResponse) async def get_conversation_detail( thread_id: str, - db: Session = Depends(get_db), + db: AsyncSession = Depends(get_db), current_user: User = Depends(get_admin_user), ): """Get conversation detail (Admin only)""" @@ -225,7 +227,7 @@ async def get_conversation_detail( @dashboard.get("/stats/users", response_model=UserActivityStats) async def get_user_activity_stats( - db: Session = Depends(get_db), + db: AsyncSession = Depends(get_db), current_user: User = Depends(get_admin_user), ): """Get user activity statistics (Admin only)""" @@ -242,40 +244,38 @@ async def get_user_activity_stats( ) # 基础用户统计(排除已删除用户) - total_users = db.query(func.count(User.id)).filter(User.is_deleted == 0).scalar() or 0 + total_users_result = await db.execute(select(func.count(User.id)).filter(User.is_deleted == 0)) + total_users = total_users_result.scalar() or 0 # 不同时间段的活跃用户数(基于对话活动,排除已删除用户) - active_users_24h = ( - db.query(func.count(distinct(User.id))) + active_users_24h_result = await db.execute( + select(func.count(distinct(User.id))) .select_from(Conversation) .join(User, user_join_condition) .filter(Conversation.updated_at >= now - timedelta(days=1), User.is_deleted == 0) - .scalar() - or 0 ) + active_users_24h = active_users_24h_result.scalar() or 0 - active_users_30d = ( - db.query(func.count(distinct(User.id))) + active_users_30d_result = await db.execute( + select(func.count(distinct(User.id))) .select_from(Conversation) .join(User, user_join_condition) .filter(Conversation.updated_at >= now - timedelta(days=30), User.is_deleted == 0) - .scalar() - or 0 ) + active_users_30d = active_users_30d_result.scalar() or 0 # 最近7天每日活跃用户(排除已删除用户) daily_active_users = [] for i in range(7): day_start = now - timedelta(days=i + 1) day_end = now - timedelta(days=i) - active_count = ( - db.query(func.count(distinct(User.id))) + active_count_result = await db.execute( + select(func.count(distinct(User.id))) .select_from(Conversation) .join(User, user_join_condition) .filter(Conversation.updated_at >= day_start, Conversation.updated_at < day_end, User.is_deleted == 0) - .scalar() - or 0 ) + active_count = active_count_result.scalar() or 0 daily_active_users.append({"date": day_start.strftime("%Y-%m-%d"), "active_users": active_count}) @@ -299,7 +299,7 @@ async def get_user_activity_stats( @dashboard.get("/stats/tools", response_model=ToolCallStats) async def get_tool_call_stats( - db: Session = Depends(get_db), + db: AsyncSession = Depends(get_db), current_user: User = Depends(get_admin_user), ): """Get tool call statistics (Admin only)""" @@ -309,28 +309,31 @@ async def get_tool_call_stats( now = utc_now() # 基础工具调用统计 - total_calls = db.query(func.count(ToolCall.id)).scalar() or 0 - successful_calls = db.query(func.count(ToolCall.id)).filter(ToolCall.status == "success").scalar() or 0 + total_calls_result = await db.execute(select(func.count(ToolCall.id))) + total_calls = total_calls_result.scalar() or 0 + + successful_calls_result = await db.execute(select(func.count(ToolCall.id)).filter(ToolCall.status == "success")) + successful_calls = successful_calls_result.scalar() or 0 failed_calls = total_calls - successful_calls success_rate = round((successful_calls / total_calls * 100), 2) if total_calls > 0 else 0 # 最常用工具 - most_used_tools = ( - db.query(ToolCall.tool_name, func.count(ToolCall.id).label("count")) + most_used_tools_result = await db.execute( + select(ToolCall.tool_name, func.count(ToolCall.id).label("count")) .group_by(ToolCall.tool_name) .order_by(func.count(ToolCall.id).desc()) .limit(10) - .all() ) + most_used_tools = most_used_tools_result.all() most_used_tools = [{"tool_name": name, "count": count} for name, count in most_used_tools] # 工具错误分布 - tool_errors = ( - db.query(ToolCall.tool_name, func.count(ToolCall.id).label("error_count")) + tool_errors_result = await db.execute( + select(ToolCall.tool_name, func.count(ToolCall.id).label("error_count")) .filter(ToolCall.status == "error") .group_by(ToolCall.tool_name) - .all() ) + tool_errors = tool_errors_result.all() tool_error_distribution = {name: count for name, count in tool_errors} # 最近7天每日工具调用数 @@ -339,12 +342,11 @@ async def get_tool_call_stats( day_start = now - timedelta(days=i + 1) day_end = now - timedelta(days=i) - daily_count = ( - db.query(func.count(ToolCall.id)) + daily_count_result = await db.execute( + select(func.count(ToolCall.id)) .filter(ToolCall.created_at >= day_start, ToolCall.created_at < day_end) - .scalar() - or 0 ) + daily_count = daily_count_result.scalar() or 0 daily_tool_calls.append({"date": day_start.strftime("%Y-%m-%d"), "call_count": daily_count}) @@ -371,7 +373,7 @@ async def get_tool_call_stats( @dashboard.get("/stats/knowledge", response_model=KnowledgeStats) async def get_knowledge_stats( - db: Session = Depends(get_db), + db: AsyncSession = Depends(get_db), current_user: User = Depends(get_admin_user), ): """Get knowledge base statistics (Admin only)""" @@ -502,7 +504,7 @@ async def get_knowledge_stats( @dashboard.get("/stats/agents", response_model=AgentAnalytics) async def get_agent_analytics( - db: Session = Depends(get_db), + db: AsyncSession = Depends(get_db), current_user: User = Depends(get_admin_user), ): """Get AI agent analytics (Admin only)""" @@ -510,11 +512,11 @@ async def get_agent_analytics( from src.storage.db.models import Conversation, MessageFeedback, Message, ToolCall # 获取所有智能体 - agents = ( - db.query(Conversation.agent_id, func.count(Conversation.id).label("conversation_count")) + agents_result = await db.execute( + select(Conversation.agent_id, func.count(Conversation.id).label("conversation_count")) .group_by(Conversation.agent_id) - .all() ) + agents = agents_result.all() total_agents = len(agents) agent_conversation_counts = [{"agent_id": agent_id, "conversation_count": count} for agent_id, count in agents] @@ -522,23 +524,21 @@ async def get_agent_analytics( # 智能体满意度统计 agent_satisfaction = [] for agent_id, _ in agents: - total_feedbacks = ( - db.query(func.count(MessageFeedback.id)) + total_feedbacks_result = await db.execute( + select(func.count(MessageFeedback.id)) .join(Message, MessageFeedback.message_id == Message.id) .join(Conversation, Message.conversation_id == Conversation.id) .filter(Conversation.agent_id == agent_id) - .scalar() - or 0 ) + total_feedbacks = total_feedbacks_result.scalar() or 0 - positive_feedbacks = ( - db.query(func.count(MessageFeedback.id)) + positive_feedbacks_result = await db.execute( + select(func.count(MessageFeedback.id)) .join(Message, MessageFeedback.message_id == Message.id) .join(Conversation, Message.conversation_id == Conversation.id) .filter(Conversation.agent_id == agent_id, MessageFeedback.rating == "like") - .scalar() - or 0 ) + positive_feedbacks = positive_feedbacks_result.scalar() or 0 satisfaction_rate = round((positive_feedbacks / total_feedbacks * 100), 2) if total_feedbacks > 0 else 100 @@ -549,14 +549,13 @@ async def get_agent_analytics( # 智能体工具使用统计 agent_tool_usage = [] for agent_id, _ in agents: - tool_usage_count = ( - db.query(func.count(ToolCall.id)) + tool_usage_count_result = await db.execute( + select(func.count(ToolCall.id)) .join(Message, ToolCall.message_id == Message.id) .join(Conversation, Message.conversation_id == Conversation.id) .filter(Conversation.agent_id == agent_id) - .scalar() - or 0 ) + tool_usage_count = tool_usage_count_result.scalar() or 0 agent_tool_usage.append({"agent_id": agent_id, "tool_usage_count": tool_usage_count}) @@ -601,7 +600,7 @@ async def get_agent_analytics( @dashboard.get("/stats") async def get_dashboard_stats( - db: Session = Depends(get_db), + db: AsyncSession = Depends(get_db), current_user: User = Depends(get_admin_user), ): """Get dashboard statistics (Admin only)""" @@ -609,16 +608,32 @@ async def get_dashboard_stats( try: # Basic counts - total_conversations = db.query(func.count(Conversation.id)).scalar() or 0 - active_conversations = ( - db.query(func.count(Conversation.id)).filter(Conversation.status == "active").scalar() or 0 + total_conversations_result = await db.execute(select(func.count(Conversation.id))) + total_conversations = total_conversations_result.scalar() or 0 + + active_conversations_result = await db.execute( + select(func.count(Conversation.id)).filter( + Conversation.status == "active" + ) ) - total_messages = db.query(func.count(Message.id)).scalar() or 0 - total_users = db.query(func.count(User.id)).filter(User.is_deleted == 0).scalar() or 0 + active_conversations = active_conversations_result.scalar() or 0 + + total_messages_result = await db.execute(select(func.count(Message.id))) + total_messages = total_messages_result.scalar() or 0 + + total_users_result = await db.execute(select(func.count(User.id)).filter(User.is_deleted == 0)) + total_users = total_users_result.scalar() or 0 # Feedback statistics - total_feedbacks = db.query(func.count(MessageFeedback.id)).scalar() or 0 - like_count = db.query(func.count(MessageFeedback.id)).filter(MessageFeedback.rating == "like").scalar() or 0 + total_feedbacks_result = await db.execute(select(func.count(MessageFeedback.id))) + total_feedbacks = total_feedbacks_result.scalar() or 0 + + like_count_result = await db.execute( + select(func.count(MessageFeedback.id)).filter( + MessageFeedback.rating == "like" + ) + ) + like_count = like_count_result.scalar() or 0 # Calculate satisfaction rate satisfaction_rate = round((like_count / total_feedbacks * 100), 2) if total_feedbacks > 0 else 100 @@ -663,7 +678,7 @@ class FeedbackListItem(BaseModel): async def get_all_feedbacks( rating: str | None = None, agent_id: str | None = None, - db: Session = Depends(get_db), + db: AsyncSession = Depends(get_db), current_user: User = Depends(get_admin_user), ): """Get all feedback records (Admin only)""" @@ -673,10 +688,14 @@ async def get_all_feedbacks( # Build query with joins including User table # Try both User.id and User.user_id as MessageFeedback.user_id might be stored as either query = ( - db.query(MessageFeedback, Message, Conversation, User) + select(MessageFeedback, Message, Conversation, User) .join(Message, MessageFeedback.message_id == Message.id) .join(Conversation, Message.conversation_id == Conversation.id) - .outerjoin(User, (MessageFeedback.user_id == User.id) | (MessageFeedback.user_id == User.user_id)) + .outerjoin( + User, + (MessageFeedback.user_id == User.id) + | (MessageFeedback.user_id == User.user_id), + ) ) # Apply filters @@ -688,7 +707,8 @@ async def get_all_feedbacks( # Order by creation time (most recent first) query = query.order_by(MessageFeedback.created_at.desc()) - results = query.all() + results = await db.execute(query) + results = results.all() # Debug logging (privacy-safe) logger.info(f"Found {len(results)} feedback records") @@ -736,7 +756,7 @@ class TimeSeriesStats(BaseModel): async def get_call_timeseries_stats( type: str = "models", # models/agents/tokens/tools time_range: str = "14days", # 14hours/14days/14weeks - db: Session = Depends(get_db), + db: AsyncSession = Depends(get_db), current_user: User = Depends(get_admin_user), ): """Get time series statistics for call analytics (Admin only)""" @@ -773,8 +793,8 @@ async def get_call_timeseries_stats( if type == "models": # 模型调用统计(基于消息数量,按模型分组) # 从message的extra_metadata中提取模型信息 - query = ( - db.query( + query_result = await db.execute( + select( group_format.label("date"), func.count(Message.id).label("count"), func.json_extract(Message.extra_metadata, "$.response_metadata.model_name").label("category"), @@ -784,6 +804,7 @@ async def get_call_timeseries_stats( .group_by(group_format, func.json_extract(Message.extra_metadata, "$.response_metadata.model_name")) .order_by(group_format) ) + query = query_result.all() elif type == "agents": # 智能体调用统计(基于对话更新时间,按智能体分组) # 为对话创建独立的时间格式化器 @@ -794,8 +815,8 @@ async def get_call_timeseries_stats( else: # 14days conv_group_format = func.strftime("%Y-%m-%d", func.datetime(Conversation.updated_at, "+8 hours")) - query = ( - db.query( + query_result = await db.execute( + select( conv_group_format.label("date"), func.count(Conversation.id).label("count"), Conversation.agent_id.label("category"), @@ -805,13 +826,14 @@ async def get_call_timeseries_stats( .group_by(conv_group_format, Conversation.agent_id) .order_by(conv_group_format) ) + query = query_result.all() elif type == "tokens": # Token消耗统计(区分input/output tokens) # 先查询input tokens from sqlalchemy import literal - input_query = ( - db.query( + input_query_result = await db.execute( + select( group_format.label("date"), func.sum( func.coalesce(func.json_extract(Message.extra_metadata, "$.usage_metadata.input_tokens"), 0) @@ -826,10 +848,11 @@ async def get_call_timeseries_stats( .group_by(group_format) .order_by(group_format) ) + input_query = input_query_result.all() # 查询output tokens - output_query = ( - db.query( + output_query_result = await db.execute( + select( group_format.label("date"), func.sum( func.coalesce(func.json_extract(Message.extra_metadata, "$.usage_metadata.output_tokens"), 0) @@ -844,10 +867,11 @@ async def get_call_timeseries_stats( .group_by(group_format) .order_by(group_format) ) + output_query = output_query_result.all() # 合并两个查询结果 - input_results = input_query.all() - output_results = output_query.all() + input_results = input_query + output_results = output_query results = input_results + output_results elif type == "tools": # 工具调用统计(按工具名称分组) @@ -859,8 +883,8 @@ async def get_call_timeseries_stats( else: # 14days tool_group_format = func.strftime("%Y-%m-%d", func.datetime(ToolCall.created_at, "+8 hours")) - query = ( - db.query( + query_result = await db.execute( + select( tool_group_format.label("date"), func.count(ToolCall.id).label("count"), ToolCall.tool_name.label("category"), @@ -869,11 +893,12 @@ async def get_call_timeseries_stats( .group_by(tool_group_format, ToolCall.tool_name) .order_by(tool_group_format) ) + query = query_result.all() else: raise HTTPException(status_code=422, detail=f"Invalid type: {type}") if type != "tokens": - results = query.all() + results = query # 处理堆叠数据格式 # 首先收集所有类别 @@ -953,7 +978,8 @@ async def get_call_timeseries_stats( # 对于工具调用,显示所有时间的总数(与ToolStatsComponent保持一致) from src.storage.db.models import ToolCall - total_count = db.query(func.count(ToolCall.id)).scalar() or 0 + total_count_result = await db.execute(select(func.count(ToolCall.id))) + total_count = total_count_result.scalar() or 0 else: # 其他类型使用时间序列数据的总和 total_count = sum(item["total"] for item in data) diff --git a/server/utils/auth_middleware.py b/server/utils/auth_middleware.py index 1fd881c9..abf1ed97 100644 --- a/server/utils/auth_middleware.py +++ b/server/utils/auth_middleware.py @@ -3,7 +3,7 @@ import re from fastapi import Depends, HTTPException, status from fastapi.security import OAuth2PasswordBearer from jose import JWTError -from sqlalchemy.orm import Session +from sqlalchemy.ext.asyncio import AsyncSession from src.storage.db.manager import db_manager from src.storage.db.models import User @@ -23,17 +23,14 @@ PUBLIC_PATHS = [ ] -# 获取数据库会话 -def get_db(): - db = db_manager.get_session() - try: +# 获取数据库会话(异步版本) +async def get_db(): + async with db_manager.get_async_session_context() as db: yield db - finally: - db.close() -# 获取当前用户 -async def get_current_user(token: str | None = Depends(oauth2_scheme), db: Session = Depends(get_db)): +# 获取当前用户(异步版本) +async def get_current_user(token: str | None = Depends(oauth2_scheme), db: AsyncSession = Depends(get_db)): credentials_exception = HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="无效的凭证", @@ -61,8 +58,10 @@ async def get_current_user(token: str | None = Depends(oauth2_scheme), db: Sessi headers={"WWW-Authenticate": "Bearer"}, ) - # 查找用户 - user = db.query(User).filter(User.id == user_id).first() + # 查找用户(异步版本) + from sqlalchemy import select + result = await db.execute(select(User).filter(User.id == user_id)) + user = result.scalar_one_or_none() if user is None: raise credentials_exception @@ -79,7 +78,6 @@ async def get_required_user(user: User | None = Depends(get_current_user)): ) return user - # 获取管理员用户 async def get_admin_user(current_user: User = Depends(get_required_user)): if current_user.role not in ["admin", "superadmin"]: @@ -89,7 +87,6 @@ async def get_admin_user(current_user: User = Depends(get_required_user)): ) return current_user - # 获取超级管理员用户 async def get_superadmin_user(current_user: User = Depends(get_required_user)): if current_user.role != "superadmin": diff --git a/src/agents/common/mcp_repos/arxiv-mcp-server b/src/agents/common/mcp_repos/arxiv-mcp-server deleted file mode 160000 index 057e2000..00000000 --- a/src/agents/common/mcp_repos/arxiv-mcp-server +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 057e2000be7b56823239815b0fe7c7fc0dbced96 diff --git a/src/agents/common/mcp_repos/mcp-server-mysql b/src/agents/common/mcp_repos/mcp-server-mysql deleted file mode 160000 index 6a036783..00000000 --- a/src/agents/common/mcp_repos/mcp-server-mysql +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 6a0367834ea0fb5e5c94b9711e3e2756966789ea diff --git a/src/storage/conversation/manager.py b/src/storage/conversation/manager.py index 0ce37c87..61a33d09 100644 --- a/src/storage/conversation/manager.py +++ b/src/storage/conversation/manager.py @@ -1,27 +1,28 @@ """ -Conversation Storage Manager +Conversation Storage Manager (Async) Manages conversation data storage including messages, tool calls, and statistics. +All database operations are now asynchronous for improved performance. """ import uuid -from sqlalchemy.orm import Session, joinedload +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy.orm import selectinload from src.storage.db.models import Conversation, ConversationStats, Message, ToolCall from src.utils import logger from src.utils.datetime_utils import utc_now -# TODO:[未完成]待修改为异步版本 - class ConversationManager: - """Manager for conversation storage operations""" + """Async Manager for conversation storage operations""" - def __init__(self, db_session: Session): + def __init__(self, db_session: AsyncSession): self.db = db_session - def create_conversation( + async def create_conversation( self, user_id: str, agent_id: str, @@ -59,18 +60,18 @@ class ConversationManager: self.db.add(conversation) # Flush to assign primary key without committing - self.db.flush() + await self.db.flush() # Create associated stats record and commit once stats = ConversationStats(conversation_id=conversation.id) self.db.add(stats) - self.db.commit() - self.db.refresh(conversation) + await self.db.commit() + await self.db.refresh(conversation) logger.info(f"Created conversation: {conversation.thread_id} for user {user_id}") return conversation - def get_conversation_by_thread_id(self, thread_id: str) -> Conversation | None: + async def get_conversation_by_thread_id(self, thread_id: str) -> Conversation | None: """ Get conversation by thread ID @@ -80,10 +81,12 @@ class ConversationManager: Returns: Conversation object or None if not found """ - return self.db.query(Conversation).filter(Conversation.thread_id == thread_id).first() + result = await self.db.execute(select(Conversation).filter(Conversation.thread_id == thread_id)) + return result.scalar_one_or_none() - def _get_conversation_by_id(self, conversation_id: int) -> Conversation | None: - return self.db.query(Conversation).filter(Conversation.id == conversation_id).first() + async def _get_conversation_by_id(self, conversation_id: int) -> Conversation | None: + result = await self.db.execute(select(Conversation).filter(Conversation.id == conversation_id)) + return result.scalar_one_or_none() def _ensure_metadata(self, conversation: Conversation) -> dict: """ @@ -96,13 +99,13 @@ class ConversationManager: metadata["attachments"] = list(metadata.get("attachments", [])) return metadata - def _save_metadata(self, conversation: Conversation, metadata: dict) -> None: + async def _save_metadata(self, conversation: Conversation, metadata: dict) -> None: conversation.extra_metadata = metadata conversation.updated_at = utc_now() - self.db.commit() - self.db.refresh(conversation) + await self.db.commit() + await self.db.refresh(conversation) - def add_message( + async def add_message( self, conversation_id: int, role: str, @@ -136,20 +139,20 @@ class ConversationManager: self.db.add(message) # Mark the parent conversation as active for sorting/analytics - conversation = self._get_conversation_by_id(conversation_id) + conversation = await self._get_conversation_by_id(conversation_id) if conversation: conversation.updated_at = utc_now() - self.db.commit() - self.db.refresh(message) + await self.db.commit() + await self.db.refresh(message) # Update conversation stats - self._update_message_count(conversation_id) + await self._update_message_count(conversation_id) logger.debug(f"Added {role} message to conversation {conversation_id}") return message - def add_message_by_thread_id( + async def add_message_by_thread_id( self, thread_id: str, role: str, @@ -172,12 +175,12 @@ class ConversationManager: Returns: Created Message object or None if conversation not found """ - conversation = self.get_conversation_by_thread_id(thread_id) + conversation = await self.get_conversation_by_thread_id(thread_id) if not conversation: logger.warning(f"Conversation not found for thread_id: {thread_id}") return None - return self.add_message( + return await self.add_message( conversation_id=conversation.id, role=role, content=content, @@ -186,7 +189,7 @@ class ConversationManager: image_content=image_content, ) - def add_tool_call( + async def add_tool_call( self, message_id: int, tool_name: str, @@ -222,13 +225,13 @@ class ConversationManager: ) self.db.add(tool_call) - self.db.commit() - self.db.refresh(tool_call) + await self.db.commit() + await self.db.refresh(tool_call) logger.debug(f"Added tool call {tool_name} to message {message_id}") return tool_call - def get_messages(self, conversation_id: int, limit: int | None = None, offset: int = 0) -> list[Message]: + async def get_messages(self, conversation_id: int, limit: int | None = None, offset: int = 0) -> list[Message]: """ Get messages for a conversation @@ -241,18 +244,21 @@ class ConversationManager: List of Message objects """ query = ( - self.db.query(Message) + select(Message) + .options(selectinload(Message.tool_calls)) .filter(Message.conversation_id == conversation_id) .order_by(Message.created_at.asc()) - .options(joinedload(Message.tool_calls)) ) if limit: query = query.limit(limit).offset(offset) - return query.all() + result = await self.db.execute(query) + return result.scalars().unique().all() - def get_messages_by_thread_id(self, thread_id: str, limit: int | None = None, offset: int = 0) -> list[Message]: + async def get_messages_by_thread_id( + self, thread_id: str, limit: int | None = None, offset: int = 0 + ) -> list[Message]: """ Get messages for a conversation by thread ID @@ -264,14 +270,14 @@ class ConversationManager: Returns: List of Message objects """ - conversation = self.get_conversation_by_thread_id(thread_id) + conversation = await self.get_conversation_by_thread_id(thread_id) if not conversation: logger.warning(f"Conversation not found for thread_id: {thread_id}") return [] - return self.get_messages(conversation.id, limit, offset) + return await self.get_messages(conversation.id, limit, offset) - def list_conversations( + async def list_conversations( self, user_id: str | None = None, agent_id: str | None = None, status: str = "active" ) -> list[Conversation]: """ @@ -285,7 +291,7 @@ class ConversationManager: Returns: List of Conversation objects """ - query = self.db.query(Conversation).filter(Conversation.status == status) + query = select(Conversation).filter(Conversation.status == status) # Only filter by user_id if it's provided and not empty if user_id: @@ -294,9 +300,11 @@ class ConversationManager: if agent_id: query = query.filter(Conversation.agent_id == agent_id) - return query.order_by(Conversation.updated_at.desc()).all() + query = query.order_by(Conversation.updated_at.desc()) + result = await self.db.execute(query) + return result.scalars().all() - def update_conversation( + async def update_conversation( self, thread_id: str, title: str | None = None, @@ -315,7 +323,7 @@ class ConversationManager: Returns: Updated Conversation object or None if not found """ - conversation = self.get_conversation_by_thread_id(thread_id) + conversation = await self.get_conversation_by_thread_id(thread_id) if not conversation: return None @@ -331,13 +339,13 @@ class ConversationManager: conversation.extra_metadata = current_metadata conversation.updated_at = utc_now() - self.db.commit() - self.db.refresh(conversation) + await self.db.commit() + await self.db.refresh(conversation) logger.info(f"Updated conversation {thread_id}") return conversation - def delete_conversation(self, thread_id: str, soft_delete: bool = True) -> bool: + async def delete_conversation(self, thread_id: str, soft_delete: bool = True) -> bool: """ Delete a conversation @@ -348,22 +356,22 @@ class ConversationManager: Returns: True if successful, False otherwise """ - conversation = self.get_conversation_by_thread_id(thread_id) + conversation = await self.get_conversation_by_thread_id(thread_id) if not conversation: return False if soft_delete: conversation.status = "deleted" - self.db.commit() + await self.db.commit() logger.info(f"Soft deleted conversation {thread_id}") else: self.db.delete(conversation) - self.db.commit() + await self.db.commit() logger.info(f"Permanently deleted conversation {thread_id}") return True - def get_stats(self, conversation_id: int) -> ConversationStats | None: + async def get_stats(self, conversation_id: int) -> ConversationStats | None: """ Get conversation statistics @@ -373,9 +381,12 @@ class ConversationManager: Returns: ConversationStats object or None if not found """ - return self.db.query(ConversationStats).filter(ConversationStats.conversation_id == conversation_id).first() + result = await self.db.execute( + select(ConversationStats).filter(ConversationStats.conversation_id == conversation_id) + ) + return result.scalar_one_or_none() - def update_stats( + async def update_stats( self, conversation_id: int, tokens_used: int | None = None, @@ -394,7 +405,7 @@ class ConversationManager: Returns: Updated ConversationStats object or None if not found """ - stats = self.get_stats(conversation_id) + stats = await self.get_stats(conversation_id) if not stats: return None @@ -406,12 +417,12 @@ class ConversationManager: stats.user_feedback = user_feedback stats.updated_at = utc_now() - self.db.commit() - self.db.refresh(stats) + await self.db.commit() + await self.db.refresh(stats) return stats - def get_tool_call_by_langgraph_id(self, langgraph_tool_call_id: str) -> ToolCall | None: + async def get_tool_call_by_langgraph_id(self, langgraph_tool_call_id: str) -> ToolCall | None: """ Get tool call by LangGraph tool_call_id @@ -421,9 +432,12 @@ class ConversationManager: Returns: ToolCall object or None if not found """ - return self.db.query(ToolCall).filter(ToolCall.langgraph_tool_call_id == langgraph_tool_call_id).first() + result = await self.db.execute( + select(ToolCall).filter(ToolCall.langgraph_tool_call_id == langgraph_tool_call_id) + ) + return result.scalar_one_or_none() - def update_tool_call_output( + async def update_tool_call_output( self, langgraph_tool_call_id: str, tool_output: str, @@ -442,7 +456,7 @@ class ConversationManager: Returns: Updated ToolCall object or None if not found """ - tool_call = self.get_tool_call_by_langgraph_id(langgraph_tool_call_id) + tool_call = await self.get_tool_call_by_langgraph_id(langgraph_tool_call_id) if not tool_call: logger.warning(f"Tool call not found for langgraph_tool_call_id: {langgraph_tool_call_id}") return None @@ -452,44 +466,47 @@ class ConversationManager: if error_message: tool_call.error_message = error_message - self.db.commit() - self.db.refresh(tool_call) + await self.db.commit() + await self.db.refresh(tool_call) logger.debug(f"Updated tool call {langgraph_tool_call_id} with output") return tool_call - def _update_message_count(self, conversation_id: int) -> None: + async def _update_message_count(self, conversation_id: int) -> None: """ Update message count in conversation stats Args: conversation_id: Conversation ID """ - stats = self.get_stats(conversation_id) + from sqlalchemy import func + + stats = await self.get_stats(conversation_id) if stats: - message_count = self.db.query(Message).filter(Message.conversation_id == conversation_id).count() + result = await self.db.execute(select(func.count()).filter(Message.conversation_id == conversation_id)) + message_count = result.scalar() stats.message_count = message_count - self.db.commit() + await self.db.commit() # ------------------------------------------------------------------------- # Attachment helpers # ------------------------------------------------------------------------- - def get_attachments(self, conversation_id: int) -> list[dict]: - conversation = self._get_conversation_by_id(conversation_id) + async def get_attachments(self, conversation_id: int) -> list[dict]: + conversation = await self._get_conversation_by_id(conversation_id) if not conversation: return [] metadata = self._ensure_metadata(conversation) return list(metadata.get("attachments", [])) - def get_attachments_by_thread_id(self, thread_id: str) -> list[dict]: - conversation = self.get_conversation_by_thread_id(thread_id) + async def get_attachments_by_thread_id(self, thread_id: str) -> list[dict]: + conversation = await self.get_conversation_by_thread_id(thread_id) if not conversation: return [] - return self.get_attachments(conversation.id) + return await self.get_attachments(conversation.id) - def add_attachment(self, conversation_id: int, attachment_info: dict) -> dict | None: - conversation = self._get_conversation_by_id(conversation_id) + async def add_attachment(self, conversation_id: int, attachment_info: dict) -> dict | None: + conversation = await self._get_conversation_by_id(conversation_id) if not conversation: return None @@ -498,13 +515,13 @@ class ConversationManager: attachments = [item for item in attachments if item.get("file_id") != attachment_info.get("file_id")] attachments.append(attachment_info) metadata["attachments"] = attachments - self._save_metadata(conversation, metadata) + await self._save_metadata(conversation, metadata) return attachment_info - def update_attachment_status( + async def update_attachment_status( self, conversation_id: int, file_id: str, status: str, update_fields: dict | None = None ) -> dict | None: - conversation = self._get_conversation_by_id(conversation_id) + conversation = await self._get_conversation_by_id(conversation_id) if not conversation: return None @@ -521,11 +538,11 @@ class ConversationManager: if target is not None: metadata["attachments"] = attachments - self._save_metadata(conversation, metadata) + await self._save_metadata(conversation, metadata) return target - def remove_attachment(self, conversation_id: int, file_id: str) -> bool: - conversation = self._get_conversation_by_id(conversation_id) + async def remove_attachment(self, conversation_id: int, file_id: str) -> bool: + conversation = await self._get_conversation_by_id(conversation_id) if not conversation: return False @@ -537,5 +554,5 @@ class ConversationManager: return False metadata["attachments"] = new_attachments - self._save_metadata(conversation, metadata) + await self._save_metadata(conversation, metadata) return True diff --git a/src/storage/db/manager.py b/src/storage/db/manager.py index 68105f72..a546b282 100644 --- a/src/storage/db/manager.py +++ b/src/storage/db/manager.py @@ -1,9 +1,10 @@ import json import os import pathlib -from contextlib import contextmanager +from contextlib import asynccontextmanager, contextmanager from sqlalchemy import create_engine +from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine from sqlalchemy.orm import sessionmaker from server.utils.singleton import SingletonMeta @@ -20,26 +21,34 @@ except ImportError: def validate_database_schema(db_path): return True, [] -# TODO:[优化建议]需要将数据库修改为异步的aiosqlite或者异步mysql,缓存使用Redis存储 +# TODO:[已完成]为DBManager添加异步支持 # TODO:[已完成]为DBManager添加单例模式 class DBManager(metaclass=SingletonMeta): - """数据库管理器 - 只提供基础的数据库连接和会话管理""" + """数据库管理器 - 提供异步数据库连接和会话管理""" def __init__(self): self.db_path = os.path.join(config.save_dir, "database", "server.db") self.ensure_db_dir() - # 创建SQLAlchemy引擎,配置JSON序列化器以支持中文 + # 创建异步SQLAlchemy引擎,配置JSON序列化器以支持中文 # 使用 ensure_ascii=False 确保中文字符不被转义为 Unicode 序列 + self.async_engine = create_async_engine( + f"sqlite+aiosqlite:///{self.db_path}", + json_serializer=lambda obj: json.dumps(obj, ensure_ascii=False), + json_deserializer=json.loads, + ) + + # 创建异步会话工厂 + self.AsyncSession = async_sessionmaker(bind=self.async_engine, class_=AsyncSession, expire_on_commit=False) + + # 保留同步引擎用于迁移等特殊操作 self.engine = create_engine( f"sqlite:///{self.db_path}", json_serializer=lambda obj: json.dumps(obj, ensure_ascii=False), json_deserializer=json.loads, ) - - # 创建会话工厂 self.Session = sessionmaker(bind=self.engine) # 首先创建基本表结构 @@ -86,12 +95,12 @@ class DBManager(metaclass=SingletonMeta): logger.warning("=" * 60) def get_session(self): - """获取数据库会话""" + """获取同步数据库会话""" return self.Session() @contextmanager def get_session_context(self): - """获取数据库会话的上下文管理器""" + """获取同步数据库会话的上下文管理器""" session = self.Session() try: yield session @@ -103,6 +112,24 @@ class DBManager(metaclass=SingletonMeta): finally: session.close() + async def get_async_session(self): + """获取异步数据库会话""" + return self.AsyncSession() + + @asynccontextmanager + async def get_async_session_context(self): + """获取异步数据库会话的上下文管理器""" + session = self.AsyncSession() + try: + yield session + await session.commit() + except Exception as e: + await session.rollback() + logger.error(f"Async database operation failed: {e}") + raise + finally: + await session.close() + def check_first_run(self): """检查是否首次运行""" session = self.get_session() diff --git a/web/src/components/AgentChatComponent.vue b/web/src/components/AgentChatComponent.vue index f6506864..0a8c0cd5 100644 --- a/web/src/components/AgentChatComponent.vue +++ b/web/src/components/AgentChatComponent.vue @@ -983,8 +983,6 @@ const selectChat = async (chatId) => { chatUIStore.isLoadingMessages = true; try { await fetchThreadMessages({ agentId: currentAgentId.value, threadId: chatId }); - await loadThreadAttachments(chatId, { silent: true }); - await fetchAgentState(currentAgentId.value, chatId); } catch (error) { handleChatError(error, 'load'); } finally { @@ -993,6 +991,8 @@ const selectChat = async (chatId) => { await nextTick(); scrollController.scrollToBottomStaticForce(); + await loadThreadAttachments(chatId, { silent: true }); + await fetchAgentState(currentAgentId.value, chatId); }; const deleteChat = async (chatId) => { diff --git a/web/src/components/ChatSidebarComponent.vue b/web/src/components/ChatSidebarComponent.vue index 972da053..447e9de7 100644 --- a/web/src/components/ChatSidebarComponent.vue +++ b/web/src/components/ChatSidebarComponent.vue @@ -188,7 +188,7 @@ const renameChat = async (chatId) => { content: h('div', { style: { marginTop: '12px' } }, [ h('input', { value: newTitle, - style: { width: '100%', padding: '4px 8px', border: '1px solid #d9d9d9', borderRadius: '4px' }, + style: { width: '100%', padding: '4px 8px', border: '1px solid var(--gray-150)', background: 'var(--gray-0)', borderRadius: '4px' }, onInput: (e) => { newTitle = e.target.value; } }) ]),