feat: TASK 031-033 - Node Guardian/Steward + Agent & MicroDAO Chat Widgets
TASK 031: Node Agents Discovery - Documented existing Monitor agents (NODE1, NODE2) - Created NODE_AGENTS_INVENTORY.md TASK 032: Node Guardian/Steward Formalize - Added migration 030_node_guardian_steward.sql - Added is_node_guardian, is_node_steward to agents - Added guardian_agent_id, steward_agent_id to node_cache - Updated repo_city.py for guardian/steward in node queries - Added NodeAgentsPanel component for Node Dashboard TASK 033: Agent & MicroDAO Chat Widgets - Added CityRoomSummary model - Added primary_city_room to AgentDashboard API - Added primary_city_room to MicrodaoDetail API - Added get_microdao_primary_room() function - Updated Agent Console with Matrix chat section - Updated MicroDAO page with public chat section - Reused existing CityChatWidget component
This commit is contained in:
@@ -396,6 +396,14 @@ class MicrodaoAgentView(BaseModel):
|
||||
is_core: bool
|
||||
|
||||
|
||||
class CityRoomSummary(BaseModel):
|
||||
"""Summary of a city room for chat embedding"""
|
||||
id: str
|
||||
slug: str
|
||||
name: str
|
||||
matrix_room_id: Optional[str] = None
|
||||
|
||||
|
||||
class MicrodaoDetail(BaseModel):
|
||||
"""Full MicroDAO detail view"""
|
||||
id: str
|
||||
@@ -423,6 +431,9 @@ class MicrodaoDetail(BaseModel):
|
||||
agents: List[MicrodaoAgentView] = []
|
||||
channels: List[MicrodaoChannelView] = []
|
||||
public_citizens: List[MicrodaoCitizenView] = []
|
||||
|
||||
# Primary city room for chat
|
||||
primary_city_room: Optional[CityRoomSummary] = None
|
||||
|
||||
|
||||
class AgentMicrodaoMembership(BaseModel):
|
||||
|
||||
@@ -1505,7 +1505,7 @@ async def get_microdao_by_slug(slug: str) -> Optional[dict]:
|
||||
# =============================================================================
|
||||
|
||||
async def get_all_nodes() -> List[dict]:
|
||||
"""Отримати список всіх нод з кількістю агентів"""
|
||||
"""Отримати список всіх нод з кількістю агентів та Guardian/Steward"""
|
||||
pool = await get_pool()
|
||||
|
||||
query = """
|
||||
@@ -1518,18 +1518,47 @@ async def get_all_nodes() -> List[dict]:
|
||||
nc.status,
|
||||
nc.gpu,
|
||||
nc.last_sync AS last_heartbeat,
|
||||
nc.guardian_agent_id,
|
||||
nc.steward_agent_id,
|
||||
(SELECT COUNT(*) FROM agents a WHERE a.node_id = nc.node_id) AS agents_total,
|
||||
(SELECT COUNT(*) FROM agents a WHERE a.node_id = nc.node_id AND a.status = 'online') AS agents_online
|
||||
(SELECT COUNT(*) FROM agents a WHERE a.node_id = nc.node_id AND a.status = 'online') AS agents_online,
|
||||
ga.display_name AS guardian_name,
|
||||
sa.display_name AS steward_name
|
||||
FROM node_cache nc
|
||||
LEFT JOIN agents ga ON nc.guardian_agent_id = ga.id
|
||||
LEFT JOIN agents sa ON nc.steward_agent_id = sa.id
|
||||
ORDER BY nc.environment DESC, nc.node_name
|
||||
"""
|
||||
|
||||
rows = await pool.fetch(query)
|
||||
return [dict(row) for row in rows]
|
||||
result = []
|
||||
for row in rows:
|
||||
data = dict(row)
|
||||
# Build guardian_agent object
|
||||
if data.get("guardian_agent_id"):
|
||||
data["guardian_agent"] = {
|
||||
"id": data.get("guardian_agent_id"),
|
||||
"name": data.get("guardian_name"),
|
||||
}
|
||||
else:
|
||||
data["guardian_agent"] = None
|
||||
# Build steward_agent object
|
||||
if data.get("steward_agent_id"):
|
||||
data["steward_agent"] = {
|
||||
"id": data.get("steward_agent_id"),
|
||||
"name": data.get("steward_name"),
|
||||
}
|
||||
else:
|
||||
data["steward_agent"] = None
|
||||
# Clean up
|
||||
data.pop("guardian_name", None)
|
||||
data.pop("steward_name", None)
|
||||
result.append(data)
|
||||
return result
|
||||
|
||||
|
||||
async def get_node_by_id(node_id: str) -> Optional[dict]:
|
||||
"""Отримати ноду по ID"""
|
||||
"""Отримати ноду по ID з Guardian та Steward агентами"""
|
||||
pool = await get_pool()
|
||||
|
||||
query = """
|
||||
@@ -1542,14 +1571,58 @@ async def get_node_by_id(node_id: str) -> Optional[dict]:
|
||||
nc.status,
|
||||
nc.gpu,
|
||||
nc.last_sync AS last_heartbeat,
|
||||
nc.guardian_agent_id,
|
||||
nc.steward_agent_id,
|
||||
(SELECT COUNT(*) FROM agents a WHERE a.node_id = nc.node_id) AS agents_total,
|
||||
(SELECT COUNT(*) FROM agents a WHERE a.node_id = nc.node_id AND a.status = 'online') AS agents_online
|
||||
(SELECT COUNT(*) FROM agents a WHERE a.node_id = nc.node_id AND a.status = 'online') AS agents_online,
|
||||
-- Guardian agent info
|
||||
ga.display_name AS guardian_name,
|
||||
ga.kind AS guardian_kind,
|
||||
ga.public_slug AS guardian_slug,
|
||||
-- Steward agent info
|
||||
sa.display_name AS steward_name,
|
||||
sa.kind AS steward_kind,
|
||||
sa.public_slug AS steward_slug
|
||||
FROM node_cache nc
|
||||
LEFT JOIN agents ga ON nc.guardian_agent_id = ga.id
|
||||
LEFT JOIN agents sa ON nc.steward_agent_id = sa.id
|
||||
WHERE nc.node_id = $1
|
||||
"""
|
||||
|
||||
row = await pool.fetchrow(query, node_id)
|
||||
return dict(row) if row else None
|
||||
if not row:
|
||||
return None
|
||||
|
||||
data = dict(row)
|
||||
|
||||
# Build guardian_agent object
|
||||
if data.get("guardian_agent_id"):
|
||||
data["guardian_agent"] = {
|
||||
"id": data.get("guardian_agent_id"),
|
||||
"name": data.get("guardian_name"),
|
||||
"kind": data.get("guardian_kind"),
|
||||
"slug": data.get("guardian_slug"),
|
||||
}
|
||||
else:
|
||||
data["guardian_agent"] = None
|
||||
|
||||
# Build steward_agent object
|
||||
if data.get("steward_agent_id"):
|
||||
data["steward_agent"] = {
|
||||
"id": data.get("steward_agent_id"),
|
||||
"name": data.get("steward_name"),
|
||||
"kind": data.get("steward_kind"),
|
||||
"slug": data.get("steward_slug"),
|
||||
}
|
||||
else:
|
||||
data["steward_agent"] = None
|
||||
|
||||
# Clean up intermediate fields
|
||||
for key in ["guardian_name", "guardian_kind", "guardian_slug",
|
||||
"steward_name", "steward_kind", "steward_slug"]:
|
||||
data.pop(key, None)
|
||||
|
||||
return data
|
||||
|
||||
|
||||
# =============================================================================
|
||||
@@ -1656,3 +1729,38 @@ async def create_microdao_for_agent(
|
||||
|
||||
return dict(dao_row)
|
||||
|
||||
|
||||
async def get_microdao_primary_room(microdao_id: str) -> Optional[dict]:
|
||||
"""
|
||||
Отримати основну кімнату MicroDAO для чату.
|
||||
Пріоритет: primary room → перша публічна кімната → будь-яка кімната.
|
||||
"""
|
||||
pool = await get_pool()
|
||||
|
||||
query = """
|
||||
SELECT
|
||||
cr.id,
|
||||
cr.slug,
|
||||
cr.name,
|
||||
cr.matrix_room_id
|
||||
FROM city_rooms cr
|
||||
WHERE cr.microdao_id = $1
|
||||
AND cr.is_active = true
|
||||
ORDER BY
|
||||
CASE WHEN cr.room_type = 'primary' THEN 0
|
||||
WHEN cr.room_type = 'public' THEN 1
|
||||
ELSE 2 END,
|
||||
cr.created_at
|
||||
LIMIT 1
|
||||
"""
|
||||
|
||||
row = await pool.fetchrow(query, microdao_id)
|
||||
if row:
|
||||
return {
|
||||
"id": str(row["id"]),
|
||||
"slug": row["slug"],
|
||||
"name": row["name"],
|
||||
"matrix_room_id": row.get("matrix_room_id")
|
||||
}
|
||||
return None
|
||||
|
||||
|
||||
@@ -1206,10 +1206,28 @@ async def get_agent_dashboard(agent_id: str):
|
||||
for item in memberships_raw
|
||||
]
|
||||
|
||||
# Get primary city room for agent
|
||||
primary_city_room = None
|
||||
# Priority 1: agent's primary room from city_rooms
|
||||
if rooms and len(rooms) > 0:
|
||||
primary_room = rooms[0] # First room as primary
|
||||
primary_city_room = {
|
||||
"id": primary_room.get("id"),
|
||||
"slug": primary_room.get("slug"),
|
||||
"name": primary_room.get("name"),
|
||||
"matrix_room_id": primary_room.get("matrix_room_id")
|
||||
}
|
||||
# Priority 2: Get from primary MicroDAO's main room
|
||||
elif agent.get("primary_microdao_id"):
|
||||
microdao_room = await repo_city.get_microdao_primary_room(agent["primary_microdao_id"])
|
||||
if microdao_room:
|
||||
primary_city_room = microdao_room
|
||||
|
||||
# Build dashboard response
|
||||
dashboard = {
|
||||
"profile": profile,
|
||||
"node": node_info,
|
||||
"primary_city_room": primary_city_room,
|
||||
"runtime": {
|
||||
"health": "healthy" if agent.get("status") == "online" else "unknown",
|
||||
"last_success_at": None,
|
||||
@@ -1466,6 +1484,18 @@ async def get_microdao_by_slug(slug: str):
|
||||
is_platform=child.get("is_platform", False)
|
||||
))
|
||||
|
||||
# Get primary city room for MicroDAO
|
||||
primary_city_room = await repo_city.get_microdao_primary_room(dao["id"])
|
||||
primary_room_summary = None
|
||||
if primary_city_room:
|
||||
from models_city import CityRoomSummary
|
||||
primary_room_summary = CityRoomSummary(
|
||||
id=primary_city_room["id"],
|
||||
slug=primary_city_room["slug"],
|
||||
name=primary_city_room["name"],
|
||||
matrix_room_id=primary_city_room.get("matrix_room_id")
|
||||
)
|
||||
|
||||
return MicrodaoDetail(
|
||||
id=dao["id"],
|
||||
slug=dao["slug"],
|
||||
@@ -1483,7 +1513,8 @@ async def get_microdao_by_slug(slug: str):
|
||||
logo_url=dao.get("logo_url"),
|
||||
agents=agents,
|
||||
channels=channels,
|
||||
public_citizens=public_citizens
|
||||
public_citizens=public_citizens,
|
||||
primary_city_room=primary_room_summary
|
||||
)
|
||||
|
||||
except HTTPException:
|
||||
|
||||
Reference in New Issue
Block a user