dashboard-nanobot/backend/services/platform_activity_service.py

132 lines
4.3 KiB
Python

import json
from datetime import datetime, timedelta
from typing import Any, Dict, List, Optional
from sqlalchemy import delete as sql_delete
from sqlmodel import Session, select
from models.platform import BotActivityEvent
from schemas.platform import PlatformActivityItem
from services.platform_settings_service import get_activity_event_retention_days
ACTIVITY_EVENT_PRUNE_INTERVAL = timedelta(minutes=10)
OPERATIONAL_ACTIVITY_EVENT_TYPES = {
"bot_created",
"bot_started",
"bot_stopped",
"bot_warning",
"bot_enabled",
"bot_disabled",
"bot_deactivated",
"command_submitted",
"command_failed",
"history_cleared",
}
_last_activity_event_prune_at: Optional[datetime] = None
def _utcnow() -> datetime:
return datetime.utcnow()
def prune_expired_activity_events(session: Session, force: bool = False) -> int:
global _last_activity_event_prune_at
now = _utcnow()
if not force and _last_activity_event_prune_at and now - _last_activity_event_prune_at < ACTIVITY_EVENT_PRUNE_INTERVAL:
return 0
retention_days = get_activity_event_retention_days(session)
cutoff = now - timedelta(days=retention_days)
result = session.exec(sql_delete(BotActivityEvent).where(BotActivityEvent.created_at < cutoff))
_last_activity_event_prune_at = now
return int(getattr(result, "rowcount", 0) or 0)
def record_activity_event(
session: Session,
bot_id: str,
event_type: str,
request_id: Optional[str] = None,
channel: str = "dashboard",
detail: Optional[str] = None,
metadata: Optional[Dict[str, Any]] = None,
) -> None:
normalized_event_type = str(event_type or "unknown").strip().lower() or "unknown"
if normalized_event_type not in OPERATIONAL_ACTIVITY_EVENT_TYPES:
return
prune_expired_activity_events(session, force=False)
row = BotActivityEvent(
bot_id=bot_id,
request_id=request_id,
event_type=normalized_event_type,
channel=str(channel or "dashboard").strip().lower() or "dashboard",
detail=(str(detail or "").strip() or None),
metadata_json=json.dumps(metadata or {}, ensure_ascii=False) if metadata else None,
created_at=_utcnow(),
)
session.add(row)
def list_activity_events(
session: Session,
bot_id: Optional[str] = None,
limit: int = 100,
) -> List[Dict[str, Any]]:
deleted = prune_expired_activity_events(session, force=False)
if deleted > 0:
session.commit()
safe_limit = max(1, min(int(limit), 500))
stmt = select(BotActivityEvent).order_by(BotActivityEvent.created_at.desc(), BotActivityEvent.id.desc()).limit(safe_limit)
if bot_id:
stmt = stmt.where(BotActivityEvent.bot_id == bot_id)
rows = session.exec(stmt).all()
items: List[Dict[str, Any]] = []
for row in rows:
try:
metadata = json.loads(row.metadata_json or "{}")
except Exception:
metadata = {}
items.append(
PlatformActivityItem(
id=int(row.id or 0),
bot_id=row.bot_id,
request_id=row.request_id,
event_type=row.event_type,
channel=row.channel,
detail=row.detail,
metadata=metadata if isinstance(metadata, dict) else {},
created_at=row.created_at.isoformat() + "Z",
).model_dump()
)
return items
def get_bot_activity_stats(session: Session) -> List[Dict[str, Any]]:
from sqlalchemy import and_, func
from models.bot import BotInstance
stmt = (
select(BotInstance.id, BotInstance.name, func.count(BotActivityEvent.id).label("count"))
.select_from(BotInstance)
.join(
BotActivityEvent,
and_(
BotActivityEvent.bot_id == BotInstance.id,
BotActivityEvent.request_id.is_not(None),
func.length(func.trim(BotActivityEvent.request_id)) > 0,
),
isouter=True,
)
.where(BotInstance.enabled.is_(True))
.group_by(BotInstance.id, BotInstance.name)
.order_by(func.count(BotActivityEvent.id).desc(), BotInstance.name.asc(), BotInstance.id.asc())
)
results = session.exec(stmt).all()
return [
{"bot_id": row[0], "name": row[1] or row[0], "count": row[2]}
for row in results
]