feat: align agent/microdao model - add is_orchestrator, is_platform, hierarchy

This commit is contained in:
Apple
2025-11-28 08:34:14 -08:00
parent 66d2c019ff
commit 37e1c8abbe
5 changed files with 454 additions and 25 deletions

View File

@@ -233,6 +233,8 @@ class MicrodaoBadge(BaseModel):
name: str
slug: Optional[str] = None
role: Optional[str] = None # orchestrator, member, etc.
is_public: bool = True
is_platform: bool = False
class AgentSummary(BaseModel):
@@ -251,11 +253,12 @@ class AgentSummary(BaseModel):
node_label: Optional[str] = None # "НОДА1" / "НОДА2"
home_node: Optional[HomeNodeView] = None
# Visibility
visibility_scope: str = "city" # city, microdao, owner_only
# Visibility & roles
visibility_scope: str = "city" # global, microdao, private
is_listed_in_directory: bool = True
is_system: bool = False
is_public: bool = False # backward compatibility
is_public: bool = False
is_orchestrator: bool = False # Can create/manage microDAOs
# MicroDAO
primary_microdao_id: Optional[str] = None
@@ -354,12 +357,27 @@ class MicrodaoSummary(BaseModel):
name: str
description: Optional[str] = None
district: Optional[str] = None
# Visibility & type
is_public: bool = True
is_platform: bool = False # Is a platform/district
is_active: bool = True
# Orchestrator
orchestrator_agent_id: Optional[str] = None
is_active: bool
orchestrator_agent_name: Optional[str] = None
# Hierarchy
parent_microdao_id: Optional[str] = None
parent_microdao_slug: Optional[str] = None
# Stats
logo_url: Optional[str] = None
agents_count: int
rooms_count: int
channels_count: int
member_count: int = 0 # alias for agents_count
agents_count: int = 0 # backward compatibility
room_count: int = 0 # alias for rooms_count
rooms_count: int = 0 # backward compatibility
channels_count: int = 0
class MicrodaoChannelView(BaseModel):
@@ -385,13 +403,25 @@ class MicrodaoDetail(BaseModel):
name: str
description: Optional[str] = None
district: Optional[str] = None
# Visibility & type
is_public: bool = True
is_platform: bool = False
is_active: bool = True
# Orchestrator
orchestrator_agent_id: Optional[str] = None
orchestrator_display_name: Optional[str] = None
is_active: bool
is_public: bool
# Hierarchy
parent_microdao_id: Optional[str] = None
parent_microdao_slug: Optional[str] = None
child_microdaos: List["MicrodaoSummary"] = []
# Content
logo_url: Optional[str] = None
agents: List[MicrodaoAgentView]
channels: List[MicrodaoChannelView]
agents: List[MicrodaoAgentView] = []
channels: List[MicrodaoChannelView] = []
public_citizens: List[MicrodaoCitizenView] = []

View File

