Clawith/backend/app/api/activity.py

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