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:
2
services/living-map-service/adapters/__init__.py
Normal file
2
services/living-map-service/adapters/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
||||
# Adapters for Living Map Service
|
||||
|
||||
61
services/living-map-service/adapters/agents_client.py
Normal file
61
services/living-map-service/adapters/agents_client.py
Normal file
@@ -0,0 +1,61 @@
|
||||
"""
|
||||
Agents Service Client Adapter
|
||||
Phase 9: Living Map
|
||||
"""
|
||||
from typing import List, Dict, Any
|
||||
from .base_client import BaseServiceClient
|
||||
import os
|
||||
|
||||
class AgentsClient(BaseServiceClient):
|
||||
"""Client for agents-service"""
|
||||
|
||||
def __init__(self):
|
||||
base_url = os.getenv("AGENTS_SERVICE_URL", "http://localhost:7014")
|
||||
super().__init__(base_url)
|
||||
|
||||
async def get_agents_list(self) -> List[Dict[str, Any]]:
|
||||
"""Get list of all agents"""
|
||||
result = await self.get_with_fallback("/agents", fallback=[])
|
||||
return result if isinstance(result, list) else []
|
||||
|
||||
async def get_agent_metrics_summary(self) -> Dict[str, Any]:
|
||||
"""Get aggregated agent metrics"""
|
||||
# This endpoint might not exist yet, use fallback
|
||||
result = await self.get_with_fallback("/agents/metrics/summary", fallback={
|
||||
"total_agents": 0,
|
||||
"online_agents": 0,
|
||||
"total_llm_calls_24h": 0,
|
||||
"total_tokens_24h": 0
|
||||
})
|
||||
return result
|
||||
|
||||
async def get_layer_data(self) -> Dict[str, Any]:
|
||||
"""Get agents layer data for Living Map"""
|
||||
agents_list = await self.get_agents_list()
|
||||
metrics_summary = await self.get_agent_metrics_summary()
|
||||
|
||||
# Transform to Living Map format
|
||||
items = []
|
||||
for agent in agents_list:
|
||||
items.append({
|
||||
"id": agent.get("id", agent.get("external_id", "unknown")),
|
||||
"name": agent.get("name", "Unknown Agent"),
|
||||
"kind": agent.get("kind", "assistant"),
|
||||
"microdao_id": agent.get("microdao_id"),
|
||||
"status": "online" if agent.get("is_active") else "offline",
|
||||
"usage": {
|
||||
"llm_calls_24h": 0, # TODO: Get from usage-engine
|
||||
"tokens_24h": 0,
|
||||
"messages_24h": 0
|
||||
},
|
||||
"model": agent.get("model"),
|
||||
"last_active": agent.get("updated_at")
|
||||
})
|
||||
|
||||
return {
|
||||
"items": items,
|
||||
"total_agents": len(items),
|
||||
"online_agents": sum(1 for a in items if a["status"] == "online"),
|
||||
"total_llm_calls_24h": metrics_summary.get("total_llm_calls_24h", 0)
|
||||
}
|
||||
|
||||
51
services/living-map-service/adapters/base_client.py
Normal file
51
services/living-map-service/adapters/base_client.py
Normal file
@@ -0,0 +1,51 @@
|
||||
"""
|
||||
Base HTTP Client for service adapters
|
||||
Phase 9: Living Map
|
||||
"""
|
||||
import httpx
|
||||
from typing import Optional, Any
|
||||
import asyncio
|
||||
|
||||
class BaseServiceClient:
|
||||
"""Base client with timeout and retry logic"""
|
||||
|
||||
def __init__(self, base_url: str, timeout: float = 5.0):
|
||||
self.base_url = base_url.rstrip('/')
|
||||
self.timeout = timeout
|
||||
|
||||
async def get(
|
||||
self,
|
||||
path: str,
|
||||
params: Optional[dict] = None,
|
||||
headers: Optional[dict] = None
|
||||
) -> Optional[Any]:
|
||||
"""GET request with error handling"""
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=self.timeout) as client:
|
||||
response = await client.get(
|
||||
f"{self.base_url}{path}",
|
||||
params=params,
|
||||
headers=headers or {}
|
||||
)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
except httpx.TimeoutException:
|
||||
print(f"⚠️ Timeout calling {self.base_url}{path}")
|
||||
return None
|
||||
except httpx.HTTPError as e:
|
||||
print(f"⚠️ HTTP error calling {self.base_url}{path}: {e}")
|
||||
return None
|
||||
except Exception as e:
|
||||
print(f"⚠️ Error calling {self.base_url}{path}: {e}")
|
||||
return None
|
||||
|
||||
async def get_with_fallback(
|
||||
self,
|
||||
path: str,
|
||||
fallback: Any,
|
||||
params: Optional[dict] = None
|
||||
) -> Any:
|
||||
"""GET with fallback value if fails"""
|
||||
result = await self.get(path, params)
|
||||
return result if result is not None else fallback
|
||||
|
||||
34
services/living-map-service/adapters/city_client.py
Normal file
34
services/living-map-service/adapters/city_client.py
Normal file
@@ -0,0 +1,34 @@
|
||||
"""
|
||||
City Service Client Adapter
|
||||
Phase 9: Living Map
|
||||
"""
|
||||
from typing import Dict, Any
|
||||
from .base_client import BaseServiceClient
|
||||
import os
|
||||
|
||||
class CityClient(BaseServiceClient):
|
||||
"""Client for city-service"""
|
||||
|
||||
def __init__(self):
|
||||
base_url = os.getenv("CITY_SERVICE_URL", "http://localhost:7001")
|
||||
super().__init__(base_url)
|
||||
|
||||
async def get_city_snapshot(self) -> Dict[str, Any]:
|
||||
"""Get city snapshot"""
|
||||
result = await self.get_with_fallback("/api/city/snapshot", fallback={})
|
||||
return result
|
||||
|
||||
async def get_layer_data(self) -> Dict[str, Any]:
|
||||
"""Get city layer data for Living Map"""
|
||||
snapshot = await self.get_city_snapshot()
|
||||
|
||||
# Return as-is or transform if needed
|
||||
# This is a placeholder - actual structure depends on city-service
|
||||
return snapshot if snapshot else {
|
||||
"microdaos_total": 0,
|
||||
"active_users": 0,
|
||||
"active_agents": 0,
|
||||
"health": "unknown",
|
||||
"items": []
|
||||
}
|
||||
|
||||
50
services/living-map-service/adapters/dao_client.py
Normal file
50
services/living-map-service/adapters/dao_client.py
Normal file
@@ -0,0 +1,50 @@
|
||||
"""
|
||||
DAO Service Client Adapter
|
||||
Phase 9: Living Map
|
||||
"""
|
||||
from typing import List, Dict, Any
|
||||
from .base_client import BaseServiceClient
|
||||
import os
|
||||
|
||||
class DaoClient(BaseServiceClient):
|
||||
"""Client for dao-service"""
|
||||
|
||||
def __init__(self):
|
||||
base_url = os.getenv("DAO_SERVICE_URL", "http://localhost:7016")
|
||||
super().__init__(base_url)
|
||||
|
||||
async def get_daos_list(self) -> List[Dict[str, Any]]:
|
||||
"""Get list of all DAOs"""
|
||||
result = await self.get_with_fallback("/dao", fallback=[])
|
||||
return result if isinstance(result, list) else []
|
||||
|
||||
async def get_proposals_summary(self) -> Dict[str, Any]:
|
||||
"""Get proposals summary across all DAOs"""
|
||||
# This endpoint might not exist, return placeholder
|
||||
return {
|
||||
"total_proposals": 0,
|
||||
"active_proposals": 0
|
||||
}
|
||||
|
||||
async def get_layer_data(self) -> Dict[str, Any]:
|
||||
"""Get space layer data for Living Map (DAO planets)"""
|
||||
daos_list = await self.get_daos_list()
|
||||
|
||||
# Transform to Living Map format (DAOs as planets)
|
||||
planets = []
|
||||
for dao in daos_list:
|
||||
planets.append({
|
||||
"id": f"dao:{dao.get('slug', dao.get('id'))}",
|
||||
"name": dao.get("name", "Unknown DAO"),
|
||||
"type": "dao",
|
||||
"status": "active" if dao.get("is_active") else "inactive",
|
||||
"orbits": [], # TODO: Link nodes to DAOs
|
||||
"treasury_value": None,
|
||||
"active_proposals": 0
|
||||
})
|
||||
|
||||
return {
|
||||
"planets": planets,
|
||||
"nodes": [] # Nodes will be added from space-service
|
||||
}
|
||||
|
||||
61
services/living-map-service/adapters/microdao_client.py
Normal file
61
services/living-map-service/adapters/microdao_client.py
Normal file
@@ -0,0 +1,61 @@
|
||||
"""
|
||||
MicroDAO Service Client Adapter
|
||||
Phase 9: Living Map
|
||||
"""
|
||||
from typing import List, Dict, Any
|
||||
from .base_client import BaseServiceClient
|
||||
import os
|
||||
|
||||
class MicrodaoClient(BaseServiceClient):
|
||||
"""Client for microdao-service"""
|
||||
|
||||
def __init__(self):
|
||||
base_url = os.getenv("MICRODAO_SERVICE_URL", "http://localhost:7015")
|
||||
super().__init__(base_url)
|
||||
|
||||
async def get_microdaos_list(self) -> List[Dict[str, Any]]:
|
||||
"""Get list of all microDAOs"""
|
||||
# Note: This endpoint might require auth, using internal endpoint if available
|
||||
result = await self.get_with_fallback("/internal/microdaos", fallback=[])
|
||||
if not result:
|
||||
result = await self.get_with_fallback("/microdao", fallback=[])
|
||||
return result if isinstance(result, list) else []
|
||||
|
||||
async def get_layer_data(self) -> Dict[str, Any]:
|
||||
"""Get city layer data for Living Map"""
|
||||
microdaos_list = await self.get_microdaos_list()
|
||||
|
||||
# Transform to Living Map format
|
||||
items = []
|
||||
total_agents = 0
|
||||
total_nodes = 0
|
||||
total_members = 0
|
||||
|
||||
for microdao in microdaos_list:
|
||||
agents_count = microdao.get("agent_count", 0)
|
||||
nodes_count = microdao.get("node_count", 0)
|
||||
members_count = microdao.get("member_count", 0)
|
||||
|
||||
total_agents += agents_count
|
||||
total_nodes += nodes_count
|
||||
total_members += members_count
|
||||
|
||||
items.append({
|
||||
"id": microdao.get("external_id", f"microdao:{microdao.get('id')}"),
|
||||
"slug": microdao.get("slug", "unknown"),
|
||||
"name": microdao.get("name", "Unknown microDAO"),
|
||||
"status": "active" if microdao.get("is_active") else "inactive",
|
||||
"agents": agents_count,
|
||||
"nodes": nodes_count,
|
||||
"members": members_count,
|
||||
"description": microdao.get("description")
|
||||
})
|
||||
|
||||
return {
|
||||
"microdaos_total": len(items),
|
||||
"active_users": total_members,
|
||||
"active_agents": total_agents,
|
||||
"health": "green" if len(items) > 0 else "yellow",
|
||||
"items": items
|
||||
}
|
||||
|
||||
34
services/living-map-service/adapters/space_client.py
Normal file
34
services/living-map-service/adapters/space_client.py
Normal file
@@ -0,0 +1,34 @@
|
||||
"""
|
||||
Space Service Client Adapter
|
||||
Phase 9: Living Map
|
||||
"""
|
||||
from typing import Dict, Any
|
||||
from .base_client import BaseServiceClient
|
||||
import os
|
||||
|
||||
class SpaceClient(BaseServiceClient):
|
||||
"""Client for space-service"""
|
||||
|
||||
def __init__(self):
|
||||
base_url = os.getenv("SPACE_SERVICE_URL", "http://localhost:7002")
|
||||
super().__init__(base_url)
|
||||
|
||||
async def get_space_scene(self) -> Dict[str, Any]:
|
||||
"""Get space scene data"""
|
||||
result = await self.get_with_fallback("/api/space/scene", fallback={})
|
||||
return result
|
||||
|
||||
async def get_layer_data(self) -> Dict[str, Any]:
|
||||
"""Get space layer data for Living Map"""
|
||||
scene = await self.get_space_scene()
|
||||
|
||||
# Extract planets and nodes from scene
|
||||
# Format depends on space-service implementation
|
||||
planets = scene.get("planets", [])
|
||||
nodes = scene.get("nodes", [])
|
||||
|
||||
return {
|
||||
"planets": planets,
|
||||
"nodes": nodes
|
||||
}
|
||||
|
||||
40
services/living-map-service/adapters/usage_client.py
Normal file
40
services/living-map-service/adapters/usage_client.py
Normal file
@@ -0,0 +1,40 @@
|
||||
"""
|
||||
Usage Engine Client Adapter
|
||||
Phase 9: Living Map
|
||||
"""
|
||||
from typing import Dict, Any
|
||||
from .base_client import BaseServiceClient
|
||||
import os
|
||||
|
||||
class UsageClient(BaseServiceClient):
|
||||
"""Client for usage-engine"""
|
||||
|
||||
def __init__(self):
|
||||
base_url = os.getenv("USAGE_ENGINE_URL", "http://localhost:7013")
|
||||
super().__init__(base_url)
|
||||
|
||||
async def get_usage_summary(self, period_hours: int = 24) -> Dict[str, Any]:
|
||||
"""Get usage summary for period"""
|
||||
result = await self.get_with_fallback(
|
||||
"/internal/usage/summary",
|
||||
fallback={
|
||||
"total_llm_calls": 0,
|
||||
"total_tokens": 0,
|
||||
"period_hours": period_hours
|
||||
},
|
||||
params={"period_hours": period_hours}
|
||||
)
|
||||
return result
|
||||
|
||||
async def get_agent_usage(self, agent_id: str) -> Dict[str, Any]:
|
||||
"""Get usage for specific agent"""
|
||||
result = await self.get_with_fallback(
|
||||
f"/internal/usage/agent/{agent_id}",
|
||||
fallback={
|
||||
"llm_calls_24h": 0,
|
||||
"tokens_24h": 0,
|
||||
"messages_24h": 0
|
||||
}
|
||||
)
|
||||
return result
|
||||
|
||||
Reference in New Issue
Block a user