ForcePilot/server/routers/dashboard_router.py
Wenjie Zhang cdb381b0e9 feat(feedback): 新增消息反馈功能,支持用户对消息的点赞和点踩
- 在 chat_router 中添加了提交和获取消息反馈的 API 接口
- 在 dashboard_router 中更新统计信息,包含反馈统计数据
- 新增 MessageFeedback 数据模型,存储用户反馈信息
- 前端实现反馈提交和状态获取功能,优化用户体验
- 更新 DashboardView 以展示用户反馈统计信息
2025-10-05 14:50:34 +08:00

321 lines
11 KiB
Python

"""
Dashboard Router - Statistics and monitoring endpoints
Provides centralized dashboard APIs for monitoring system-wide statistics.
"""
import traceback
from datetime import datetime, timedelta
from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel
from sqlalchemy import distinct, func
from sqlalchemy.orm import Session
from server.routers.auth_router import get_admin_user
from server.utils.auth_middleware import get_db
from src.storage.conversation import ConversationManager
from src.storage.db.models import User
from src.utils.logging_config import logger
dashboard = APIRouter(prefix="/dashboard", tags=["Dashboard"])
# =============================================================================
# Response Models
# =============================================================================
class ConversationListItem(BaseModel):
"""Conversation list item"""
thread_id: str
user_id: str
agent_id: str
title: str
status: str
message_count: int
created_at: str
updated_at: str
class ConversationDetailResponse(BaseModel):
"""Conversation detail"""
thread_id: str
user_id: str
agent_id: str
title: str
status: str
message_count: int
created_at: str
updated_at: str
total_tokens: int
messages: list[dict]
# =============================================================================
# Conversation Management
# =============================================================================
@dashboard.get("/conversations", response_model=list[ConversationListItem])
async def get_all_conversations(
user_id: str | None = None,
agent_id: str | None = None,
status: str = "active",
limit: int = 100,
offset: int = 0,
db: Session = Depends(get_db),
current_user: User = Depends(get_admin_user),
):
"""Get all conversations (Admin only)"""
from src.storage.db.models import Conversation, ConversationStats
try:
# Build query
query = db.query(Conversation, ConversationStats).outerjoin(
ConversationStats, Conversation.id == ConversationStats.conversation_id
)
# Apply filters
if user_id:
query = query.filter(Conversation.user_id == user_id)
if agent_id:
query = query.filter(Conversation.agent_id == agent_id)
if status != "all":
query = query.filter(Conversation.status == status)
# Order and paginate
query = query.order_by(Conversation.updated_at.desc()).limit(limit).offset(offset)
results = query.all()
return [
{
"thread_id": conv.thread_id,
"user_id": conv.user_id,
"agent_id": conv.agent_id,
"title": conv.title,
"status": conv.status,
"message_count": stats.message_count if stats else 0,
"created_at": conv.created_at.isoformat(),
"updated_at": conv.updated_at.isoformat(),
}
for conv, stats in results
]
except Exception as e:
logger.error(f"Error getting conversations: {e}")
logger.error(traceback.format_exc())
raise HTTPException(status_code=500, detail=f"Failed to get conversations: {str(e)}")
@dashboard.get("/conversations/{thread_id}", response_model=ConversationDetailResponse)
async def get_conversation_detail(
thread_id: str,
db: Session = Depends(get_db),
current_user: User = Depends(get_admin_user),
):
"""Get conversation detail (Admin only)"""
try:
conv_manager = ConversationManager(db)
conversation = conv_manager.get_conversation_by_thread_id(thread_id)
if not conversation:
raise HTTPException(status_code=404, detail="Conversation not found")
# Get messages and stats
messages = conv_manager.get_messages(conversation.id)
stats = conv_manager.get_stats(conversation.id)
# Format messages
message_list = []
for msg in messages:
msg_dict = {
"id": msg.id,
"role": msg.role,
"content": msg.content,
"message_type": msg.message_type,
"created_at": msg.created_at.isoformat(),
}
# Include tool calls if present
if msg.tool_calls:
msg_dict["tool_calls"] = [
{
"id": tc.id,
"tool_name": tc.tool_name,
"tool_input": tc.tool_input,
"tool_output": tc.tool_output,
"status": tc.status,
}
for tc in msg.tool_calls
]
message_list.append(msg_dict)
return {
"thread_id": conversation.thread_id,
"user_id": conversation.user_id,
"agent_id": conversation.agent_id,
"title": conversation.title,
"status": conversation.status,
"message_count": stats.message_count if stats else len(message_list),
"created_at": conversation.created_at.isoformat(),
"updated_at": conversation.updated_at.isoformat(),
"total_tokens": stats.total_tokens if stats else 0,
"messages": message_list,
}
except HTTPException:
raise
except Exception as e:
logger.error(f"Error getting conversation detail: {e}")
logger.error(traceback.format_exc())
raise HTTPException(status_code=500, detail=f"Failed to get conversation detail: {str(e)}")
# =============================================================================
# Statistics
# =============================================================================
@dashboard.get("/stats")
async def get_dashboard_stats(
db: Session = Depends(get_db),
current_user: User = Depends(get_admin_user),
):
"""Get dashboard statistics (Admin only)"""
from src.storage.db.models import Conversation, Message, MessageFeedback
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_messages = db.query(func.count(Message.id)).scalar() or 0
total_users = db.query(func.count(distinct(Conversation.user_id))).scalar() or 0
# Conversations by agent
conversations_by_agent = (
db.query(Conversation.agent_id, func.count(Conversation.id).label("count"))
.group_by(Conversation.agent_id)
.all()
)
# Recent activity (last 24 hours)
yesterday = datetime.utcnow() - timedelta(days=1)
recent_conversations = (
db.query(func.count(Conversation.id)).filter(Conversation.created_at >= yesterday).scalar() or 0
)
recent_messages = db.query(func.count(Message.id)).filter(Message.created_at >= yesterday).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
dislike_count = (
db.query(func.count(MessageFeedback.id)).filter(MessageFeedback.rating == "dislike").scalar() or 0
)
# Calculate satisfaction rate
satisfaction_rate = round((like_count / total_feedbacks * 100), 2) if total_feedbacks > 0 else 0
# Recent feedback (last 24 hours)
recent_feedbacks = (
db.query(func.count(MessageFeedback.id)).filter(MessageFeedback.created_at >= yesterday).scalar() or 0
)
return {
"timestamp": datetime.utcnow().isoformat(),
"total_conversations": total_conversations,
"active_conversations": active_conversations,
"total_messages": total_messages,
"total_users": total_users,
"conversations_by_agent": [
{"agent_id": agent_id or "unknown", "count": count} for agent_id, count in conversations_by_agent
],
"recent_activity": {
"conversations_24h": recent_conversations,
"messages_24h": recent_messages,
},
"feedback_stats": {
"total_feedbacks": total_feedbacks,
"like_count": like_count,
"dislike_count": dislike_count,
"satisfaction_rate": satisfaction_rate,
"recent_feedbacks_24h": recent_feedbacks,
},
}
except Exception as e:
logger.error(f"Error getting dashboard stats: {e}")
logger.error(traceback.format_exc())
raise HTTPException(status_code=500, detail=f"Failed to get dashboard stats: {str(e)}")
# =============================================================================
# Feedback Management
# =============================================================================
class FeedbackListItem(BaseModel):
"""Feedback list item"""
id: int
message_id: int
user_id: str
rating: str
reason: str | None
created_at: str
message_content: str
conversation_title: str | None
agent_id: str
@dashboard.get("/feedbacks", response_model=list[FeedbackListItem])
async def get_all_feedbacks(
rating: str | None = None,
limit: int = 100,
offset: int = 0,
db: Session = Depends(get_db),
current_user: User = Depends(get_admin_user),
):
"""Get all feedback records (Admin only)"""
from src.storage.db.models import MessageFeedback, Message, Conversation
try:
# Build query with joins
query = (
db.query(MessageFeedback, Message, Conversation)
.join(Message, MessageFeedback.message_id == Message.id)
.join(Conversation, Message.conversation_id == Conversation.id)
)
# Apply filters
if rating and rating in ["like", "dislike"]:
query = query.filter(MessageFeedback.rating == rating)
# Order and paginate
query = query.order_by(MessageFeedback.created_at.desc()).limit(limit).offset(offset)
results = query.all()
return [
{
"id": feedback.id,
"message_id": feedback.message_id,
"user_id": feedback.user_id,
"rating": feedback.rating,
"reason": feedback.reason,
"created_at": feedback.created_at.isoformat(),
"message_content": message.content[:100] + ("..." if len(message.content) > 100 else ""),
"conversation_title": conversation.title,
"agent_id": conversation.agent_id,
}
for feedback, message, conversation in results
]
except Exception as e:
logger.error(f"Error getting feedbacks: {e}")
logger.error(traceback.format_exc())
raise HTTPException(status_code=500, detail=f"Failed to get feedbacks: {str(e)}")