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:
Apple
2025-11-28 13:51:51 -08:00
parent 4d7c4b9744
commit 773a955ecc
13 changed files with 744 additions and 67 deletions

View File

@@ -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):

View File

@@ -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

View File

@@ -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: