feat(city-map): Add 2D City Map with coordinates and agent presence
- Add migration 013_city_map_coordinates.sql with map coordinates, zones, and agents table - Add /city/map API endpoint in city-service - Add /city/agents and /city/agents/online endpoints - Extend presence aggregator to include agents[] in snapshot - Add AgentsSource for fetching agent data from DB - Create CityMap component with interactive room tiles - Add useCityMap hook for fetching map data - Update useGlobalPresence to include agents - Add map/list view toggle on /city page - Add agent badges to room cards and map tiles
This commit is contained in:
@@ -22,3 +22,4 @@ CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "7005"]
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -292,3 +292,4 @@ curl http://localhost:7004/internal/messaging/channels/{channel_id}/context
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -16,3 +16,4 @@ rules:
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -160,3 +160,4 @@ async def shutdown_event():
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -36,3 +36,4 @@ class FilterContext(BaseModel):
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -8,3 +8,4 @@ PyYAML==6.0.1
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -115,3 +115,4 @@ class FilterRules:
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -22,3 +22,4 @@ CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "7006"]
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -405,3 +405,4 @@ curl -X POST http://localhost:7006/internal/agent-runtime/test-channel \
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -21,3 +21,4 @@ memory:
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -72,3 +72,4 @@ async def post_message(agent_id: str, channel_id: str, text: str) -> bool:
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -35,3 +35,4 @@ class LLMResponse(BaseModel):
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -73,3 +73,4 @@ pep_client = PEPClient()
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -8,3 +8,4 @@ PyYAML==6.0.1
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -172,3 +172,4 @@ Connects to:
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -218,3 +218,4 @@ docker run -p 7011:7011 \
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -127,3 +127,4 @@ async def require_actor(
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,32 +1,69 @@
|
||||
"""
|
||||
Auth Service Configuration
|
||||
"""
|
||||
from pydantic_settings import BaseSettings
|
||||
from functools import lru_cache
|
||||
|
||||
from pydantic import AliasChoices, Field
|
||||
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||
|
||||
|
||||
class Settings(BaseSettings):
|
||||
"""
|
||||
Settings loader that supports both the new AUTH_* env vars and the legacy
|
||||
ones used in docker-compose files (e.g. DATABASE_URL, JWT_SECRET).
|
||||
"""
|
||||
|
||||
model_config = SettingsConfigDict(env_file=".env", extra="ignore")
|
||||
|
||||
# Service
|
||||
service_name: str = "auth-service"
|
||||
service_version: str = "1.0.0"
|
||||
port: int = 7020
|
||||
debug: bool = False
|
||||
service_name: str = Field(
|
||||
default="auth-service",
|
||||
validation_alias=AliasChoices("AUTH_SERVICE_NAME", "SERVICE_NAME"),
|
||||
)
|
||||
service_version: str = Field(
|
||||
default="1.0.0",
|
||||
validation_alias=AliasChoices("AUTH_SERVICE_VERSION", "SERVICE_VERSION"),
|
||||
)
|
||||
port: int = Field(
|
||||
default=7020,
|
||||
validation_alias=AliasChoices("AUTH_PORT", "PORT", "AUTH_SERVICE_PORT"),
|
||||
)
|
||||
debug: bool = Field(
|
||||
default=False,
|
||||
validation_alias=AliasChoices("AUTH_DEBUG", "DEBUG"),
|
||||
)
|
||||
|
||||
# Database
|
||||
database_url: str = "postgresql://postgres:postgres@localhost:5432/daarion"
|
||||
database_url: str = Field(
|
||||
default="postgresql://postgres:postgres@localhost:5432/daarion",
|
||||
validation_alias=AliasChoices("AUTH_DATABASE_URL", "DATABASE_URL"),
|
||||
)
|
||||
|
||||
# JWT
|
||||
jwt_secret: str = "your-very-long-secret-key-change-in-production"
|
||||
jwt_algorithm: str = "HS256"
|
||||
access_token_ttl: int = 1800 # 30 minutes
|
||||
refresh_token_ttl: int = 604800 # 7 days
|
||||
jwt_secret: str = Field(
|
||||
default="your-very-long-secret-key-change-in-production",
|
||||
validation_alias=AliasChoices("AUTH_JWT_SECRET", "JWT_SECRET"),
|
||||
)
|
||||
jwt_algorithm: str = Field(
|
||||
default="HS256",
|
||||
validation_alias=AliasChoices(
|
||||
"AUTH_JWT_ALGORITHM", "JWT_ALGO", "JWT_ALGORITHM"
|
||||
),
|
||||
)
|
||||
access_token_ttl: int = Field(
|
||||
default=1800,
|
||||
validation_alias=AliasChoices("AUTH_ACCESS_TOKEN_TTL", "ACCESS_TOKEN_TTL"),
|
||||
)
|
||||
refresh_token_ttl: int = Field(
|
||||
default=604800,
|
||||
validation_alias=AliasChoices("AUTH_REFRESH_TOKEN_TTL", "REFRESH_TOKEN_TTL"),
|
||||
)
|
||||
|
||||
# Security
|
||||
bcrypt_rounds: int = 12
|
||||
|
||||
class Config:
|
||||
env_prefix = "AUTH_"
|
||||
env_file = ".env"
|
||||
bcrypt_rounds: int = Field(
|
||||
default=12,
|
||||
validation_alias=AliasChoices("AUTH_BCRYPT_ROUNDS", "BCRYPT_ROUNDS"),
|
||||
)
|
||||
|
||||
|
||||
@lru_cache()
|
||||
|
||||
@@ -228,3 +228,4 @@ class PasskeyStore:
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -125,3 +125,4 @@ async def delete_api_key(
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -327,3 +327,4 @@ async def authenticate_finish(
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -127,3 +127,4 @@ async def logout(
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -207,3 +207,4 @@ def hash_credential_id(credential_id: str) -> str:
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -22,3 +22,4 @@ CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "7001"]
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -345,3 +345,4 @@ Proprietary — DAARION Ecosystem
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -107,3 +107,66 @@ class WSPresenceMessage(BaseModel):
|
||||
user_id: str
|
||||
status: Optional[str] = None
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# City Map (2D Map)
|
||||
# =============================================================================
|
||||
|
||||
class CityMapRoom(BaseModel):
|
||||
"""Room representation on 2D city map"""
|
||||
id: str
|
||||
slug: str
|
||||
name: str
|
||||
description: Optional[str] = None
|
||||
room_type: str = "public"
|
||||
zone: str = "central"
|
||||
icon: Optional[str] = None
|
||||
color: Optional[str] = None
|
||||
# Map coordinates
|
||||
x: int = 0
|
||||
y: int = 0
|
||||
w: int = 1
|
||||
h: int = 1
|
||||
# Matrix integration
|
||||
matrix_room_id: Optional[str] = None
|
||||
|
||||
|
||||
class CityMapConfig(BaseModel):
|
||||
"""Global city map configuration"""
|
||||
grid_width: int = 6
|
||||
grid_height: int = 3
|
||||
cell_size: int = 100
|
||||
background_url: Optional[str] = None
|
||||
|
||||
|
||||
class CityMapResponse(BaseModel):
|
||||
"""Full city map response"""
|
||||
config: CityMapConfig
|
||||
rooms: List[CityMapRoom]
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Agents (for Agent Presence)
|
||||
# =============================================================================
|
||||
|
||||
class AgentRead(BaseModel):
|
||||
"""Agent representation"""
|
||||
id: str
|
||||
display_name: str
|
||||
kind: str = "assistant" # assistant, civic, oracle, builder
|
||||
avatar_url: Optional[str] = None
|
||||
color: str = "cyan"
|
||||
status: str = "offline" # online, offline, busy
|
||||
current_room_id: Optional[str] = None
|
||||
capabilities: List[str] = []
|
||||
|
||||
|
||||
class AgentPresence(BaseModel):
|
||||
"""Agent presence in a room"""
|
||||
agent_id: str
|
||||
display_name: str
|
||||
kind: str
|
||||
status: str
|
||||
room_id: Optional[str] = None
|
||||
color: Optional[str] = None
|
||||
|
||||
|
||||
@@ -226,3 +226,124 @@ async def create_feed_event(
|
||||
row = await pool.fetchrow(query, event_id, kind, room_id, user_id, agent_id, payload_json)
|
||||
return dict(row)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# City Map Repository
|
||||
# =============================================================================
|
||||
|
||||
async def get_map_config() -> dict:
|
||||
"""Отримати конфігурацію мапи міста"""
|
||||
pool = await get_pool()
|
||||
|
||||
query = """
|
||||
SELECT id, grid_width, grid_height, cell_size, background_url, updated_at
|
||||
FROM city_map_config
|
||||
WHERE id = 'default'
|
||||
"""
|
||||
|
||||
row = await pool.fetchrow(query)
|
||||
if row:
|
||||
return dict(row)
|
||||
|
||||
# Повернути дефолтні значення якщо немає запису
|
||||
return {
|
||||
"id": "default",
|
||||
"grid_width": 6,
|
||||
"grid_height": 3,
|
||||
"cell_size": 100,
|
||||
"background_url": None
|
||||
}
|
||||
|
||||
|
||||
async def get_rooms_for_map() -> List[dict]:
|
||||
"""Отримати кімнати з координатами для 2D мапи"""
|
||||
pool = await get_pool()
|
||||
|
||||
query = """
|
||||
SELECT
|
||||
id, slug, name, description,
|
||||
room_type, zone, icon, color,
|
||||
map_x, map_y, map_w, map_h,
|
||||
matrix_room_id
|
||||
FROM city_rooms
|
||||
ORDER BY map_y, map_x
|
||||
"""
|
||||
|
||||
rows = await pool.fetch(query)
|
||||
return [dict(row) for row in rows]
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Agents Repository
|
||||
# =============================================================================
|
||||
|
||||
async def get_all_agents() -> List[dict]:
|
||||
"""Отримати всіх агентів"""
|
||||
pool = await get_pool()
|
||||
|
||||
query = """
|
||||
SELECT id, display_name, kind, avatar_url, color, status,
|
||||
current_room_id, capabilities, created_at, updated_at
|
||||
FROM agents
|
||||
ORDER BY display_name
|
||||
"""
|
||||
|
||||
rows = await pool.fetch(query)
|
||||
return [dict(row) for row in rows]
|
||||
|
||||
|
||||
async def get_agents_by_room(room_id: str) -> List[dict]:
|
||||
"""Отримати агентів у конкретній кімнаті"""
|
||||
pool = await get_pool()
|
||||
|
||||
query = """
|
||||
SELECT id, display_name, kind, avatar_url, color, status,
|
||||
current_room_id, capabilities
|
||||
FROM agents
|
||||
WHERE current_room_id = $1 AND status != 'offline'
|
||||
ORDER BY display_name
|
||||
"""
|
||||
|
||||
rows = await pool.fetch(query, room_id)
|
||||
return [dict(row) for row in rows]
|
||||
|
||||
|
||||
async def get_online_agents() -> List[dict]:
|
||||
"""Отримати всіх онлайн агентів"""
|
||||
pool = await get_pool()
|
||||
|
||||
query = """
|
||||
SELECT id, display_name, kind, avatar_url, color, status,
|
||||
current_room_id, capabilities
|
||||
FROM agents
|
||||
WHERE status IN ('online', 'busy')
|
||||
ORDER BY display_name
|
||||
"""
|
||||
|
||||
rows = await pool.fetch(query)
|
||||
return [dict(row) for row in rows]
|
||||
|
||||
|
||||
async def update_agent_status(agent_id: str, status: str, room_id: Optional[str] = None) -> Optional[dict]:
|
||||
"""Оновити статус агента"""
|
||||
pool = await get_pool()
|
||||
|
||||
if room_id:
|
||||
query = """
|
||||
UPDATE agents
|
||||
SET status = $2, current_room_id = $3, updated_at = NOW()
|
||||
WHERE id = $1
|
||||
RETURNING id, display_name, kind, status, current_room_id
|
||||
"""
|
||||
row = await pool.fetchrow(query, agent_id, status, room_id)
|
||||
else:
|
||||
query = """
|
||||
UPDATE agents
|
||||
SET status = $2, updated_at = NOW()
|
||||
WHERE id = $1
|
||||
RETURNING id, display_name, kind, status, current_room_id
|
||||
"""
|
||||
row = await pool.fetchrow(query, agent_id, status)
|
||||
|
||||
return dict(row) if row else None
|
||||
|
||||
|
||||
@@ -14,7 +14,12 @@ from models_city import (
|
||||
CityRoomDetail,
|
||||
CityRoomMessageRead,
|
||||
CityRoomMessageCreate,
|
||||
CityFeedEventRead
|
||||
CityFeedEventRead,
|
||||
CityMapRoom,
|
||||
CityMapConfig,
|
||||
CityMapResponse,
|
||||
AgentRead,
|
||||
AgentPresence
|
||||
)
|
||||
import repo_city
|
||||
from common.redis_client import PresenceRedis, get_redis
|
||||
@@ -412,3 +417,143 @@ async def get_city_feed(limit: int = 20, offset: int = 0):
|
||||
logger.error(f"Failed to get city feed: {e}")
|
||||
raise HTTPException(status_code=500, detail="Failed to get city feed")
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# City Map API (2D Map)
|
||||
# =============================================================================
|
||||
|
||||
@router.get("/map", response_model=CityMapResponse)
|
||||
async def get_city_map():
|
||||
"""
|
||||
Отримати дані для 2D мапи міста.
|
||||
|
||||
Повертає:
|
||||
- config: розміри сітки та налаштування
|
||||
- rooms: список кімнат з координатами
|
||||
"""
|
||||
try:
|
||||
# Отримати конфігурацію
|
||||
config_data = await repo_city.get_map_config()
|
||||
config = CityMapConfig(
|
||||
grid_width=config_data.get("grid_width", 6),
|
||||
grid_height=config_data.get("grid_height", 3),
|
||||
cell_size=config_data.get("cell_size", 100),
|
||||
background_url=config_data.get("background_url")
|
||||
)
|
||||
|
||||
# Отримати кімнати з координатами
|
||||
rooms_data = await repo_city.get_rooms_for_map()
|
||||
rooms = []
|
||||
|
||||
for room in rooms_data:
|
||||
rooms.append(CityMapRoom(
|
||||
id=room["id"],
|
||||
slug=room["slug"],
|
||||
name=room["name"],
|
||||
description=room.get("description"),
|
||||
room_type=room.get("room_type", "public"),
|
||||
zone=room.get("zone", "central"),
|
||||
icon=room.get("icon"),
|
||||
color=room.get("color"),
|
||||
x=room.get("map_x", 0),
|
||||
y=room.get("map_y", 0),
|
||||
w=room.get("map_w", 1),
|
||||
h=room.get("map_h", 1),
|
||||
matrix_room_id=room.get("matrix_room_id")
|
||||
))
|
||||
|
||||
return CityMapResponse(config=config, rooms=rooms)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get city map: {e}")
|
||||
raise HTTPException(status_code=500, detail="Failed to get city map")
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Agents API
|
||||
# =============================================================================
|
||||
|
||||
@router.get("/agents", response_model=List[AgentRead])
|
||||
async def get_agents():
|
||||
"""
|
||||
Отримати список всіх агентів
|
||||
"""
|
||||
try:
|
||||
agents = await repo_city.get_all_agents()
|
||||
result = []
|
||||
|
||||
for agent in agents:
|
||||
capabilities = agent.get("capabilities", [])
|
||||
if isinstance(capabilities, str):
|
||||
import json
|
||||
capabilities = json.loads(capabilities)
|
||||
|
||||
result.append(AgentRead(
|
||||
id=agent["id"],
|
||||
display_name=agent["display_name"],
|
||||
kind=agent.get("kind", "assistant"),
|
||||
avatar_url=agent.get("avatar_url"),
|
||||
color=agent.get("color", "cyan"),
|
||||
status=agent.get("status", "offline"),
|
||||
current_room_id=agent.get("current_room_id"),
|
||||
capabilities=capabilities
|
||||
))
|
||||
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get agents: {e}")
|
||||
raise HTTPException(status_code=500, detail="Failed to get agents")
|
||||
|
||||
|
||||
@router.get("/agents/online", response_model=List[AgentPresence])
|
||||
async def get_online_agents():
|
||||
"""
|
||||
Отримати список онлайн агентів (для presence)
|
||||
"""
|
||||
try:
|
||||
agents = await repo_city.get_online_agents()
|
||||
result = []
|
||||
|
||||
for agent in agents:
|
||||
result.append(AgentPresence(
|
||||
agent_id=agent["id"],
|
||||
display_name=agent["display_name"],
|
||||
kind=agent.get("kind", "assistant"),
|
||||
status=agent.get("status", "offline"),
|
||||
room_id=agent.get("current_room_id"),
|
||||
color=agent.get("color", "cyan")
|
||||
))
|
||||
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get online agents: {e}")
|
||||
raise HTTPException(status_code=500, detail="Failed to get online agents")
|
||||
|
||||
|
||||
@router.get("/rooms/{room_id}/agents", response_model=List[AgentPresence])
|
||||
async def get_room_agents(room_id: str):
|
||||
"""
|
||||
Отримати агентів у конкретній кімнаті
|
||||
"""
|
||||
try:
|
||||
agents = await repo_city.get_agents_by_room(room_id)
|
||||
result = []
|
||||
|
||||
for agent in agents:
|
||||
result.append(AgentPresence(
|
||||
agent_id=agent["id"],
|
||||
display_name=agent["display_name"],
|
||||
kind=agent.get("kind", "assistant"),
|
||||
status=agent.get("status", "offline"),
|
||||
room_id=room_id,
|
||||
color=agent.get("color", "cyan")
|
||||
))
|
||||
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get room agents: {e}")
|
||||
raise HTTPException(status_code=500, detail="Failed to get room agents")
|
||||
|
||||
|
||||
@@ -160,3 +160,4 @@ async def agents_presence_generator():
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -22,3 +22,4 @@ CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "7007"]
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -332,3 +332,4 @@ Internal DAARION service
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -58,3 +58,4 @@ logging:
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -193,3 +193,4 @@ if __name__ == "__main__":
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -100,3 +100,4 @@ class UsageTracker:
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -59,3 +59,4 @@ class UsageLog(BaseModel):
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -8,3 +8,4 @@ __all__ = ['BaseProvider', 'OpenAIProvider', 'DeepSeekProvider', 'LocalProvider'
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -34,3 +34,4 @@ class BaseProvider(Protocol):
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -73,3 +73,4 @@ class DeepSeekProvider:
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -95,3 +95,4 @@ class LocalProvider:
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -73,3 +73,4 @@ class OpenAIProvider:
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -8,3 +8,4 @@ python-multipart==0.0.6
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -73,3 +73,4 @@ class ModelRouter:
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -644,3 +644,4 @@ Content-Type: application/json
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -15,3 +15,4 @@ COPY app ./app
|
||||
# Run the service
|
||||
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8085"]
|
||||
|
||||
|
||||
|
||||
92
services/matrix-presence-aggregator/app/agents_source.py
Normal file
92
services/matrix-presence-aggregator/app/agents_source.py
Normal file
@@ -0,0 +1,92 @@
|
||||
"""Agents data source from PostgreSQL"""
|
||||
from sqlalchemy import create_engine, text
|
||||
from typing import List, Dict
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class AgentsSource:
|
||||
"""Fetches agent data from PostgreSQL"""
|
||||
|
||||
def __init__(self, db_dsn: str):
|
||||
self.engine = create_engine(db_dsn)
|
||||
|
||||
def get_online_agents(self) -> List[Dict]:
|
||||
"""
|
||||
Get all online/busy agents.
|
||||
|
||||
Returns list of dicts with:
|
||||
- agent_id
|
||||
- display_name
|
||||
- kind
|
||||
- status
|
||||
- room_id (current_room_id)
|
||||
- color
|
||||
"""
|
||||
query = text("""
|
||||
SELECT
|
||||
id as agent_id,
|
||||
display_name,
|
||||
kind,
|
||||
status,
|
||||
current_room_id as room_id,
|
||||
color
|
||||
FROM agents
|
||||
WHERE status IN ('online', 'busy')
|
||||
ORDER BY display_name
|
||||
""")
|
||||
|
||||
try:
|
||||
with self.engine.connect() as conn:
|
||||
rows = conn.execute(query).mappings().all()
|
||||
return [dict(r) for r in rows]
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get online agents: {e}")
|
||||
return []
|
||||
|
||||
def get_agents_by_room(self, room_id: str) -> List[Dict]:
|
||||
"""Get agents in a specific room"""
|
||||
query = text("""
|
||||
SELECT
|
||||
id as agent_id,
|
||||
display_name,
|
||||
kind,
|
||||
status,
|
||||
current_room_id as room_id,
|
||||
color
|
||||
FROM agents
|
||||
WHERE current_room_id = :room_id AND status != 'offline'
|
||||
ORDER BY display_name
|
||||
""")
|
||||
|
||||
try:
|
||||
with self.engine.connect() as conn:
|
||||
rows = conn.execute(query, {"room_id": room_id}).mappings().all()
|
||||
return [dict(r) for r in rows]
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get agents for room {room_id}: {e}")
|
||||
return []
|
||||
|
||||
def get_all_agents(self) -> List[Dict]:
|
||||
"""Get all agents (including offline)"""
|
||||
query = text("""
|
||||
SELECT
|
||||
id as agent_id,
|
||||
display_name,
|
||||
kind,
|
||||
status,
|
||||
current_room_id as room_id,
|
||||
color
|
||||
FROM agents
|
||||
ORDER BY display_name
|
||||
""")
|
||||
|
||||
try:
|
||||
with self.engine.connect() as conn:
|
||||
rows = conn.execute(query).mappings().all()
|
||||
return [dict(r) for r in rows]
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get all agents: {e}")
|
||||
return []
|
||||
|
||||
@@ -4,9 +4,10 @@ from datetime import datetime, timezone
|
||||
from typing import List, Optional
|
||||
import logging
|
||||
|
||||
from .models import PresenceSnapshot, RoomPresence, CityPresence
|
||||
from .models import PresenceSnapshot, RoomPresence, CityPresence, AgentPresence
|
||||
from .matrix_client import MatrixClient
|
||||
from .rooms_source import RoomsSource
|
||||
from .agents_source import AgentsSource
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -16,6 +17,7 @@ class PresenceAggregator:
|
||||
Aggregates presence data from Matrix and broadcasts to subscribers.
|
||||
|
||||
- Periodically polls Matrix for room members and presence
|
||||
- Fetches agent status from database
|
||||
- Caches the latest snapshot
|
||||
- Broadcasts updates to SSE subscribers
|
||||
"""
|
||||
@@ -24,10 +26,12 @@ class PresenceAggregator:
|
||||
self,
|
||||
matrix_client: MatrixClient,
|
||||
rooms_source: RoomsSource,
|
||||
agents_source: Optional[AgentsSource] = None,
|
||||
poll_interval_seconds: int = 5,
|
||||
):
|
||||
self.matrix_client = matrix_client
|
||||
self.rooms_source = rooms_source
|
||||
self.agents_source = agents_source
|
||||
self.poll_interval_seconds = poll_interval_seconds
|
||||
|
||||
self._snapshot: Optional[PresenceSnapshot] = None
|
||||
@@ -62,18 +66,47 @@ class PresenceAggregator:
|
||||
pass
|
||||
|
||||
async def _compute_snapshot(self) -> PresenceSnapshot:
|
||||
"""Compute a new presence snapshot from Matrix"""
|
||||
"""Compute a new presence snapshot from Matrix and agents DB"""
|
||||
rooms = self.rooms_source.get_rooms()
|
||||
|
||||
if not rooms:
|
||||
logger.warning("No rooms with matrix_room_id found")
|
||||
|
||||
# Fetch agents from database
|
||||
all_agents: List[AgentPresence] = []
|
||||
agents_by_room: dict = {}
|
||||
|
||||
if self.agents_source:
|
||||
try:
|
||||
online_agents = self.agents_source.get_online_agents()
|
||||
for agent in online_agents:
|
||||
ap = AgentPresence(
|
||||
agent_id=agent["agent_id"],
|
||||
display_name=agent["display_name"],
|
||||
kind=agent.get("kind", "assistant"),
|
||||
status=agent.get("status", "online"),
|
||||
room_id=agent.get("room_id"),
|
||||
color=agent.get("color", "cyan")
|
||||
)
|
||||
all_agents.append(ap)
|
||||
|
||||
# Group by room
|
||||
room_id = agent.get("room_id")
|
||||
if room_id:
|
||||
if room_id not in agents_by_room:
|
||||
agents_by_room[room_id] = []
|
||||
agents_by_room[room_id].append(ap)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error fetching agents: {e}")
|
||||
|
||||
room_presences: List[RoomPresence] = []
|
||||
city_online_total = 0
|
||||
rooms_online = 0
|
||||
|
||||
for r in rooms:
|
||||
matrix_room_id = r["matrix_room_id"]
|
||||
room_id = r["room_id"]
|
||||
|
||||
try:
|
||||
# Get room members
|
||||
@@ -99,24 +132,30 @@ class PresenceAggregator:
|
||||
|
||||
city_online_total += online_count
|
||||
|
||||
# Get agents for this room
|
||||
room_agents = agents_by_room.get(room_id, [])
|
||||
|
||||
room_presences.append(
|
||||
RoomPresence(
|
||||
room_id=r["room_id"],
|
||||
room_id=room_id,
|
||||
matrix_room_id=matrix_room_id,
|
||||
online=online_count,
|
||||
typing=typing_count,
|
||||
agents=room_agents,
|
||||
)
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error processing room {r['room_id']}: {e}")
|
||||
# Add room with 0 online
|
||||
logger.error(f"Error processing room {room_id}: {e}")
|
||||
# Add room with 0 online but include agents
|
||||
room_agents = agents_by_room.get(room_id, [])
|
||||
room_presences.append(
|
||||
RoomPresence(
|
||||
room_id=r["room_id"],
|
||||
room_id=room_id,
|
||||
matrix_room_id=matrix_room_id,
|
||||
online=0,
|
||||
typing=0,
|
||||
agents=room_agents,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -125,11 +164,13 @@ class PresenceAggregator:
|
||||
city=CityPresence(
|
||||
online_total=city_online_total,
|
||||
rooms_online=rooms_online,
|
||||
agents_online=len(all_agents),
|
||||
),
|
||||
rooms=room_presences,
|
||||
agents=all_agents,
|
||||
)
|
||||
|
||||
logger.info(f"Computed snapshot: {city_online_total} online in {rooms_online} rooms")
|
||||
logger.info(f"Computed snapshot: {city_online_total} online, {len(all_agents)} agents in {rooms_online} rooms")
|
||||
return snapshot
|
||||
|
||||
async def run_forever(self):
|
||||
@@ -152,3 +193,4 @@ class PresenceAggregator:
|
||||
self._running = False
|
||||
logger.info("Stopping presence aggregator")
|
||||
|
||||
|
||||
|
||||
@@ -34,3 +34,4 @@ def load_settings() -> Settings:
|
||||
presence_daemon_user=os.getenv("PRESENCE_DAEMON_USER", "@presence_daemon:daarion.space"),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ import logging
|
||||
from .config import load_settings
|
||||
from .matrix_client import MatrixClient
|
||||
from .rooms_source import RoomsSource, StaticRoomsSource
|
||||
from .agents_source import AgentsSource
|
||||
from .aggregator import PresenceAggregator
|
||||
|
||||
# Configure logging
|
||||
@@ -58,9 +59,19 @@ else:
|
||||
rooms_source = RoomsSource(db_dsn=settings.db_dsn or "postgresql://postgres:postgres@localhost:5432/postgres")
|
||||
logger.warning("No rooms source configured, using default database")
|
||||
|
||||
# Initialize agents source (uses same DB as rooms)
|
||||
agents_source = None
|
||||
if settings.db_dsn:
|
||||
try:
|
||||
agents_source = AgentsSource(db_dsn=settings.db_dsn)
|
||||
logger.info("Agents source initialized")
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to initialize agents source: {e}")
|
||||
|
||||
aggregator = PresenceAggregator(
|
||||
matrix_client=matrix_client,
|
||||
rooms_source=rooms_source,
|
||||
agents_source=agents_source,
|
||||
poll_interval_seconds=settings.poll_interval_seconds,
|
||||
)
|
||||
|
||||
@@ -157,3 +168,4 @@ if __name__ == "__main__":
|
||||
reload=True,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -92,3 +92,4 @@ class MatrixClient:
|
||||
async def close(self):
|
||||
await self._client.aclose()
|
||||
|
||||
|
||||
|
||||
@@ -1,19 +1,31 @@
|
||||
"""Data models for Presence Aggregator"""
|
||||
from pydantic import BaseModel
|
||||
from typing import List
|
||||
from typing import List, Optional
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
class AgentPresence(BaseModel):
|
||||
"""Agent presence in a room"""
|
||||
agent_id: str
|
||||
display_name: str
|
||||
kind: str = "assistant" # assistant, civic, oracle, builder
|
||||
status: str = "offline" # online, offline, busy
|
||||
room_id: Optional[str] = None
|
||||
color: Optional[str] = None
|
||||
|
||||
|
||||
class RoomPresence(BaseModel):
|
||||
room_id: str # internal room id from DB
|
||||
matrix_room_id: str # Matrix room ID (!xxx:domain)
|
||||
online: int
|
||||
typing: int
|
||||
agents: List[AgentPresence] = [] # Agents present in this room
|
||||
|
||||
|
||||
class CityPresence(BaseModel):
|
||||
online_total: int
|
||||
rooms_online: int
|
||||
agents_online: int = 0
|
||||
|
||||
|
||||
class PresenceSnapshot(BaseModel):
|
||||
@@ -21,4 +33,6 @@ class PresenceSnapshot(BaseModel):
|
||||
timestamp: datetime
|
||||
city: CityPresence
|
||||
rooms: List[RoomPresence]
|
||||
agents: List[AgentPresence] = [] # All online agents
|
||||
|
||||
|
||||
|
||||
@@ -67,3 +67,4 @@ class StaticRoomsSource:
|
||||
def get_rooms(self) -> List[Dict]:
|
||||
return self._rooms
|
||||
|
||||
|
||||
|
||||
@@ -22,3 +22,4 @@ CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "7008"]
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -315,3 +315,4 @@ CREATE TABLE agent_memories_vector (
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -7,3 +7,4 @@ __all__ = ['ShortTermBackend', 'VectorStoreBackend', 'KnowledgeBaseBackend']
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -73,3 +73,4 @@ class KnowledgeBaseBackend:
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -107,3 +107,4 @@ class ShortTermBackend:
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -183,3 +183,4 @@ class VectorStoreBackend:
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -30,3 +30,4 @@ limits:
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -50,3 +50,4 @@ class EmbeddingClient:
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -242,3 +242,4 @@ if __name__ == "__main__":
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -49,3 +49,4 @@ class MemorySummarizeResponse(BaseModel):
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -9,3 +9,4 @@ python-multipart==0.0.6
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -22,3 +22,4 @@ CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "7004"]
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -359,3 +359,4 @@ DAARION Platform Team
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -176,3 +176,4 @@ async def require_microdao_permission(
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -228,3 +228,4 @@ def get_monitor_agent_file_urls(agent_id: str, base_url: str = "/") -> Dict[str,
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -22,3 +22,4 @@ CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "7012"]
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -349,3 +349,4 @@ docker exec postgres psql -U postgres -d daarion \
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -58,3 +58,4 @@ defaults:
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -205,3 +205,4 @@ def evaluate_usage_access(request: PolicyRequest, policy_store: PolicyStore) ->
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -158,3 +158,4 @@ if __name__ == "__main__":
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -57,3 +57,4 @@ class PolicyDecision(BaseModel):
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -99,3 +99,4 @@ class PolicyStore:
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -8,3 +8,4 @@ python-multipart==0.0.6
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -22,3 +22,4 @@ CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -209,3 +209,4 @@ curl -X POST http://localhost:8000/internal/router/test-messaging \
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -209,3 +209,4 @@ async def shutdown_event():
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -7,3 +7,4 @@ PyYAML==6.0.1
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -9,3 +9,4 @@ messaging_inbound:
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -22,3 +22,4 @@ CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "7002"]
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -256,3 +256,4 @@ Proprietary — DAARION Ecosystem
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -237,3 +237,4 @@ if __name__ == "__main__":
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -8,3 +8,4 @@ asyncio-nats-client==0.11.5
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -22,3 +22,4 @@ CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "7009"]
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -302,3 +302,4 @@ Each tool has a `timeout` (seconds). If execution exceeds timeout, it fails grac
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -78,3 +78,4 @@ logging:
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -6,3 +6,4 @@ __all__ = ['HTTPExecutor', 'PythonExecutor']
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -102,3 +102,4 @@ class HTTPExecutor:
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -66,3 +66,4 @@ class PythonExecutor:
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -195,3 +195,4 @@ if __name__ == "__main__":
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -30,3 +30,4 @@ class ToolCallResult(BaseModel):
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -77,3 +77,4 @@ class ToolRegistry:
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -8,3 +8,4 @@ python-multipart==0.0.6
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -22,3 +22,4 @@ CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "7013"]
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -361,3 +361,4 @@ await publish_nats_event("usage.agent", {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -237,3 +237,4 @@ class UsageAggregator:
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -182,3 +182,4 @@ class UsageCollector:
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -219,3 +219,4 @@ if __name__ == "__main__":
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -159,3 +159,4 @@ class UsageQueryResponse(BaseModel):
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -8,3 +8,4 @@ python-multipart==0.0.6
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user