- 在 chat_router 中添加了提交和获取消息反馈的 API 接口 - 在 dashboard_router 中更新统计信息,包含反馈统计数据 - 新增 MessageFeedback 数据模型,存储用户反馈信息 - 前端实现反馈提交和状态获取功能,优化用户体验 - 更新 DashboardView 以展示用户反馈统计信息
321 lines
11 KiB
Python
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)}")
|