287 lines
11 KiB
Python
287 lines
11 KiB
Python
"""Activity log API — view agent work history."""
|
|
|
|
import uuid
|
|
from fastapi import APIRouter, Depends, Query
|
|
from sqlalchemy import select, func
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from app.core.security import get_current_user
|
|
from app.core.permissions import check_agent_access
|
|
from app.database import get_db
|
|
from app.models.activity_log import AgentActivityLog
|
|
from app.models.user import User
|
|
|
|
router = APIRouter(tags=["activity"])
|
|
|
|
|
|
@router.get("/agents/{agent_id}/activity")
|
|
async def get_agent_activity(
|
|
agent_id: uuid.UUID,
|
|
limit: int = Query(50, le=200),
|
|
current_user: User = Depends(get_current_user),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
"""Get recent activity logs for an agent."""
|
|
await check_agent_access(db, current_user, agent_id)
|
|
|
|
result = await db.execute(
|
|
select(AgentActivityLog)
|
|
.where(AgentActivityLog.agent_id == agent_id)
|
|
.order_by(AgentActivityLog.created_at.desc())
|
|
.limit(limit)
|
|
)
|
|
logs = result.scalars().all()
|
|
|
|
return [
|
|
{
|
|
"id": str(log.id),
|
|
"action_type": log.action_type,
|
|
"summary": log.summary,
|
|
"detail": log.detail_json,
|
|
"related_id": str(log.related_id) if log.related_id else None,
|
|
"created_at": log.created_at.isoformat() if log.created_at else None,
|
|
}
|
|
for log in logs
|
|
]
|
|
|
|
|
|
# ─── Chat History (per-agent) ─────────────────────────────────
|
|
|
|
@router.get("/agents/{agent_id}/chat-history/conversations")
|
|
async def list_conversations(
|
|
agent_id: uuid.UUID,
|
|
current_user: User = Depends(get_current_user),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
"""List all conversation partners for this agent (web users + other agents)."""
|
|
await check_agent_access(db, current_user, agent_id)
|
|
|
|
from app.models.audit import ChatMessage
|
|
from app.models.agent import Agent
|
|
from app.models.chat_session import ChatSession
|
|
|
|
conversations = []
|
|
|
|
# 1. Web chat conversations (from ChatMessage table, grouped by user)
|
|
web_users_q = await db.execute(
|
|
select(ChatMessage.user_id, func.max(ChatMessage.created_at).label("last_at"), func.count(ChatMessage.id).label("cnt"))
|
|
.where(ChatMessage.agent_id == agent_id, ChatMessage.conversation_id.like("web_%"))
|
|
.group_by(ChatMessage.user_id)
|
|
)
|
|
for row in web_users_q.fetchall():
|
|
user_id, last_at, cnt = row
|
|
user_r = await db.execute(select(User.display_name).where(User.id == user_id))
|
|
name = user_r.scalar_one_or_none() or "未知用户"
|
|
# Get last message
|
|
last_msg_r = await db.execute(
|
|
select(ChatMessage.content)
|
|
.where(ChatMessage.agent_id == agent_id, ChatMessage.user_id == user_id)
|
|
.order_by(ChatMessage.created_at.desc()).limit(1)
|
|
)
|
|
last_content = last_msg_r.scalar_one_or_none() or ""
|
|
conversations.append({
|
|
"conv_id": f"web_{user_id}",
|
|
"partner_type": "user",
|
|
"partner_id": str(user_id),
|
|
"partner_name": f"👤 {name}",
|
|
"last_message": last_content[:80],
|
|
"message_count": cnt,
|
|
"last_at": last_at.isoformat() if last_at else None,
|
|
})
|
|
|
|
# 1b. Feishu conversations (P2P and group)
|
|
feishu_convs_q = await db.execute(
|
|
select(
|
|
ChatMessage.conversation_id,
|
|
func.max(ChatMessage.created_at).label("last_at"),
|
|
func.count(ChatMessage.id).label("cnt"),
|
|
)
|
|
.where(
|
|
ChatMessage.agent_id == agent_id,
|
|
ChatMessage.conversation_id.like("feishu_%"),
|
|
)
|
|
.group_by(ChatMessage.conversation_id)
|
|
)
|
|
for row in feishu_convs_q.fetchall():
|
|
conv_id, last_at, cnt = row
|
|
# Get last message
|
|
last_msg_r = await db.execute(
|
|
select(ChatMessage.content)
|
|
.where(ChatMessage.agent_id == agent_id, ChatMessage.conversation_id == conv_id)
|
|
.order_by(ChatMessage.created_at.desc()).limit(1)
|
|
)
|
|
last_content = last_msg_r.scalar_one_or_none() or ""
|
|
|
|
# Determine display name
|
|
if conv_id.startswith("feishu_p2p_"):
|
|
# Try to get sender name from first user message
|
|
name_r = await db.execute(
|
|
select(ChatMessage.content)
|
|
.where(
|
|
ChatMessage.agent_id == agent_id,
|
|
ChatMessage.conversation_id == conv_id,
|
|
ChatMessage.role == "user",
|
|
)
|
|
.order_by(ChatMessage.created_at.asc()).limit(1)
|
|
)
|
|
first_msg = name_r.scalar_one_or_none() or ""
|
|
# Extract sender name from [发送者: xxx] prefix
|
|
import re
|
|
sender_match = re.search(r'\[发送者:\s*([^\]]+?)(?:\s*\(ID:.*?\))?\]', first_msg)
|
|
display_name = f"📱 {sender_match.group(1)}" if sender_match else f"📱 飞书用户"
|
|
else:
|
|
display_name = "👥 飞书群聊"
|
|
|
|
conversations.append({
|
|
"conv_id": conv_id,
|
|
"partner_type": "feishu",
|
|
"partner_id": conv_id,
|
|
"partner_name": display_name,
|
|
"last_message": last_content[:80],
|
|
"message_count": cnt,
|
|
"last_at": last_at.isoformat() if last_at else None,
|
|
})
|
|
|
|
# 1c. Slack conversations
|
|
for prefix, icon, label in [("slack_", "💬", "Slack"), ("discord_", "🎮", "Discord")]:
|
|
ch_convs_q = await db.execute(
|
|
select(
|
|
ChatMessage.conversation_id,
|
|
func.max(ChatMessage.created_at).label("last_at"),
|
|
func.count(ChatMessage.id).label("cnt"),
|
|
)
|
|
.where(ChatMessage.agent_id == agent_id, ChatMessage.conversation_id.like(f"{prefix}%"))
|
|
.group_by(ChatMessage.conversation_id)
|
|
)
|
|
for row in ch_convs_q.fetchall():
|
|
conv_id, last_at, cnt = row
|
|
last_msg_r = await db.execute(
|
|
select(ChatMessage.content)
|
|
.where(ChatMessage.agent_id == agent_id, ChatMessage.conversation_id == conv_id)
|
|
.order_by(ChatMessage.created_at.desc()).limit(1)
|
|
)
|
|
last_content = last_msg_r.scalar_one_or_none() or ""
|
|
# Build a readable name from conv_id e.g. slack_C123_U456 → Slack C123
|
|
parts = conv_id.split("_", 2)
|
|
channel_part = parts[1] if len(parts) > 1 else conv_id
|
|
display_name = f"{icon} {label} #{channel_part}" if channel_part != "dm" else f"{icon} {label} DM"
|
|
conversations.append({
|
|
"conv_id": conv_id,
|
|
"partner_type": prefix.rstrip("_"),
|
|
"partner_id": conv_id,
|
|
"partner_name": display_name,
|
|
"last_message": last_content[:80],
|
|
"message_count": cnt,
|
|
"last_at": last_at.isoformat() if last_at else None,
|
|
})
|
|
|
|
# 2. Agent-to-agent conversations (from ChatSession with peer_agent_id)
|
|
agent_sessions_q = await db.execute(
|
|
select(ChatSession).where(
|
|
ChatSession.source_channel == "agent",
|
|
(ChatSession.agent_id == agent_id) | (ChatSession.peer_agent_id == agent_id),
|
|
)
|
|
)
|
|
for sess in agent_sessions_q.scalars().all():
|
|
# Determine the partner agent
|
|
partner_id = sess.peer_agent_id if sess.agent_id == agent_id else sess.agent_id
|
|
agent_r = await db.execute(select(Agent.name).where(Agent.id == partner_id))
|
|
partner_name = agent_r.scalar_one_or_none() or "未知数字员工"
|
|
|
|
# Count messages in this session
|
|
stats_q = await db.execute(
|
|
select(func.count(ChatMessage.id), func.max(ChatMessage.created_at))
|
|
.where(ChatMessage.conversation_id == str(sess.id))
|
|
)
|
|
stats = stats_q.fetchone()
|
|
cnt = stats[0] if stats else 0
|
|
last_at = stats[1] if stats else None
|
|
|
|
# Get last message
|
|
last_msg_r = await db.execute(
|
|
select(ChatMessage.content)
|
|
.where(ChatMessage.conversation_id == str(sess.id))
|
|
.order_by(ChatMessage.created_at.desc()).limit(1)
|
|
)
|
|
last_content = last_msg_r.scalar_one_or_none() or ""
|
|
|
|
conversations.append({
|
|
"conv_id": str(sess.id),
|
|
"partner_type": "agent",
|
|
"partner_id": str(partner_id),
|
|
"partner_name": f"🤖 {partner_name}",
|
|
"last_message": last_content[:80],
|
|
"message_count": cnt,
|
|
"last_at": last_at.isoformat() if last_at else None,
|
|
})
|
|
|
|
# Sort by last_at desc
|
|
conversations.sort(key=lambda c: c["last_at"] or "", reverse=True)
|
|
return conversations
|
|
|
|
|
|
@router.get("/agents/{agent_id}/chat-history/{conv_id:path}")
|
|
async def get_conversation_messages(
|
|
agent_id: uuid.UUID,
|
|
conv_id: str,
|
|
limit: int = Query(100, le=500),
|
|
current_user: User = Depends(get_current_user),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
"""Get messages for a specific conversation."""
|
|
await check_agent_access(db, current_user, agent_id)
|
|
|
|
messages = []
|
|
|
|
if conv_id.startswith("web_") or conv_id.startswith("feishu_") or conv_id.startswith("slack_") or conv_id.startswith("discord_"):
|
|
from app.models.audit import ChatMessage
|
|
result = await db.execute(
|
|
select(ChatMessage)
|
|
.where(ChatMessage.agent_id == agent_id, ChatMessage.conversation_id == conv_id)
|
|
.order_by(ChatMessage.created_at.asc())
|
|
.limit(limit)
|
|
)
|
|
for m in result.scalars().all():
|
|
content = m.content
|
|
# Strip [发送者: xxx] prefix for display (identity shown in UI)
|
|
if content.startswith("[发送者:"):
|
|
import re
|
|
content = re.sub(r'^\[发送者:[^\]]*\]\s*', '', content)
|
|
messages.append({
|
|
"id": str(m.id),
|
|
"role": m.role,
|
|
"content": content,
|
|
"created_at": m.created_at.isoformat() if m.created_at else None,
|
|
})
|
|
elif conv_id.startswith("agent_") or len(conv_id) == 36:
|
|
# Agent-to-agent conversation — conv_id is the ChatSession UUID
|
|
from app.models.audit import ChatMessage
|
|
from app.models.agent import Agent
|
|
from app.models.participant import Participant
|
|
|
|
result = await db.execute(
|
|
select(ChatMessage)
|
|
.where(ChatMessage.conversation_id == conv_id)
|
|
.order_by(ChatMessage.created_at.asc())
|
|
.limit(limit)
|
|
)
|
|
name_cache = {}
|
|
for m in result.scalars().all():
|
|
# Determine sender name from participant_id
|
|
sender_name = "未知"
|
|
if m.participant_id:
|
|
pid_str = str(m.participant_id)
|
|
if pid_str not in name_cache:
|
|
p_r = await db.execute(select(Participant.display_name).where(Participant.id == m.participant_id))
|
|
name_cache[pid_str] = p_r.scalar_one_or_none() or "未知"
|
|
sender_name = name_cache[pid_str]
|
|
messages.append({
|
|
"id": str(m.id),
|
|
"role": m.role,
|
|
"sender_name": sender_name,
|
|
"content": m.content,
|
|
"created_at": m.created_at.isoformat() if m.created_at else None,
|
|
})
|
|
|
|
return messages
|