feat: MicroDAO Registry API (read-only) - GET /city/microdao, GET /city/microdao/{slug}
This commit is contained in:
60
migrations/021_microdao_registry.sql
Normal file
60
migrations/021_microdao_registry.sql
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
-- 1. Update microdaos table structure
|
||||||
|
ALTER TABLE microdaos
|
||||||
|
ADD COLUMN IF NOT EXISTS district text,
|
||||||
|
ADD COLUMN IF NOT EXISTS is_active boolean NOT NULL DEFAULT true,
|
||||||
|
ADD COLUMN IF NOT EXISTS is_public boolean NOT NULL DEFAULT true;
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_microdaos_district ON microdaos(district);
|
||||||
|
|
||||||
|
-- 2. Create microdao_agents table
|
||||||
|
CREATE TABLE IF NOT EXISTS microdao_agents (
|
||||||
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
microdao_id text NOT NULL REFERENCES microdaos(id) ON DELETE CASCADE,
|
||||||
|
agent_id text NOT NULL REFERENCES agents(id) ON DELETE CASCADE,
|
||||||
|
role text,
|
||||||
|
is_core boolean NOT NULL DEFAULT false,
|
||||||
|
created_at timestamptz NOT NULL DEFAULT now(),
|
||||||
|
UNIQUE(microdao_id, agent_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- 3. Create microdao_channels table
|
||||||
|
CREATE TABLE IF NOT EXISTS microdao_channels (
|
||||||
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
microdao_id text NOT NULL REFERENCES microdaos(id) ON DELETE CASCADE,
|
||||||
|
kind text NOT NULL,
|
||||||
|
ref_id text NOT NULL,
|
||||||
|
display_name text,
|
||||||
|
is_primary boolean NOT NULL DEFAULT false,
|
||||||
|
extra jsonb DEFAULT '{}'::jsonb,
|
||||||
|
created_at timestamptz NOT NULL DEFAULT now()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_microdao_channels_microdao ON microdao_channels(microdao_id);
|
||||||
|
|
||||||
|
-- 4. Seed Data Updates
|
||||||
|
-- Update districts
|
||||||
|
UPDATE microdaos SET district = 'Core' WHERE slug = 'daarion';
|
||||||
|
UPDATE microdaos SET district = 'Energy' WHERE slug = 'energy-union';
|
||||||
|
UPDATE microdaos SET district = 'Green' WHERE slug = 'greenfood';
|
||||||
|
UPDATE microdaos SET district = 'Clan' WHERE slug = 'clan';
|
||||||
|
UPDATE microdaos SET district = 'Soul' WHERE slug = 'soul';
|
||||||
|
UPDATE microdaos SET district = 'Council' WHERE slug = 'yaromir';
|
||||||
|
UPDATE microdaos SET district = 'Labs' WHERE slug = 'druid';
|
||||||
|
UPDATE microdaos SET district = 'Labs' WHERE slug = 'nutra';
|
||||||
|
UPDATE microdaos SET district = 'Creators' WHERE slug = 'eonarch';
|
||||||
|
|
||||||
|
-- Link orchestrators as core agents
|
||||||
|
INSERT INTO microdao_agents (microdao_id, agent_id, role, is_core)
|
||||||
|
SELECT id, owner_agent_id, 'orchestrator', true
|
||||||
|
FROM microdaos
|
||||||
|
WHERE owner_agent_id IS NOT NULL
|
||||||
|
ON CONFLICT (microdao_id, agent_id) DO NOTHING;
|
||||||
|
|
||||||
|
-- Seed basic Telegram channels (assumed based on slug)
|
||||||
|
INSERT INTO microdao_channels (microdao_id, kind, ref_id, display_name, is_primary)
|
||||||
|
SELECT id, 'telegram', '@' || replace(slug, '-', '') || 'bot', name || ' Telegram Bot', true
|
||||||
|
FROM microdaos
|
||||||
|
WHERE NOT EXISTS (
|
||||||
|
SELECT 1 FROM microdao_channels WHERE microdao_id = microdaos.id AND kind = 'telegram'
|
||||||
|
);
|
||||||
|
|
||||||
@@ -169,4 +169,60 @@ class AgentPresence(BaseModel):
|
|||||||
status: str
|
status: str
|
||||||
room_id: Optional[str] = None
|
room_id: Optional[str] = None
|
||||||
color: Optional[str] = None
|
color: Optional[str] = None
|
||||||
|
node_id: Optional[str] = None
|
||||||
|
district: Optional[str] = None
|
||||||
|
model: Optional[str] = None
|
||||||
|
role: Optional[str] = None
|
||||||
|
avatar_url: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# MicroDAO
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
class MicrodaoSummary(BaseModel):
|
||||||
|
"""MicroDAO summary for list view"""
|
||||||
|
id: str
|
||||||
|
slug: str
|
||||||
|
name: str
|
||||||
|
description: Optional[str] = None
|
||||||
|
district: Optional[str] = None
|
||||||
|
orchestrator_agent_id: Optional[str] = None
|
||||||
|
is_active: bool
|
||||||
|
logo_url: Optional[str] = None
|
||||||
|
agents_count: int
|
||||||
|
rooms_count: int
|
||||||
|
channels_count: int
|
||||||
|
|
||||||
|
|
||||||
|
class MicrodaoChannelView(BaseModel):
|
||||||
|
"""Channel/integration view for MicroDAO"""
|
||||||
|
kind: str # 'matrix' | 'telegram' | 'city_room' | 'crew'
|
||||||
|
ref_id: str
|
||||||
|
display_name: Optional[str] = None
|
||||||
|
is_primary: bool
|
||||||
|
|
||||||
|
|
||||||
|
class MicrodaoAgentView(BaseModel):
|
||||||
|
"""Agent view within MicroDAO"""
|
||||||
|
agent_id: str
|
||||||
|
display_name: str
|
||||||
|
role: Optional[str] = None
|
||||||
|
is_core: bool
|
||||||
|
|
||||||
|
|
||||||
|
class MicrodaoDetail(BaseModel):
|
||||||
|
"""Full MicroDAO detail view"""
|
||||||
|
id: str
|
||||||
|
slug: str
|
||||||
|
name: str
|
||||||
|
description: Optional[str] = None
|
||||||
|
district: Optional[str] = None
|
||||||
|
orchestrator_agent_id: Optional[str] = None
|
||||||
|
orchestrator_display_name: Optional[str] = None
|
||||||
|
is_active: bool
|
||||||
|
is_public: bool
|
||||||
|
logo_url: Optional[str] = None
|
||||||
|
agents: List[MicrodaoAgentView]
|
||||||
|
channels: List[MicrodaoChannelView]
|
||||||
|
|
||||||
|
|||||||
@@ -347,3 +347,116 @@ async def update_agent_status(agent_id: str, status: str, room_id: Optional[str]
|
|||||||
|
|
||||||
return dict(row) if row else None
|
return dict(row) if row else None
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# MicroDAO Repository
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
async def get_microdaos(district: Optional[str] = None, q: Optional[str] = None, limit: int = 50, offset: int = 0) -> List[dict]:
|
||||||
|
"""Отримати список MicroDAOs з агрегованою статистикою"""
|
||||||
|
pool = await get_pool()
|
||||||
|
|
||||||
|
params = []
|
||||||
|
|
||||||
|
where_clauses = ["m.is_public = true", "m.is_active = true"]
|
||||||
|
|
||||||
|
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,
|
||||||
|
m.owner_agent_id as orchestrator_agent_id,
|
||||||
|
m.is_active,
|
||||||
|
m.logo_url,
|
||||||
|
COUNT(DISTINCT ma.agent_id) AS agents_count,
|
||||||
|
COUNT(DISTINCT mc.id) AS channels_count,
|
||||||
|
COUNT(DISTINCT CASE WHEN mc.kind = 'city_room' THEN mc.id END) AS rooms_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
|
||||||
|
WHERE {where_sql}
|
||||||
|
GROUP BY m.id
|
||||||
|
ORDER BY m.name
|
||||||
|
LIMIT ${len(params) + 1} OFFSET ${len(params) + 2}
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Append limit and offset to params
|
||||||
|
params.append(limit)
|
||||||
|
params.append(offset)
|
||||||
|
|
||||||
|
rows = await pool.fetch(query, *params)
|
||||||
|
return [dict(row) for row in rows]
|
||||||
|
|
||||||
|
|
||||||
|
async def get_microdao_by_slug(slug: str) -> Optional[dict]:
|
||||||
|
"""Отримати детальну інформацію про MicroDAO"""
|
||||||
|
pool = await get_pool()
|
||||||
|
|
||||||
|
# 1. Get main DAO info
|
||||||
|
query_dao = """
|
||||||
|
SELECT
|
||||||
|
m.id,
|
||||||
|
m.slug,
|
||||||
|
m.name,
|
||||||
|
m.description,
|
||||||
|
m.district,
|
||||||
|
m.owner_agent_id as orchestrator_agent_id,
|
||||||
|
m.is_active,
|
||||||
|
m.is_public,
|
||||||
|
m.logo_url,
|
||||||
|
a.display_name as orchestrator_display_name
|
||||||
|
FROM microdaos m
|
||||||
|
LEFT JOIN agents a ON m.owner_agent_id = a.id
|
||||||
|
WHERE m.slug = $1
|
||||||
|
"""
|
||||||
|
|
||||||
|
dao_row = await pool.fetchrow(query_dao, slug)
|
||||||
|
if not dao_row:
|
||||||
|
return None
|
||||||
|
|
||||||
|
result = dict(dao_row)
|
||||||
|
dao_id = result["id"]
|
||||||
|
|
||||||
|
# 2. Get Agents
|
||||||
|
query_agents = """
|
||||||
|
SELECT
|
||||||
|
ma.agent_id,
|
||||||
|
ma.role,
|
||||||
|
ma.is_core,
|
||||||
|
a.display_name
|
||||||
|
FROM microdao_agents ma
|
||||||
|
JOIN agents a ON ma.agent_id = a.id
|
||||||
|
WHERE ma.microdao_id = $1
|
||||||
|
ORDER BY ma.is_core DESC, ma.role
|
||||||
|
"""
|
||||||
|
agents_rows = await pool.fetch(query_agents, dao_id)
|
||||||
|
result["agents"] = [dict(row) for row in agents_rows]
|
||||||
|
|
||||||
|
# 3. Get Channels
|
||||||
|
query_channels = """
|
||||||
|
SELECT
|
||||||
|
kind,
|
||||||
|
ref_id,
|
||||||
|
display_name,
|
||||||
|
is_primary
|
||||||
|
FROM microdao_channels
|
||||||
|
WHERE microdao_id = $1
|
||||||
|
ORDER BY is_primary DESC, kind
|
||||||
|
"""
|
||||||
|
channels_rows = await pool.fetch(query_channels, dao_id)
|
||||||
|
result["channels"] = [dict(row) for row in channels_rows]
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
City Backend API Routes
|
City Backend API Routes
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from fastapi import APIRouter, HTTPException, Depends, Body, Header, Query
|
from fastapi import APIRouter, HTTPException, Depends, Body, Header, Query, Request
|
||||||
from typing import List, Optional
|
from typing import List, Optional
|
||||||
import logging
|
import logging
|
||||||
import httpx
|
import httpx
|
||||||
@@ -19,7 +19,11 @@ from models_city import (
|
|||||||
CityMapConfig,
|
CityMapConfig,
|
||||||
CityMapResponse,
|
CityMapResponse,
|
||||||
AgentRead,
|
AgentRead,
|
||||||
AgentPresence
|
AgentPresence,
|
||||||
|
MicrodaoSummary,
|
||||||
|
MicrodaoDetail,
|
||||||
|
MicrodaoAgentView,
|
||||||
|
MicrodaoChannelView
|
||||||
)
|
)
|
||||||
import repo_city
|
import repo_city
|
||||||
from common.redis_client import PresenceRedis, get_redis
|
from common.redis_client import PresenceRedis, get_redis
|
||||||
@@ -473,6 +477,270 @@ async def get_city_map():
|
|||||||
# Agents API
|
# Agents API
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|
||||||
|
@router.put("/agents/{agent_id}/public-profile")
|
||||||
|
async def update_agent_public_profile(agent_id: str, request: Request):
|
||||||
|
"""
|
||||||
|
Оновити публічний профіль агента.
|
||||||
|
Тільки для Architect/Admin.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Check agent exists
|
||||||
|
agent = await repo_city.get_agent_by_id(agent_id)
|
||||||
|
if not agent:
|
||||||
|
raise HTTPException(status_code=404, detail=f"Agent not found: {agent_id}")
|
||||||
|
|
||||||
|
# Parse body
|
||||||
|
body = await request.json()
|
||||||
|
|
||||||
|
is_public = body.get("is_public", False)
|
||||||
|
public_slug = body.get("public_slug")
|
||||||
|
public_title = body.get("public_title")
|
||||||
|
public_tagline = body.get("public_tagline")
|
||||||
|
public_skills = body.get("public_skills", [])
|
||||||
|
public_district = body.get("public_district")
|
||||||
|
public_primary_room_slug = body.get("public_primary_room_slug")
|
||||||
|
|
||||||
|
# Validate: if is_public, slug is required
|
||||||
|
if is_public and not public_slug:
|
||||||
|
raise HTTPException(status_code=400, detail="public_slug is required when is_public is true")
|
||||||
|
|
||||||
|
# Validate slug format
|
||||||
|
if public_slug:
|
||||||
|
import re
|
||||||
|
if not re.match(r'^[a-z0-9_-]+$', public_slug.lower()):
|
||||||
|
raise HTTPException(status_code=400, detail="public_slug must contain only lowercase letters, numbers, underscores, and hyphens")
|
||||||
|
|
||||||
|
# Validate skills (max 10, max 64 chars each)
|
||||||
|
if public_skills:
|
||||||
|
public_skills = [s[:64] for s in public_skills[:10]]
|
||||||
|
|
||||||
|
# Update
|
||||||
|
result = await repo_city.update_agent_public_profile(
|
||||||
|
agent_id=agent_id,
|
||||||
|
is_public=is_public,
|
||||||
|
public_slug=public_slug,
|
||||||
|
public_title=public_title,
|
||||||
|
public_tagline=public_tagline,
|
||||||
|
public_skills=public_skills,
|
||||||
|
public_district=public_district,
|
||||||
|
public_primary_room_slug=public_primary_room_slug
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.info(f"Updated public profile for agent {agent_id}: is_public={is_public}, slug={public_slug}")
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
except ValueError as e:
|
||||||
|
raise HTTPException(status_code=400, detail=str(e))
|
||||||
|
except HTTPException:
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to update agent public profile: {e}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
raise HTTPException(status_code=500, detail="Failed to update agent public profile")
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/citizens")
|
||||||
|
async def get_public_citizens(limit: int = 50, offset: int = 0):
|
||||||
|
"""
|
||||||
|
Отримати список публічних громадян DAARION City.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
citizens = await repo_city.get_public_citizens(limit, offset)
|
||||||
|
return {"citizens": citizens, "total": len(citizens)}
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to get public citizens: {e}")
|
||||||
|
raise HTTPException(status_code=500, detail="Failed to get public citizens")
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/citizens/{slug}")
|
||||||
|
async def get_citizen_by_slug(slug: str, request: Request):
|
||||||
|
"""
|
||||||
|
Отримати публічного громадянина за slug.
|
||||||
|
Для адмінів/архітекторів додається admin_panel_url.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# TODO: Check user role from JWT
|
||||||
|
# For now, always include admin URL (will be filtered by frontend auth)
|
||||||
|
include_admin_url = True # Should be: user.role in ['admin', 'architect']
|
||||||
|
|
||||||
|
citizen = await repo_city.get_citizen_by_slug(slug, include_admin_url=include_admin_url)
|
||||||
|
if not citizen:
|
||||||
|
raise HTTPException(status_code=404, detail=f"Citizen not found: {slug}")
|
||||||
|
return citizen
|
||||||
|
except HTTPException:
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to get citizen: {e}")
|
||||||
|
raise HTTPException(status_code=500, detail="Failed to get citizen")
|
||||||
|
|
||||||
|
|
||||||
|
@router.put("/agents/{agent_id}/prompts/{kind}")
|
||||||
|
async def update_agent_prompt(agent_id: str, kind: str, request: Request):
|
||||||
|
"""
|
||||||
|
Оновити системний промт агента.
|
||||||
|
Тільки для Architect/Admin.
|
||||||
|
kind: core | safety | governance | tools
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Validate kind
|
||||||
|
valid_kinds = ["core", "safety", "governance", "tools"]
|
||||||
|
if kind not in valid_kinds:
|
||||||
|
raise HTTPException(status_code=400, detail=f"Invalid kind. Must be one of: {valid_kinds}")
|
||||||
|
|
||||||
|
# Check agent exists
|
||||||
|
agent = await repo_city.get_agent_by_id(agent_id)
|
||||||
|
if not agent:
|
||||||
|
raise HTTPException(status_code=404, detail=f"Agent not found: {agent_id}")
|
||||||
|
|
||||||
|
# Parse body
|
||||||
|
body = await request.json()
|
||||||
|
content = body.get("content")
|
||||||
|
note = body.get("note")
|
||||||
|
|
||||||
|
if not content or not content.strip():
|
||||||
|
raise HTTPException(status_code=400, detail="Content is required")
|
||||||
|
|
||||||
|
# TODO: Get user from JWT and check permissions
|
||||||
|
# For now, use a placeholder
|
||||||
|
created_by = "ARCHITECT" # Will be replaced with actual user from auth
|
||||||
|
|
||||||
|
# Update prompt
|
||||||
|
result = await repo_city.update_agent_prompt(
|
||||||
|
agent_id=agent_id,
|
||||||
|
kind=kind,
|
||||||
|
content=content.strip(),
|
||||||
|
created_by=created_by,
|
||||||
|
note=note
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.info(f"Updated {kind} prompt for agent {agent_id} to version {result['version']}")
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
except HTTPException:
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to update agent prompt: {e}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
raise HTTPException(status_code=500, detail="Failed to update agent prompt")
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/agents/{agent_id}/prompts/{kind}/history")
|
||||||
|
async def get_agent_prompt_history(agent_id: str, kind: str, limit: int = 10):
|
||||||
|
"""
|
||||||
|
Отримати історію версій промту агента.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
valid_kinds = ["core", "safety", "governance", "tools"]
|
||||||
|
if kind not in valid_kinds:
|
||||||
|
raise HTTPException(status_code=400, detail=f"Invalid kind. Must be one of: {valid_kinds}")
|
||||||
|
|
||||||
|
history = await repo_city.get_agent_prompt_history(agent_id, kind, limit)
|
||||||
|
return {"agent_id": agent_id, "kind": kind, "history": history}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to get prompt history: {e}")
|
||||||
|
raise HTTPException(status_code=500, detail="Failed to get prompt history")
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/agents/{agent_id}/dashboard")
|
||||||
|
async def get_agent_dashboard(agent_id: str):
|
||||||
|
"""
|
||||||
|
Отримати повний dashboard агента (DAIS Profile + Node + Metrics)
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Get agent profile
|
||||||
|
agent = await repo_city.get_agent_by_id(agent_id)
|
||||||
|
if not agent:
|
||||||
|
raise HTTPException(status_code=404, detail=f"Agent not found: {agent_id}")
|
||||||
|
|
||||||
|
# Get agent's rooms
|
||||||
|
rooms = await repo_city.get_agent_rooms(agent_id)
|
||||||
|
|
||||||
|
# Build DAIS profile
|
||||||
|
profile = {
|
||||||
|
"agent_id": agent["id"],
|
||||||
|
"display_name": agent["display_name"],
|
||||||
|
"kind": agent.get("kind", "assistant"),
|
||||||
|
"status": agent.get("status", "offline"),
|
||||||
|
"node_id": agent.get("node_id"),
|
||||||
|
"roles": [agent.get("role")] if agent.get("role") else [],
|
||||||
|
"tags": [],
|
||||||
|
"dais": {
|
||||||
|
"core": {
|
||||||
|
"title": agent.get("display_name"),
|
||||||
|
"bio": f"{agent.get('kind', 'assistant').title()} agent in DAARION",
|
||||||
|
"version": "1.0.0"
|
||||||
|
},
|
||||||
|
"vis": {
|
||||||
|
"avatar_url": agent.get("avatar_url"),
|
||||||
|
"color_primary": agent.get("color", "#22D3EE")
|
||||||
|
},
|
||||||
|
"cog": {
|
||||||
|
"base_model": agent.get("model", "default"),
|
||||||
|
"provider": "ollama",
|
||||||
|
"node_id": agent.get("node_id")
|
||||||
|
},
|
||||||
|
"act": {
|
||||||
|
"tools": agent.get("capabilities", [])
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"city_presence": {
|
||||||
|
"primary_room_slug": agent.get("primary_room_slug"),
|
||||||
|
"district": agent.get("home_district"),
|
||||||
|
"rooms": rooms
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Get node info (simplified)
|
||||||
|
node_info = None
|
||||||
|
if agent.get("node_id"):
|
||||||
|
node_info = {
|
||||||
|
"node_id": agent["node_id"],
|
||||||
|
"status": "online" # Would fetch from Node Registry in production
|
||||||
|
}
|
||||||
|
|
||||||
|
# Get system prompts
|
||||||
|
system_prompts = await repo_city.get_agent_prompts(agent_id)
|
||||||
|
|
||||||
|
# Get public profile
|
||||||
|
public_profile = await repo_city.get_agent_public_profile(agent_id)
|
||||||
|
|
||||||
|
# Build dashboard response
|
||||||
|
dashboard = {
|
||||||
|
"profile": profile,
|
||||||
|
"node": node_info,
|
||||||
|
"runtime": {
|
||||||
|
"health": "healthy" if agent.get("status") == "online" else "unknown",
|
||||||
|
"last_success_at": None,
|
||||||
|
"last_error_at": None
|
||||||
|
},
|
||||||
|
"metrics": {
|
||||||
|
"tasks_1h": 0,
|
||||||
|
"tasks_24h": 0,
|
||||||
|
"errors_24h": 0,
|
||||||
|
"avg_latency_ms_1h": 0,
|
||||||
|
"success_rate_24h": 1.0
|
||||||
|
},
|
||||||
|
"recent_activity": [],
|
||||||
|
"system_prompts": system_prompts,
|
||||||
|
"public_profile": public_profile
|
||||||
|
}
|
||||||
|
|
||||||
|
return dashboard
|
||||||
|
|
||||||
|
except HTTPException:
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to get agent dashboard: {e}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
raise HTTPException(status_code=500, detail="Failed to get agent dashboard")
|
||||||
|
|
||||||
|
|
||||||
@router.get("/agents", response_model=List[AgentRead])
|
@router.get("/agents", response_model=List[AgentRead])
|
||||||
async def get_agents():
|
async def get_agents():
|
||||||
"""
|
"""
|
||||||
@@ -548,7 +816,10 @@ async def get_room_agents(room_id: str):
|
|||||||
kind=agent.get("kind", "assistant"),
|
kind=agent.get("kind", "assistant"),
|
||||||
status=agent.get("status", "offline"),
|
status=agent.get("status", "offline"),
|
||||||
room_id=room_id,
|
room_id=room_id,
|
||||||
color=agent.get("color", "cyan")
|
color=agent.get("color", "cyan"),
|
||||||
|
node_id=agent.get("node_id"),
|
||||||
|
model=agent.get("model"),
|
||||||
|
role=agent.get("role")
|
||||||
))
|
))
|
||||||
|
|
||||||
return result
|
return result
|
||||||
@@ -557,3 +828,120 @@ async def get_room_agents(room_id: str):
|
|||||||
logger.error(f"Failed to get room agents: {e}")
|
logger.error(f"Failed to get room agents: {e}")
|
||||||
raise HTTPException(status_code=500, detail="Failed to get room agents")
|
raise HTTPException(status_code=500, detail="Failed to get room agents")
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/agents/presence-snapshot")
|
||||||
|
async def get_agents_presence_snapshot():
|
||||||
|
"""
|
||||||
|
Отримати snapshot всіх агентів для presence (50 агентів по 10 districts)
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
snapshot = await repo_city.get_agents_presence_snapshot()
|
||||||
|
return snapshot
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to get agents presence snapshot: {e}")
|
||||||
|
raise HTTPException(status_code=500, detail="Failed to get agents presence snapshot")
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# MicroDAO API
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
@router.get("/microdao", response_model=List[MicrodaoSummary])
|
||||||
|
async def get_microdaos(
|
||||||
|
district: Optional[str] = Query(None, description="Filter by district"),
|
||||||
|
q: Optional[str] = Query(None, description="Search by name/description"),
|
||||||
|
limit: int = Query(50, le=100),
|
||||||
|
offset: int = Query(0, ge=0)
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Отримати список MicroDAOs.
|
||||||
|
|
||||||
|
- **district**: фільтр по дістрікту (Core, Energy, Green, Labs, etc.)
|
||||||
|
- **q**: пошук по назві або опису
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
daos = await repo_city.get_microdaos(district=district, q=q, limit=limit, offset=offset)
|
||||||
|
|
||||||
|
result = []
|
||||||
|
for dao in daos:
|
||||||
|
result.append(MicrodaoSummary(
|
||||||
|
id=dao["id"],
|
||||||
|
slug=dao["slug"],
|
||||||
|
name=dao["name"],
|
||||||
|
description=dao.get("description"),
|
||||||
|
district=dao.get("district"),
|
||||||
|
orchestrator_agent_id=dao.get("orchestrator_agent_id"),
|
||||||
|
is_active=dao.get("is_active", True),
|
||||||
|
logo_url=dao.get("logo_url"),
|
||||||
|
agents_count=dao.get("agents_count", 0),
|
||||||
|
rooms_count=dao.get("rooms_count", 0),
|
||||||
|
channels_count=dao.get("channels_count", 0)
|
||||||
|
))
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to get microdaos: {e}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
raise HTTPException(status_code=500, detail="Failed to get microdaos")
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/microdao/{slug}", response_model=MicrodaoDetail)
|
||||||
|
async def get_microdao_by_slug(slug: str):
|
||||||
|
"""
|
||||||
|
Отримати детальну інформацію про MicroDAO.
|
||||||
|
|
||||||
|
Включає:
|
||||||
|
- Базову інформацію про DAO
|
||||||
|
- Список агентів (з ролями)
|
||||||
|
- Список каналів (Telegram, Matrix, City rooms, CrewAI)
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
dao = await repo_city.get_microdao_by_slug(slug)
|
||||||
|
if not dao:
|
||||||
|
raise HTTPException(status_code=404, detail=f"MicroDAO not found: {slug}")
|
||||||
|
|
||||||
|
# Build agents list
|
||||||
|
agents = []
|
||||||
|
for agent in dao.get("agents", []):
|
||||||
|
agents.append(MicrodaoAgentView(
|
||||||
|
agent_id=agent["agent_id"],
|
||||||
|
display_name=agent.get("display_name", agent["agent_id"]),
|
||||||
|
role=agent.get("role"),
|
||||||
|
is_core=agent.get("is_core", False)
|
||||||
|
))
|
||||||
|
|
||||||
|
# Build channels list
|
||||||
|
channels = []
|
||||||
|
for channel in dao.get("channels", []):
|
||||||
|
channels.append(MicrodaoChannelView(
|
||||||
|
kind=channel["kind"],
|
||||||
|
ref_id=channel["ref_id"],
|
||||||
|
display_name=channel.get("display_name"),
|
||||||
|
is_primary=channel.get("is_primary", False)
|
||||||
|
))
|
||||||
|
|
||||||
|
return MicrodaoDetail(
|
||||||
|
id=dao["id"],
|
||||||
|
slug=dao["slug"],
|
||||||
|
name=dao["name"],
|
||||||
|
description=dao.get("description"),
|
||||||
|
district=dao.get("district"),
|
||||||
|
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),
|
||||||
|
logo_url=dao.get("logo_url"),
|
||||||
|
agents=agents,
|
||||||
|
channels=channels
|
||||||
|
)
|
||||||
|
|
||||||
|
except HTTPException:
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to get microdao {slug}: {e}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
raise HTTPException(status_code=500, detail="Failed to get microdao")
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user