**Phase 1 — Agent Usage Sync to AgentHub Collector**
- Add agent_usage service: per-agent stats (messages, tokens, conversations, unique users, first/last used)
- Collector sync now includes usage data in payload; sync_agent accepts optional db session
- Celery beat task runs every 6h to sync all active agents with fresh usage stats
**Phase 2 — LibreCodeInterpreter Integration**
- Add code-interpreter, redis, minio services to docker-compose.prod.yml
- CodeInterpreterTool (BaseTool): sandboxed execution via /exec, 13 languages, Python session persistence via conversation_id
- ToolContext extended with conversation_id and agent_slug
- enable_code_interpreter boolean on Agent model (migration 027), tool seeded in tool_definitions (migration 026)
- Code interpreter auto-injected into agent tools when enabled
- Frontend: CodeExecutionResult component with terminal-style stdout/stderr/files rendering
**Phase 3 — Agent API Endpoints**
- GET /api/v1/agents/{slug}/analytics — per-agent usage stats + daily time series
- POST /api/v1/agents/{slug}/execute — synchronous programmatic agent execution (non-SSE)
- Sub-routes registered before /{slug} to avoid FastAPI route conflict
**Phase 4 — Fix Department & Region RAG Scoping**
- Department filter now OR-includes global (null department) docs, matching region filter behaviour
- retriever.search_documents/retrieve_and_prepare/query accept department_ids/region_codes lists
- MatchAny used for multi-value Qdrant filters; chat.py passes full arrays from knowledge_scope
- Admin PATCH /users/{id} now validates region_code against the regions table
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
90 lines
2.8 KiB
Python
90 lines
2.8 KiB
Python
"""
|
|
Agent Usage Statistics Service
|
|
|
|
Computes per-agent usage metrics from conversations and messages tables.
|
|
Used by the agent collector sync and the analytics API endpoint.
|
|
"""
|
|
import logging
|
|
from datetime import datetime, timezone, timedelta
|
|
from typing import Optional
|
|
from uuid import UUID
|
|
|
|
from sqlalchemy import select, func
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from app.models.conversation import Conversation, Message
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
async def get_agent_usage_stats(
|
|
db: AsyncSession,
|
|
agent_id: UUID,
|
|
days: Optional[int] = None,
|
|
) -> dict:
|
|
"""
|
|
Compute usage statistics for a single agent.
|
|
|
|
Args:
|
|
db: Async database session
|
|
agent_id: Agent UUID
|
|
days: If set, restrict to the last N days. If None, all-time.
|
|
|
|
Returns:
|
|
{
|
|
"total_messages": int,
|
|
"total_tokens": int,
|
|
"conversations": int,
|
|
"unique_users": int,
|
|
"first_used": datetime | None,
|
|
"last_used": datetime | None,
|
|
}
|
|
"""
|
|
since: Optional[datetime] = None
|
|
if days is not None:
|
|
since = datetime.now(timezone.utc) - timedelta(days=days)
|
|
|
|
# Base filter: conversations linked to this agent
|
|
conv_filter = Conversation.agent_id == agent_id
|
|
if since:
|
|
conv_filter = conv_filter & (Conversation.created_at >= since)
|
|
|
|
# Subquery: conversation IDs for this agent
|
|
conv_ids_subq = select(Conversation.id).where(
|
|
Conversation.agent_id == agent_id,
|
|
*([Conversation.created_at >= since] if since else []),
|
|
).scalar_subquery()
|
|
|
|
# Aggregate query over messages in those conversations
|
|
msg_query = select(
|
|
func.count(Message.id).label("total_messages"),
|
|
func.coalesce(func.sum(Message.output_tokens), 0).label("total_tokens"),
|
|
func.min(Message.created_at).label("first_used"),
|
|
func.max(Message.created_at).label("last_used"),
|
|
).where(
|
|
Message.conversation_id.in_(conv_ids_subq),
|
|
Message.role == "assistant",
|
|
)
|
|
|
|
msg_result = await db.execute(msg_query)
|
|
msg_row = msg_result.one()
|
|
|
|
# Count distinct conversations and users
|
|
conv_query = select(
|
|
func.count(Conversation.id.distinct()).label("conversations"),
|
|
func.count(Conversation.user_id.distinct()).label("unique_users"),
|
|
).where(Conversation.agent_id == agent_id)
|
|
if since:
|
|
conv_query = conv_query.where(Conversation.created_at >= since)
|
|
|
|
conv_result = await db.execute(conv_query)
|
|
conv_row = conv_result.one()
|
|
|
|
return {
|
|
"total_messages": msg_row.total_messages or 0,
|
|
"total_tokens": int(msg_row.total_tokens or 0),
|
|
"conversations": conv_row.conversations or 0,
|
|
"unique_users": conv_row.unique_users or 0,
|
|
"first_used": msg_row.first_used,
|
|
"last_used": msg_row.last_used,
|
|
}
|