@@ -295,6 +295,8 @@ async def get_rooms_for_map() -> List[dict]:
async def list_agent_summaries(
*,
node_id: Optional[str] = None,
microdao_id: Optional[str] = None,
is_public: Optional[bool] = None,
visibility_scope: Optional[str] = None,
listed_only: Optional[bool] = None,
kinds: Optional[List[str]] = None,
@@ -322,6 +324,14 @@ async def list_agent_summaries(
params.append(node_id)
where_clauses.append(f"a.node_id = ${len(params)}")
if microdao_id:
params.append(microdao_id)
where_clauses.append(f"EXISTS (SELECT 1 FROM microdao_agents ma WHERE ma.agent_id = a.id AND ma.microdao_id = ${len(params)})")
if is_public is not None:
params.append(is_public)
where_clauses.append(f"COALESCE(a.is_public, false) = ${len(params)}")
if visibility_scope:
params.append(visibility_scope)
where_clauses.append(f"COALESCE(a.visibility_scope, 'city') = ${len(params)}")
@@ -359,6 +369,7 @@ async def list_agent_summaries(
COALESCE(a.is_listed_in_directory, true) AS is_listed_in_directory,
COALESCE(a.is_system, false) AS is_system,
COALESCE(a.is_public, false) AS is_public,
COALESCE(a.is_orchestrator, false) AS is_orchestrator,
a.primary_microdao_id,
pm.name AS primary_microdao_name,
pm.slug AS primary_microdao_slug,
@@ -1246,17 +1257,26 @@ async def get_microdaos(district: Optional[str] = None, q: Optional[str] = None,
m.name,
m.description,
m.district,
m.owner_agent_id as orchestrator_agent_id,
COALESCE(m.orchestrator_agent_id, m.owner_agent_id) as orchestrator_agent_id,
oa.display_name as orchestrator_agent_name,
m.is_active,
COALESCE(m.is_public, true) as is_public,
COALESCE(m.is_platform, false) as is_platform,
m.parent_microdao_id,
pm.slug as parent_microdao_slug,
m.logo_url,
COUNT(DISTINCT ma.agent_id) AS agents_count,
COUNT(DISTINCT ma.agent_id) AS member_count,
COUNT(DISTINCT mc.id) AS channels_count,
COUNT(DISTINCT CASE WHEN mc.kind = 'city_room' THEN mc.id END) AS rooms_count
COUNT(DISTINCT CASE WHEN mc.kind = 'city_room' THEN mc.id END) AS rooms_count,
COUNT(DISTINCT CASE WHEN mc.kind = 'city_room' THEN mc.id END) AS room_count
FROM microdaos m
LEFT JOIN microdao_agents ma ON ma.microdao_id = m.id
LEFT JOIN microdao_channels mc ON mc.microdao_id = m.id
LEFT JOIN agents oa ON COALESCE(m.orchestrator_agent_id, m.owner_agent_id) = oa.id
LEFT JOIN microdaos pm ON m.parent_microdao_id = pm.id
WHERE {where_sql}
GROUP BY m.id
GROUP BY m.id, oa.display_name, pm.slug
ORDER BY m.name
LIMIT ${len(params) + 1} OFFSET ${len(params) + 2}
"""
@@ -1269,6 +1289,93 @@ async def get_microdaos(district: Optional[str] = None, q: Optional[str] = None,
return [dict(row) for row in rows]
async def list_microdao_summaries(
*,
is_public: Optional[bool] = None,
is_platform: Optional[bool] = None,
district: Optional[str] = None,
q: Optional[str] = None,
limit: int = 50,
offset: int = 0
) -> List[dict]:
"""
Unified method to list microDAOs.
Wraps get_microdaos with additional filtering.
"""
pool = await get_pool()
params = []
where_clauses = [
"COALESCE(m.is_archived, false) = false",
"COALESCE(m.is_test, false) = false",
"m.deleted_at IS NULL",
"m.is_active = true"
]
if is_public is not None:
params.append(is_public)
where_clauses.append(f"COALESCE(m.is_public, true) = ${len(params)}")
if is_platform is not None:
params.append(is_platform)
where_clauses.append(f"COALESCE(m.is_platform, false) = ${len(params)}")
if district:
params.append(district)
where_clauses.append(f"m.district = ${len(params)}")
if q:
params.append(f"%{q}%")
where_clauses.append(f"(m.name ILIKE ${len(params)} OR m.description ILIKE ${len(params)})")
where_sql = " AND ".join(where_clauses)
query = f"""
SELECT
m.id,
m.slug,
m.name,
m.description,
m.district,
COALESCE(m.orchestrator_agent_id, m.owner_agent_id) as orchestrator_agent_id,
oa.display_name as orchestrator_agent_name,
m.is_active,
COALESCE(m.is_public, true) as is_public,
COALESCE(m.is_platform, false) as is_platform,
m.parent_microdao_id,
pm.slug as parent_microdao_slug,
m.logo_url,
COUNT(DISTINCT ma.agent_id) AS agents_count,
COUNT(DISTINCT ma.agent_id) AS member_count,
COUNT(DISTINCT mc.id) AS channels_count,
COUNT(DISTINCT CASE WHEN mc.kind = 'city_room' THEN mc.id END) AS rooms_count,
COUNT(DISTINCT CASE WHEN mc.kind = 'city_room' THEN mc.id END) AS room_count
FROM microdaos m
LEFT JOIN microdao_agents ma ON ma.microdao_id = m.id
LEFT JOIN microdao_channels mc ON mc.microdao_id = m.id
LEFT JOIN agents oa ON COALESCE(m.orchestrator_agent_id, m.owner_agent_id) = oa.id
LEFT JOIN microdaos pm ON m.parent_microdao_id = pm.id
WHERE {where_sql}
GROUP BY m.id, oa.display_name, pm.slug
ORDER BY m.name
LIMIT ${len(params) + 1} OFFSET ${len(params) + 2}
"""
params.append(limit)
params.append(offset)
rows = await pool.fetch(query, *params)
return [dict(row) for row in rows]
async def get_microdao_detail(slug: str) -> Optional[dict]:
"""
Get detailed microDAO info including agents, channels, children.
Alias for get_microdao_by_slug with clearer naming.
"""
return await get_microdao_by_slug(slug)
async def get_microdao_by_slug(slug: str) -> Optional[dict]:
"""Отримати детальну інформацію про MicroDAO"""
pool = await get_pool()
@@ -1281,14 +1388,21 @@ async def get_microdao_by_slug(slug: str) -> Optional[dict]:
m.name,
m.description,
m.district,
m.owner_agent_id as orchestrator_agent_id,
COALESCE(m.orchestrator_agent_id, m.owner_agent_id) as orchestrator_agent_id,
a.display_name as orchestrator_display_name,
m.is_active,
m.is_public,
m.logo_url,
a.display_name as orchestrator_display_name
COALESCE(m.is_public, true) as is_public,
COALESCE(m.is_platform, false) as is_platform,
m.parent_microdao_id,
pm.slug as parent_microdao_slug,
m.logo_url
FROM microdaos m
LEFT JOIN agents a ON m.owner_agent_id = a.id
WHERE m.slug = $1 AND COALESCE(m.is_archived, false) = false
LEFT JOIN agents a ON COALESCE(m.orchestrator_agent_id, m.owner_agent_id) = a.id
LEFT JOIN microdaos pm ON m.parent_microdao_id = pm.id
WHERE m.slug = $1
AND COALESCE(m.is_archived, false) = false
AND COALESCE(m.is_test, false) = false
AND m.deleted_at IS NULL
"""
dao_row = await pool.fetchrow(query_dao, slug)
@@ -1308,6 +1422,9 @@ async def get_microdao_by_slug(slug: str) -> Optional[dict]:
FROM microdao_agents ma
JOIN agents a ON ma.agent_id = a.id
WHERE ma.microdao_id = $1
AND COALESCE(a.is_archived, false) = false
AND COALESCE(a.is_test, false) = false
AND a.deleted_at IS NULL
ORDER BY ma.is_core DESC, ma.role
"""
agents_rows = await pool.fetch(query_agents, dao_id)
@@ -1327,6 +1444,20 @@ async def get_microdao_by_slug(slug: str) -> Optional[dict]:
channels_rows = await pool.fetch(query_channels, dao_id)
result["channels"] = [dict(row) for row in channels_rows]
# 4. Get child microDAOs
query_children = """
SELECT id, slug, name, COALESCE(is_public, true) as is_public,
COALESCE(is_platform, false) as is_platform
FROM microdaos
WHERE parent_microdao_id = $1
AND COALESCE(is_archived, false) = false
AND COALESCE(is_test, false) = false
AND deleted_at IS NULL
ORDER BY name
"""
children_rows = await pool.fetch(query_children, dao_id)
result["child_microdaos"] = [dict(row) for row in children_rows]
public_citizens = await get_microdao_public_citizens(dao_id)
result["public_citizens"] = public_citizens

View File

@@ -68,7 +68,9 @@ class MicrodaoMembershipPayload(BaseModel):
async def list_agents(
kind: Optional[str] = Query(None, description="Filter by agent kind"),
node_id: Optional[str] = Query(None, description="Filter by node_id"),
visibility_scope: Optional[str] = Query(None, description="Filter by visibility: city, microdao, owner_only"),
microdao_id: Optional[str] = Query(None, description="Filter by microDAO id"),
is_public: Optional[bool] = Query(None, description="Filter by public status"),
visibility_scope: Optional[str] = Query(None, description="Filter by visibility: global, microdao, private"),
include_system: bool = Query(True, description="Include system agents"),
limit: int = Query(100, le=200),
offset: int = Query(0, ge=0)
@@ -78,6 +80,8 @@ async def list_agents(
kinds_list = [kind] if kind else None
agents, total = await repo_city.list_agent_summaries(
node_id=node_id,
microdao_id=microdao_id,
is_public=is_public,
visibility_scope=visibility_scope,
kinds=kinds_list,
include_system=include_system,
@@ -126,6 +130,7 @@ async def list_agents(
is_listed_in_directory=agent.get("is_listed_in_directory", True),
is_system=agent.get("is_system", False),
is_public=agent.get("is_public", False),
is_orchestrator=agent.get("is_orchestrator", False),
primary_microdao_id=agent.get("primary_microdao_id"),
primary_microdao_name=agent.get("primary_microdao_name"),
primary_microdao_slug=agent.get("primary_microdao_slug"),
@@ -1320,6 +1325,8 @@ async def get_agents_presence_snapshot():
@router.get("/microdao", response_model=List[MicrodaoSummary])
async def get_microdaos(
district: Optional[str] = Query(None, description="Filter by district"),
is_public: Optional[bool] = Query(None, description="Filter by public status"),
is_platform: Optional[bool] = Query(None, description="Filter by platform status"),
q: Optional[str] = Query(None, description="Search by name/description"),
limit: int = Query(50, le=100),
offset: int = Query(0, ge=0)
@@ -1328,10 +1335,19 @@ async def get_microdaos(
Отримати список MicroDAOs.
- **district**: фільтр по дістрікту (Core, Energy, Green, Labs, etc.)
- **is_public**: фільтр по публічності
- **is_platform**: фільтр по типу (платформа/дістрікт)
- **q**: пошук по назві або опису
"""
try:
daos = await repo_city.get_microdaos(district=district, q=q, limit=limit, offset=offset)
daos = await repo_city.list_microdao_summaries(
district=district,
is_public=is_public,
is_platform=is_platform,
q=q,
limit=limit,
offset=offset
)
result = []
for dao in daos:
@@ -1341,10 +1357,17 @@ async def get_microdaos(
name=dao["name"],
description=dao.get("description"),
district=dao.get("district"),
orchestrator_agent_id=dao.get("orchestrator_agent_id"),
is_public=dao.get("is_public", True),
is_platform=dao.get("is_platform", False),
is_active=dao.get("is_active", True),
orchestrator_agent_id=dao.get("orchestrator_agent_id"),
orchestrator_agent_name=dao.get("orchestrator_agent_name"),
parent_microdao_id=dao.get("parent_microdao_id"),
parent_microdao_slug=dao.get("parent_microdao_slug"),
logo_url=dao.get("logo_url"),
member_count=dao.get("member_count", 0),
agents_count=dao.get("agents_count", 0),
room_count=dao.get("room_count", 0),
rooms_count=dao.get("rooms_count", 0),
channels_count=dao.get("channels_count", 0)
))
@@ -1405,16 +1428,31 @@ async def get_microdao_by_slug(slug: str):
primary_room_slug=citizen.get("public_primary_room_slug")
))
# Build child microDAOs list
child_microdaos = []
for child in dao.get("child_microdaos", []):
child_microdaos.append(MicrodaoSummary(
id=child["id"],
slug=child["slug"],
name=child["name"],
is_public=child.get("is_public", True),
is_platform=child.get("is_platform", False)
))
return MicrodaoDetail(
id=dao["id"],
slug=dao["slug"],
name=dao["name"],
description=dao.get("description"),
district=dao.get("district"),
is_public=dao.get("is_public", True),
is_platform=dao.get("is_platform", False),
is_active=dao.get("is_active", True),
orchestrator_agent_id=dao.get("orchestrator_agent_id"),
orchestrator_display_name=dao.get("orchestrator_display_name"),
is_active=dao.get("is_active", True),
is_public=dao.get("is_public", True),
parent_microdao_id=dao.get("parent_microdao_id"),
parent_microdao_slug=dao.get("parent_microdao_slug"),
child_microdaos=child_microdaos,
logo_url=dao.get("logo_url"),
agents=agents,
channels=channels,