feat: add orphan agent finder script, update alignment doc with MicroDAO rule
This commit is contained in:
171
scripts/maintenance/find_orphan_agents.py
Normal file
171
scripts/maintenance/find_orphan_agents.py
Normal file
@@ -0,0 +1,171 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Find orphan agents that don't belong to any MicroDAO.
|
||||
|
||||
This script checks:
|
||||
1. Agents without MicroDAO membership
|
||||
2. Agents without node_id (home node)
|
||||
3. Agents that are not in router-config
|
||||
|
||||
Usage:
|
||||
python scripts/maintenance/find_orphan_agents.py
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
import asyncpg
|
||||
from typing import List, Dict, Any
|
||||
|
||||
DATABASE_URL = os.getenv(
|
||||
"DATABASE_URL",
|
||||
"postgresql://postgres:postgres@localhost:5432/daarion"
|
||||
)
|
||||
|
||||
|
||||
async def get_pool():
|
||||
"""Get database connection pool."""
|
||||
return await asyncpg.create_pool(DATABASE_URL)
|
||||
|
||||
|
||||
async def find_agents_without_microdao(pool) -> List[Dict[str, Any]]:
|
||||
"""Find agents that don't belong to any MicroDAO."""
|
||||
query = """
|
||||
SELECT a.id, a.display_name, a.kind, a.node_id, a.is_public, a.is_archived
|
||||
FROM agents a
|
||||
LEFT JOIN microdao_agents ma ON ma.agent_id = a.id
|
||||
WHERE ma.agent_id IS NULL
|
||||
AND COALESCE(a.is_archived, false) = false
|
||||
ORDER BY a.display_name
|
||||
"""
|
||||
rows = await pool.fetch(query)
|
||||
return [dict(row) for row in rows]
|
||||
|
||||
|
||||
async def find_agents_without_node(pool) -> List[Dict[str, Any]]:
|
||||
"""Find agents without home node."""
|
||||
query = """
|
||||
SELECT id, display_name, kind
|
||||
FROM agents
|
||||
WHERE (node_id IS NULL OR node_id = '')
|
||||
AND COALESCE(is_archived, false) = false
|
||||
ORDER BY display_name
|
||||
"""
|
||||
rows = await pool.fetch(query)
|
||||
return [dict(row) for row in rows]
|
||||
|
||||
|
||||
async def find_microdao_without_orchestrator(pool) -> List[Dict[str, Any]]:
|
||||
"""Find MicroDAOs without an orchestrator."""
|
||||
query = """
|
||||
SELECT m.id, m.slug, m.name, COUNT(ma.agent_id) as agents_count
|
||||
FROM microdaos m
|
||||
LEFT JOIN microdao_agents ma ON ma.microdao_id = m.id AND ma.role = 'orchestrator'
|
||||
WHERE COALESCE(m.is_archived, false) = false
|
||||
GROUP BY m.id
|
||||
HAVING COUNT(ma.agent_id) = 0
|
||||
ORDER BY m.name
|
||||
"""
|
||||
rows = await pool.fetch(query)
|
||||
return [dict(row) for row in rows]
|
||||
|
||||
|
||||
async def find_empty_microdao(pool) -> List[Dict[str, Any]]:
|
||||
"""Find MicroDAOs with 0 agents."""
|
||||
query = """
|
||||
SELECT m.id, m.slug, m.name
|
||||
FROM microdaos m
|
||||
LEFT JOIN microdao_agents ma ON ma.microdao_id = m.id
|
||||
WHERE COALESCE(m.is_archived, false) = false
|
||||
GROUP BY m.id
|
||||
HAVING COUNT(ma.agent_id) = 0
|
||||
ORDER BY m.name
|
||||
"""
|
||||
rows = await pool.fetch(query)
|
||||
return [dict(row) for row in rows]
|
||||
|
||||
|
||||
async def get_stats(pool) -> Dict[str, int]:
|
||||
"""Get overall stats."""
|
||||
total_agents = await pool.fetchval(
|
||||
"SELECT COUNT(*) FROM agents WHERE COALESCE(is_archived, false) = false"
|
||||
)
|
||||
total_microdao = await pool.fetchval(
|
||||
"SELECT COUNT(*) FROM microdaos WHERE COALESCE(is_archived, false) = false"
|
||||
)
|
||||
agents_with_microdao = await pool.fetchval("""
|
||||
SELECT COUNT(DISTINCT a.id)
|
||||
FROM agents a
|
||||
JOIN microdao_agents ma ON ma.agent_id = a.id
|
||||
WHERE COALESCE(a.is_archived, false) = false
|
||||
""")
|
||||
|
||||
return {
|
||||
"total_agents": total_agents,
|
||||
"total_microdao": total_microdao,
|
||||
"agents_with_microdao": agents_with_microdao,
|
||||
"agents_without_microdao": total_agents - agents_with_microdao,
|
||||
}
|
||||
|
||||
|
||||
async def main():
|
||||
print("=" * 60)
|
||||
print("DAARION Agent Orphan Finder")
|
||||
print("=" * 60)
|
||||
|
||||
pool = await get_pool()
|
||||
|
||||
try:
|
||||
# Stats
|
||||
stats = await get_stats(pool)
|
||||
print(f"\n📊 Overall Stats:")
|
||||
print(f" Total agents: {stats['total_agents']}")
|
||||
print(f" Total MicroDAOs: {stats['total_microdao']}")
|
||||
print(f" Agents with MicroDAO: {stats['agents_with_microdao']}")
|
||||
print(f" Agents without MicroDAO: {stats['agents_without_microdao']}")
|
||||
|
||||
# Orphan agents
|
||||
orphans = await find_agents_without_microdao(pool)
|
||||
if orphans:
|
||||
print(f"\n⚠️ Agents WITHOUT MicroDAO ({len(orphans)}):")
|
||||
for agent in orphans:
|
||||
print(f" - {agent['display_name']} ({agent['kind']}) @ {agent['node_id']}")
|
||||
else:
|
||||
print("\n✅ All agents belong to at least one MicroDAO!")
|
||||
|
||||
# Agents without node
|
||||
no_node = await find_agents_without_node(pool)
|
||||
if no_node:
|
||||
print(f"\n⚠️ Agents WITHOUT node_id ({len(no_node)}):")
|
||||
for agent in no_node:
|
||||
print(f" - {agent['display_name']} ({agent['kind']})")
|
||||
else:
|
||||
print("\n✅ All agents have a home node!")
|
||||
|
||||
# MicroDAOs without orchestrator
|
||||
no_orch = await find_microdao_without_orchestrator(pool)
|
||||
if no_orch:
|
||||
print(f"\n⚠️ MicroDAOs WITHOUT orchestrator ({len(no_orch)}):")
|
||||
for dao in no_orch:
|
||||
print(f" - {dao['name']} ({dao['slug']})")
|
||||
else:
|
||||
print("\n✅ All MicroDAOs have an orchestrator!")
|
||||
|
||||
# Empty MicroDAOs
|
||||
empty = await find_empty_microdao(pool)
|
||||
if empty:
|
||||
print(f"\n⚠️ Empty MicroDAOs ({len(empty)}):")
|
||||
for dao in empty:
|
||||
print(f" - {dao['name']} ({dao['slug']})")
|
||||
else:
|
||||
print("\n✅ All MicroDAOs have at least one agent!")
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("Done!")
|
||||
|
||||
finally:
|
||||
await pool.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
|
||||
Reference in New Issue
Block a user