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,2 @@
# NATS module

View File

@@ -0,0 +1,162 @@
"""
NATS Publisher — Публікація подій до NATS
"""
import json
from typing import Dict, Any, Optional
from nats.aio.client import Client as NATS
from datetime import datetime
class NATSPublisher:
def __init__(self, nc: NATS):
self.nc = nc
async def publish(self, subject: str, payload: Dict[str, Any]) -> None:
"""
Опублікувати подію до NATS
Args:
subject: NATS subject (e.g., "agents.invoke")
payload: Дані події (dict)
"""
try:
# Додаємо timestamp, якщо не вказано
if "ts" not in payload:
payload["ts"] = datetime.utcnow().isoformat() + "Z"
# Серіалізуємо в JSON
data = json.dumps(payload).encode()
# Публікуємо
await self.nc.publish(subject, data)
print(f"📤 Published: {subject}{len(data)} bytes")
except Exception as e:
print(f"⚠️ Failed to publish {subject}: {e}")
raise
async def publish_agent_invoke(
self,
agent_id: str,
channel_id: str,
message_text: str,
user_id: Optional[str] = None,
context: Optional[Dict[str, Any]] = None
) -> None:
"""
Опублікувати подію виклику агента
Subject: agents.invoke
Payload: {
"agent_id": "agent:sofia",
"channel_id": "channel:123",
"message_text": "What are my tasks?",
"user_id": "user:456",
"context": {...}
}
"""
await self.publish("agents.invoke", {
"agent_id": agent_id,
"channel_id": channel_id,
"message_text": message_text,
"user_id": user_id,
"context": context or {}
})
async def publish_agent_reply(
self,
agent_id: str,
channel_id: str,
reply_text: str,
tokens_used: int = 0,
latency_ms: int = 0
) -> None:
"""
Опублікувати відповідь агента
Subject: agents.reply
"""
await self.publish("agents.reply", {
"agent_id": agent_id,
"channel_id": channel_id,
"reply_text": reply_text,
"tokens_used": tokens_used,
"latency_ms": latency_ms
})
async def publish_agent_error(
self,
agent_id: str,
error_type: str,
error_message: str,
context: Optional[Dict[str, Any]] = None
) -> None:
"""
Опублікувати помилку агента
Subject: agents.error
"""
await self.publish("agents.error", {
"agent_id": agent_id,
"error_type": error_type,
"error_message": error_message,
"context": context or {}
})
async def publish_agent_telemetry(
self,
agent_id: str,
metric_name: str,
metric_value: float,
tags: Optional[Dict[str, str]] = None
) -> None:
"""
Опублікувати телеметрію агента
Subject: agents.telemetry
"""
await self.publish("agents.telemetry", {
"agent_id": agent_id,
"metric_name": metric_name,
"metric_value": metric_value,
"tags": tags or {}
})
async def publish_run_created(
self,
run_id: str,
agent_id: str,
input_text: str
) -> None:
"""
Опублікувати створення run
Subject: agents.runs.created
"""
await self.publish("agents.runs.created", {
"run_id": run_id,
"agent_id": agent_id,
"input_text": input_text[:500] # Limit preview
})
async def publish_run_finished(
self,
run_id: str,
agent_id: str,
success: bool,
duration_ms: int,
tokens_used: int = 0
) -> None:
"""
Опублікувати завершення run
Subject: agents.runs.finished
"""
await self.publish("agents.runs.finished", {
"run_id": run_id,
"agent_id": agent_id,
"success": success,
"duration_ms": duration_ms,
"tokens_used": tokens_used
})

View File

@@ -0,0 +1,77 @@
"""
NATS Subject Registry — Централізований реєстр всіх NATS subjects для Agents Core
"""
# ============================================================================
# PUBLISH (Agents Service → NATS)
# ============================================================================
# Matrix integration (stub для майбутнього)
INTEGRATION_MATRIX_MESSAGE = "integration.matrix.message"
# Agent lifecycle
AGENTS_INVOKE = "agents.invoke"
AGENTS_REPLY = "agents.reply"
AGENTS_ERROR = "agents.error"
AGENTS_TELEMETRY = "agents.telemetry"
# Agent runs
AGENTS_RUNS_CREATED = "agents.runs.created"
AGENTS_RUNS_FINISHED = "agents.runs.finished"
# Agent activity (для living map)
AGENTS_ACTIVITY = "agents.activity"
# ============================================================================
# SUBSCRIBE (Agents Service ← NATS)
# ============================================================================
# Messenger events
MESSAGE_CREATED = "message.created"
MESSAGE_UPDATED = "message.updated"
MESSAGE_DELETED = "message.deleted"
# Task events
TASK_CREATED = "task.created"
TASK_UPDATED = "task.updated"
TASK_ASSIGNED = "task.assigned"
# User actions
EVENT_USER_ACTION = "event.user.action"
# Usage tracking (already subscribed in Phase 6)
USAGE_AGENT = "usage.agent"
USAGE_LLM = "usage.llm"
USAGE_TOOL = "usage.tool"
# Agent replies
AGENT_REPLY_SENT = "agent.reply.sent"
AGENT_ERROR_EVENT = "agent.error"
# ============================================================================
# Subject Patterns (wildcards)
# ============================================================================
AGENTS_ALL = "agents.*"
MESSAGE_ALL = "message.*"
USAGE_ALL = "usage.*"
EVENT_ALL = "event.*"
# ============================================================================
# Helper Functions
# ============================================================================
def get_agent_subject(agent_id: str, event_type: str) -> str:
"""
Генерує subject для конкретного агента
Example: agents.agent:sofia.invoke
"""
return f"agents.{agent_id}.{event_type}"
def get_channel_subject(channel_id: str, event_type: str) -> str:
"""
Генерує subject для конкретного каналу
Example: channel.channel:123.message.created
"""
return f"channel.{channel_id}.{event_type}"