feat: Add presence heartbeat for Matrix online status

- matrix-gateway: POST /internal/matrix/presence/online endpoint
- usePresenceHeartbeat hook with activity tracking
- Auto away after 5 min inactivity
- Offline on page close/visibility change
- Integrated in MatrixChatRoom component
This commit is contained in:
Apple
2025-11-27 00:19:40 -08:00
parent 5bed515852
commit 3de3c8cb36
6371 changed files with 1317450 additions and 932 deletions

View File

@@ -0,0 +1,239 @@
"""
Usage Data Aggregators
Queries and aggregates usage data from database
"""
import asyncpg
from datetime import datetime, timedelta
from typing import Optional, List
from models import (
UsageSummary,
ModelUsage,
AgentUsage,
ToolUsage,
UsageQueryRequest
)
class UsageAggregator:
"""Aggregates usage data for reporting"""
def __init__(self, db_pool: asyncpg.Pool):
self.db_pool = db_pool
async def get_summary(
self,
microdao_id: Optional[str] = None,
agent_id: Optional[str] = None,
period_hours: int = 24
) -> UsageSummary:
"""Get aggregated usage summary"""
period_start = datetime.utcnow() - timedelta(hours=period_hours)
period_end = datetime.utcnow()
async with self.db_pool.acquire() as conn:
# LLM stats
llm_stats = await conn.fetchrow("""
SELECT
COUNT(*) as calls,
SUM(total_tokens) as tokens_total,
SUM(prompt_tokens) as tokens_prompt,
SUM(completion_tokens) as tokens_completion,
AVG(latency_ms) as latency_avg
FROM usage_llm
WHERE timestamp >= $1 AND timestamp <= $2
AND ($3::text IS NULL OR microdao_id = $3)
AND ($4::text IS NULL OR agent_id = $4)
""", period_start, period_end, microdao_id, agent_id)
# Tool stats
tool_stats = await conn.fetchrow("""
SELECT
COUNT(*) as calls,
SUM(CASE WHEN success THEN 1 ELSE 0 END) as success,
SUM(CASE WHEN NOT success THEN 1 ELSE 0 END) as failed,
AVG(latency_ms) as latency_avg
FROM usage_tool
WHERE timestamp >= $1 AND timestamp <= $2
AND ($3::text IS NULL OR microdao_id = $3)
AND ($4::text IS NULL OR agent_id = $4)
""", period_start, period_end, microdao_id, agent_id)
# Agent stats
agent_stats = await conn.fetchrow("""
SELECT
COUNT(*) as invocations,
SUM(CASE WHEN success THEN 1 ELSE 0 END) as success,
SUM(CASE WHEN NOT success THEN 1 ELSE 0 END) as failed
FROM usage_agent
WHERE timestamp >= $1 AND timestamp <= $2
AND ($3::text IS NULL OR microdao_id = $3)
AND ($4::text IS NULL OR agent_id = $4)
""", period_start, period_end, microdao_id, agent_id)
# Message stats
message_stats = await conn.fetchrow("""
SELECT
COUNT(*) as sent,
SUM(message_length) as total_length
FROM usage_message
WHERE timestamp >= $1 AND timestamp <= $2
AND ($3::text IS NULL OR microdao_id = $3)
""", period_start, period_end, microdao_id)
return UsageSummary(
period_start=period_start,
period_end=period_end,
microdao_id=microdao_id,
agent_id=agent_id,
llm_calls_total=llm_stats['calls'] or 0,
llm_tokens_total=llm_stats['tokens_total'] or 0,
llm_tokens_prompt=llm_stats['tokens_prompt'] or 0,
llm_tokens_completion=llm_stats['tokens_completion'] or 0,
llm_latency_avg_ms=float(llm_stats['latency_avg'] or 0),
tool_calls_total=tool_stats['calls'] or 0,
tool_calls_success=tool_stats['success'] or 0,
tool_calls_failed=tool_stats['failed'] or 0,
tool_latency_avg_ms=float(tool_stats['latency_avg'] or 0),
agent_invocations_total=agent_stats['invocations'] or 0,
agent_invocations_success=agent_stats['success'] or 0,
agent_invocations_failed=agent_stats['failed'] or 0,
messages_sent=message_stats['sent'] or 0,
messages_total_length=message_stats['total_length'] or 0
)
async def get_model_breakdown(
self,
microdao_id: Optional[str] = None,
period_hours: int = 24
) -> List[ModelUsage]:
"""Get usage breakdown by model"""
period_start = datetime.utcnow() - timedelta(hours=period_hours)
period_end = datetime.utcnow()
async with self.db_pool.acquire() as conn:
rows = await conn.fetch("""
SELECT
model,
provider,
COUNT(*) as calls,
SUM(total_tokens) as tokens,
AVG(latency_ms) as latency_avg
FROM usage_llm
WHERE timestamp >= $1 AND timestamp <= $2
AND ($3::text IS NULL OR microdao_id = $3)
GROUP BY model, provider
ORDER BY tokens DESC
LIMIT 20
""", period_start, period_end, microdao_id)
return [
ModelUsage(
model=row['model'],
provider=row['provider'],
calls=row['calls'],
tokens=row['tokens'] or 0,
avg_latency_ms=float(row['latency_avg'] or 0)
)
for row in rows
]
async def get_agent_breakdown(
self,
microdao_id: Optional[str] = None,
period_hours: int = 24
) -> List[AgentUsage]:
"""Get usage breakdown by agent"""
period_start = datetime.utcnow() - timedelta(hours=period_hours)
period_end = datetime.utcnow()
async with self.db_pool.acquire() as conn:
rows = await conn.fetch("""
SELECT
a.agent_id,
COUNT(DISTINCT a.event_id) as invocations,
COALESCE(SUM(a.llm_calls), 0) as llm_calls,
COALESCE(SUM(a.tool_calls), 0) as tool_calls,
COALESCE(llm.tokens, 0) as total_tokens,
COALESCE(msg.messages, 0) as messages_sent
FROM usage_agent a
LEFT JOIN (
SELECT agent_id, SUM(total_tokens) as tokens
FROM usage_llm
WHERE timestamp >= $1 AND timestamp <= $2
AND ($3::text IS NULL OR microdao_id = $3)
GROUP BY agent_id
) llm ON llm.agent_id = a.agent_id
LEFT JOIN (
SELECT actor_id, COUNT(*) as messages
FROM usage_message
WHERE timestamp >= $1 AND timestamp <= $2
AND actor_type = 'agent'
AND ($3::text IS NULL OR microdao_id = $3)
GROUP BY actor_id
) msg ON msg.actor_id = a.agent_id
WHERE a.timestamp >= $1 AND a.timestamp <= $2
AND ($3::text IS NULL OR a.microdao_id = $3)
GROUP BY a.agent_id, llm.tokens, msg.messages
ORDER BY invocations DESC
LIMIT 20
""", period_start, period_end, microdao_id)
return [
AgentUsage(
agent_id=row['agent_id'],
invocations=row['invocations'],
llm_calls=row['llm_calls'],
tool_calls=row['tool_calls'],
messages_sent=row['messages_sent'],
total_tokens=row['total_tokens']
)
for row in rows
]
async def get_tool_breakdown(
self,
microdao_id: Optional[str] = None,
period_hours: int = 24
) -> List[ToolUsage]:
"""Get usage breakdown by tool"""
period_start = datetime.utcnow() - timedelta(hours=period_hours)
period_end = datetime.utcnow()
async with self.db_pool.acquire() as conn:
rows = await conn.fetch("""
SELECT
tool_id,
tool_name,
COUNT(*) as calls,
AVG(CASE WHEN success THEN 1.0 ELSE 0.0 END) as success_rate,
AVG(latency_ms) as latency_avg
FROM usage_tool
WHERE timestamp >= $1 AND timestamp <= $2
AND ($3::text IS NULL OR microdao_id = $3)
GROUP BY tool_id, tool_name
ORDER BY calls DESC
LIMIT 20
""", period_start, period_end, microdao_id)
return [
ToolUsage(
tool_id=row['tool_id'],
tool_name=row['tool_name'],
calls=row['calls'],
success_rate=float(row['success_rate'] or 0),
avg_latency_ms=float(row['latency_avg'] or 0)
)
for row in rows
]