Oliver-ai-bot_2.0/backend/app/services/agent_usage.py
Vadym Samoilenko f2b5dce63a feat: code interpreter, agent analytics/execute APIs, usage sync, RAG scoping fixes
**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>
2026-03-30 20:13:27 +01:00

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,
}