Files
microdao-daarion/services/city-service/routes_city.py

1239 lines
45 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
City Backend API Routes
"""
from fastapi import APIRouter, HTTPException, Depends, Body, Header, Query, Request
from pydantic import BaseModel
from typing import List, Optional
import logging
import httpx
import os
from models_city import (
CityRoomRead,
CityRoomCreate,
CityRoomDetail,
CityRoomMessageRead,
CityRoomMessageCreate,
CityFeedEventRead,
CityMapRoom,
CityMapConfig,
CityMapResponse,
AgentRead,
AgentPresence,
PublicCitizenSummary,
PublicCitizenProfile,
CitizenInteractionInfo,
CitizenAskRequest,
CitizenAskResponse,
AgentMicrodaoMembership,
MicrodaoSummary,
MicrodaoDetail,
MicrodaoAgentView,
MicrodaoChannelView,
MicrodaoCitizenView,
MicrodaoOption
)
import repo_city
from common.redis_client import PresenceRedis, get_redis
from matrix_client import create_matrix_room, find_matrix_room_by_alias
from dagi_router_client import get_dagi_router_client, DagiRouterClient
logger = logging.getLogger(__name__)
# JWT validation (simplified for MVP)
AUTH_SERVICE_URL = os.getenv("AUTH_SERVICE_URL", "http://daarion-auth:7020")
MATRIX_GATEWAY_URL = os.getenv("MATRIX_GATEWAY_URL", "http://daarion-matrix-gateway:7025")
router = APIRouter(prefix="/city", tags=["city"])
public_router = APIRouter(prefix="/public", tags=["public"])
api_router = APIRouter(prefix="/api/v1", tags=["api_v1"])
class MicrodaoMembershipPayload(BaseModel):
microdao_id: str
role: Optional[str] = None
is_core: bool = False
# =============================================================================
# Public Citizens API
# =============================================================================
@public_router.get("/citizens")
async def list_public_citizens(
district: Optional[str] = Query(None, description="Filter by district"),
kind: Optional[str] = Query(None, description="Filter by agent kind"),
q: Optional[str] = Query(None, description="Search by display name or title"),
limit: int = Query(50, le=100),
offset: int = Query(0, ge=0)
):
"""Публічний список громадян з фільтрами"""
try:
citizens, total = await repo_city.get_public_citizens(
district=district,
kind=kind,
q=q,
limit=limit,
offset=offset
)
items: List[PublicCitizenSummary] = []
for citizen in citizens:
items.append(PublicCitizenSummary(
slug=citizen["public_slug"],
display_name=citizen["display_name"],
public_title=citizen.get("public_title"),
public_tagline=citizen.get("public_tagline"),
avatar_url=citizen.get("avatar_url"),
kind=citizen.get("kind"),
district=citizen.get("public_district"),
primary_room_slug=citizen.get("public_primary_room_slug"),
public_skills=citizen.get("public_skills", []),
online_status=citizen.get("online_status"),
status=citizen.get("status")
))
return {"items": items, "total": total}
except Exception as e:
logger.error(f"Failed to list public citizens: {e}")
raise HTTPException(status_code=500, detail="Failed to list public citizens")
@public_router.get("/citizens/{slug}")
async def get_public_citizen(slug: str, request: Request):
"""Отримати публічний профіль громадянина"""
try:
include_admin_url = False
authorization = request.headers.get("Authorization")
if authorization:
user_info = await validate_jwt_token(authorization)
if user_info:
roles = user_info.get("roles", [])
if any(role in ["admin", "architect"] for role in roles):
include_admin_url = True
citizen = await repo_city.get_public_citizen_by_slug(slug)
if not citizen:
raise HTTPException(status_code=404, detail=f"Citizen not found: {slug}")
if not include_admin_url:
citizen["admin_panel_url"] = None
return PublicCitizenProfile(**citizen)
except HTTPException:
raise
except Exception as e:
logger.error(f"Failed to get public citizen {slug}: {e}")
raise HTTPException(status_code=500, detail="Failed to get citizen")
@public_router.get("/citizens/{slug}/interaction", response_model=CitizenInteractionInfo)
async def get_citizen_interaction_info(slug: str):
"""Отримати інформацію для взаємодії з громадянином"""
try:
agent = await repo_city.get_public_agent_by_slug(slug)
if not agent:
raise HTTPException(status_code=404, detail=f"Citizen not found: {slug}")
matrix_config = await repo_city.get_agent_matrix_config(agent["id"])
matrix_user_id = matrix_config.get("matrix_user_id") if matrix_config else None
primary_room_slug = agent.get("public_primary_room_slug") or agent.get("primary_room_slug")
primary_room_id = matrix_config.get("primary_room_id") if matrix_config else None
primary_room_name = None
room_record = None
if primary_room_id:
room_record = await repo_city.get_room_by_id(primary_room_id)
elif primary_room_slug:
room_record = await repo_city.get_room_by_slug(primary_room_slug)
if room_record:
primary_room_id = room_record.get("id")
primary_room_name = room_record.get("name")
primary_room_slug = room_record.get("slug") or primary_room_slug
microdao = await repo_city.get_microdao_for_agent(agent["id"])
return CitizenInteractionInfo(
slug=slug,
display_name=agent["display_name"],
primary_room_slug=primary_room_slug,
primary_room_id=primary_room_id,
primary_room_name=primary_room_name,
matrix_user_id=matrix_user_id,
district=agent.get("public_district"),
microdao_slug=microdao.get("slug") if microdao else None,
microdao_name=microdao.get("name") if microdao else None,
)
except HTTPException:
raise
except Exception as e:
logger.error(f"Failed to get interaction info for citizen {slug}: {e}")
raise HTTPException(status_code=500, detail="Failed to load interaction info")
@public_router.post("/citizens/{slug}/ask", response_model=CitizenAskResponse)
async def ask_citizen(
slug: str,
payload: CitizenAskRequest,
router_client: DagiRouterClient = Depends(get_dagi_router_client),
):
"""Надіслати запитання громадянину через DAGI Router"""
question = (payload.question or "").strip()
if not question:
raise HTTPException(status_code=400, detail="Question is required")
try:
agent = await repo_city.get_public_agent_by_slug(slug)
if not agent:
raise HTTPException(status_code=404, detail=f"Citizen not found: {slug}")
router_response = await router_client.ask_agent(
agent_id=agent["id"],
prompt=question,
system_prompt=payload.context,
)
answer = (
router_response.get("response")
or router_response.get("answer")
or router_response.get("result")
)
if answer:
answer = answer.strip()
if not answer:
answer = "Вибач, агент наразі не може відповісти."
return CitizenAskResponse(
answer=answer,
agent_display_name=agent["display_name"],
agent_id=agent["id"],
)
except HTTPException:
raise
except httpx.HTTPError as e:
logger.error(f"DAGI Router request failed for citizen {slug}: {e}")
raise HTTPException(status_code=502, detail="Citizen is temporarily unavailable")
except Exception as e:
logger.error(f"Failed to ask citizen {slug}: {e}")
raise HTTPException(status_code=500, detail="Failed to ask citizen")
# =============================================================================
# API v1 — MicroDAO Membership
# =============================================================================
@api_router.get("/microdao/options")
async def get_microdao_options():
"""Отримати список MicroDAO для селектора"""
try:
options = await repo_city.get_microdao_options()
items = [MicrodaoOption(**option) for option in options]
return {"items": items}
except Exception as e:
logger.error(f"Failed to get microdao options: {e}")
raise HTTPException(status_code=500, detail="Failed to get microdao options")
@api_router.put("/agents/{agent_id}/microdao-membership")
async def assign_agent_microdao_membership(
agent_id: str,
payload: MicrodaoMembershipPayload,
authorization: Optional[str] = Header(None)
):
"""Призначити/оновити членство агента в MicroDAO"""
await ensure_architect_or_admin(authorization)
try:
membership = await repo_city.upsert_agent_microdao_membership(
agent_id=agent_id,
microdao_id=payload.microdao_id,
role=payload.role,
is_core=payload.is_core
)
if not membership:
raise HTTPException(status_code=404, detail="MicroDAO not found")
return membership
except HTTPException:
raise
except Exception as e:
logger.error(f"Failed to assign microdao membership: {e}")
raise HTTPException(status_code=500, detail="Failed to assign microdao membership")
@api_router.delete("/agents/{agent_id}/microdao-membership/{microdao_id}")
async def delete_agent_microdao_membership(
agent_id: str,
microdao_id: str,
authorization: Optional[str] = Header(None)
):
"""Видалити членство агента в MicroDAO"""
await ensure_architect_or_admin(authorization)
try:
deleted = await repo_city.remove_agent_microdao_membership(agent_id, microdao_id)
if not deleted:
raise HTTPException(status_code=404, detail="Membership not found")
return {"status": "deleted"}
except HTTPException:
raise
except Exception as e:
logger.error(f"Failed to delete microdao membership: {e}")
raise HTTPException(status_code=500, detail="Failed to delete microdao membership")
# =============================================================================
# City Rooms API
# =============================================================================
@router.get("/rooms", response_model=List[CityRoomRead])
async def get_city_rooms(limit: int = 100, offset: int = 0):
"""
Отримати список всіх City Rooms
"""
try:
rooms = await repo_city.get_all_rooms(limit=limit, offset=offset)
# Додати online count (приблизно)
online_count = await PresenceRedis.get_online_count()
result = []
for room in rooms:
result.append({
**room,
"members_online": online_count if room.get("is_default") else max(1, online_count // 2),
"last_event": None # TODO: з останнього повідомлення
})
return result
except Exception as e:
logger.error(f"Failed to get city rooms: {e}")
raise HTTPException(status_code=500, detail="Failed to get city rooms")
@router.post("/rooms", response_model=CityRoomRead)
async def create_city_room(payload: CityRoomCreate):
"""
Створити нову City Room (автоматично створює Matrix room)
"""
try:
# TODO: витягнути user_id з JWT
created_by = "u_system" # Mock для MVP
# Перевірити чи не існує вже
existing = await repo_city.get_room_by_slug(payload.slug)
if existing:
raise HTTPException(status_code=409, detail="Room with this slug already exists")
# Створити Matrix room
matrix_room_id, matrix_room_alias = await create_matrix_room(
slug=payload.slug,
name=payload.name,
visibility="public"
)
if not matrix_room_id:
logger.warning(f"Failed to create Matrix room for {payload.slug}, proceeding without Matrix")
room = await repo_city.create_room(
slug=payload.slug,
name=payload.name,
description=payload.description,
created_by=created_by,
matrix_room_id=matrix_room_id,
matrix_room_alias=matrix_room_alias
)
# Додати початкове повідомлення
await repo_city.create_room_message(
room_id=room["id"],
body=f"Кімната '{payload.name}' створена! Ласкаво просимо! 🎉",
author_agent_id="ag_system"
)
# Додати в feed
await repo_city.create_feed_event(
kind="system",
room_id=room["id"],
payload={"action": "room_created", "room_name": payload.name, "matrix_room_id": matrix_room_id}
)
return {**room, "members_online": 1, "last_event": None}
except HTTPException:
raise
except Exception as e:
logger.error(f"Failed to create city room: {e}")
raise HTTPException(status_code=500, detail="Failed to create city room")
@router.get("/rooms/{room_id}", response_model=CityRoomDetail)
async def get_city_room(room_id: str):
"""
Отримати деталі City Room з повідомленнями
"""
try:
room = await repo_city.get_room_by_id(room_id)
if not room:
raise HTTPException(status_code=404, detail="Room not found")
messages = await repo_city.get_room_messages(room_id, limit=50)
# Додати username до повідомлень
for msg in messages:
if msg.get("author_user_id"):
msg["username"] = f"User-{msg['author_user_id'][-4:]}" # Mock
elif msg.get("author_agent_id"):
msg["username"] = "System Agent"
else:
msg["username"] = "Anonymous"
online_users = await PresenceRedis.get_all_online()
return {
**room,
"members_online": len(online_users),
"last_event": None,
"messages": messages,
"online_members": online_users[:20] # Перші 20
}
except HTTPException:
raise
except Exception as e:
logger.error(f"Failed to get city room: {e}")
raise HTTPException(status_code=500, detail="Failed to get city room")
@router.post("/rooms/{room_id}/messages", response_model=CityRoomMessageRead)
async def send_city_room_message(room_id: str, payload: CityRoomMessageCreate):
"""
Надіслати повідомлення в City Room
"""
try:
# Перевірити чи кімната існує
room = await repo_city.get_room_by_id(room_id)
if not room:
raise HTTPException(status_code=404, detail="Room not found")
# TODO: витягнути user_id з JWT
author_user_id = "u_mock_user" # Mock для MVP
# Створити повідомлення
message = await repo_city.create_room_message(
room_id=room_id,
body=payload.body,
author_user_id=author_user_id
)
# Додати в feed
await repo_city.create_feed_event(
kind="room_message",
room_id=room_id,
user_id=author_user_id,
payload={"body": payload.body[:100], "message_id": message["id"]}
)
# TODO: Broadcast WS event
# await ws_manager.broadcast_to_room(room_id, {
# "event": "room.message",
# "message": message
# })
# Додати username
message["username"] = f"User-{author_user_id[-4:]}"
return message
except HTTPException:
raise
except Exception as e:
logger.error(f"Failed to send room message: {e}")
raise HTTPException(status_code=500, detail="Failed to send message")
@router.post("/rooms/{room_id}/join")
async def join_city_room(room_id: str):
"""
Приєднатися до City Room (для tracking)
"""
# TODO: витягнути user_id з JWT
user_id = "u_mock_user"
# Для MVP просто повертаємо success
# У production можна зберігати active memberships в Redis
logger.info(f"User {user_id} joined room {room_id}")
return {"status": "joined", "room_id": room_id}
@router.post("/rooms/{room_id}/leave")
async def leave_city_room(room_id: str):
"""
Покинути City Room
"""
# TODO: витягнути user_id з JWT
user_id = "u_mock_user"
logger.info(f"User {user_id} left room {room_id}")
return {"status": "left", "room_id": room_id}
# =============================================================================
# Matrix Backfill API (Internal)
# =============================================================================
@router.post("/matrix/backfill")
async def backfill_matrix_rooms():
"""
Backfill Matrix rooms for existing City Rooms that don't have Matrix integration.
This is an internal endpoint for admin use.
"""
try:
rooms_without_matrix = await repo_city.get_rooms_without_matrix()
results = {
"processed": 0,
"created": 0,
"found": 0,
"failed": 0,
"details": []
}
for room in rooms_without_matrix:
results["processed"] += 1
slug = room["slug"]
name = room["name"]
room_id = room["id"]
# Спочатку спробувати знайти існуючу Matrix room
alias = f"#city_{slug}:daarion.space"
matrix_room_id, matrix_room_alias = await find_matrix_room_by_alias(alias)
if matrix_room_id:
# Знайдено існуючу
await repo_city.update_room_matrix(room_id, matrix_room_id, matrix_room_alias)
results["found"] += 1
results["details"].append({
"room_id": room_id,
"slug": slug,
"status": "found",
"matrix_room_id": matrix_room_id
})
else:
# Створити нову
matrix_room_id, matrix_room_alias = await create_matrix_room(slug, name, "public")
if matrix_room_id:
await repo_city.update_room_matrix(room_id, matrix_room_id, matrix_room_alias)
results["created"] += 1
results["details"].append({
"room_id": room_id,
"slug": slug,
"status": "created",
"matrix_room_id": matrix_room_id
})
else:
results["failed"] += 1
results["details"].append({
"room_id": room_id,
"slug": slug,
"status": "failed",
"error": "Could not create Matrix room"
})
logger.info(f"Matrix backfill completed: {results['processed']} processed, "
f"{results['created']} created, {results['found']} found, {results['failed']} failed")
return results
except Exception as e:
logger.error(f"Matrix backfill failed: {e}")
raise HTTPException(status_code=500, detail=f"Backfill failed: {str(e)}")
# =============================================================================
# Chat Bootstrap API (Matrix Integration)
# =============================================================================
async def validate_jwt_token(authorization: str) -> Optional[dict]:
"""Validate JWT token via auth-service introspect endpoint."""
if not authorization or not authorization.startswith("Bearer "):
return None
token = authorization.replace("Bearer ", "")
async with httpx.AsyncClient(timeout=10.0) as client:
try:
resp = await client.post(
f"{AUTH_SERVICE_URL}/api/auth/introspect",
json={"token": token}
)
if resp.status_code == 200:
data = resp.json()
if data.get("active"):
return {"user_id": data.get("sub"), "email": data.get("email"), "roles": data.get("roles", [])}
return None
except Exception as e:
logger.error(f"JWT validation error: {e}")
return None
async def ensure_architect_or_admin(authorization: Optional[str]) -> dict:
"""Переконатися, що користувач має роль architect/admin"""
if not authorization:
raise HTTPException(status_code=403, detail="Missing authorization token")
user_info = await validate_jwt_token(authorization)
if not user_info:
raise HTTPException(status_code=403, detail="Invalid authorization token")
roles = user_info.get("roles", [])
if not any(role in ["admin", "architect"] for role in roles):
raise HTTPException(status_code=403, detail="Insufficient permissions")
return user_info
@router.get("/chat/bootstrap")
async def chat_bootstrap(
room_slug: str = Query(..., description="City room slug"),
authorization: Optional[str] = Header(None)
):
"""
Bootstrap Matrix chat for a city room.
Returns Matrix credentials and room info for the authenticated user.
"""
# Validate JWT
user_info = await validate_jwt_token(authorization)
if not user_info:
raise HTTPException(status_code=401, detail="Invalid or missing authorization token")
user_id = user_info.get("user_id")
if not user_id:
raise HTTPException(status_code=401, detail="Invalid token: missing user_id")
# Get room by slug
room = await repo_city.get_room_by_slug(room_slug)
if not room:
raise HTTPException(status_code=404, detail=f"Room '{room_slug}' not found")
# Check if room has Matrix integration
matrix_room_id = room.get("matrix_room_id")
matrix_room_alias = room.get("matrix_room_alias")
if not matrix_room_id:
raise HTTPException(
status_code=400,
detail="Room does not have Matrix integration. Run /city/matrix/backfill first."
)
# Get Matrix user token from matrix-gateway
async with httpx.AsyncClient(timeout=30.0) as client:
try:
token_resp = await client.post(
f"{MATRIX_GATEWAY_URL}/internal/matrix/users/token",
json={"user_id": user_id}
)
if token_resp.status_code != 200:
error = token_resp.json()
logger.error(f"Failed to get Matrix token: {error}")
raise HTTPException(status_code=500, detail="Failed to get Matrix credentials")
matrix_creds = token_resp.json()
except httpx.RequestError as e:
logger.error(f"Matrix gateway request error: {e}")
raise HTTPException(status_code=503, detail="Matrix service unavailable")
# Return bootstrap data
return {
"matrix_hs_url": f"https://app.daarion.space", # Through nginx proxy
"matrix_user_id": matrix_creds["matrix_user_id"],
"matrix_access_token": matrix_creds["access_token"],
"matrix_device_id": matrix_creds["device_id"],
"matrix_room_id": matrix_room_id,
"matrix_room_alias": matrix_room_alias,
"room": {
"id": room["id"],
"slug": room["slug"],
"name": room["name"],
"description": room.get("description")
}
}
# =============================================================================
# City Feed API
# =============================================================================
@router.get("/feed", response_model=List[CityFeedEventRead])
async def get_city_feed(limit: int = 20, offset: int = 0):
"""
Отримати City Feed (останні події)
"""
try:
events = await repo_city.get_feed_events(limit=limit, offset=offset)
return events
except Exception as e:
logger.error(f"Failed to get city feed: {e}")
raise HTTPException(status_code=500, detail="Failed to get city feed")
# =============================================================================
# City Map API (2D Map)
# =============================================================================
@router.get("/map", response_model=CityMapResponse)
async def get_city_map():
"""
Отримати дані для 2D мапи міста.
Повертає:
- config: розміри сітки та налаштування
- rooms: список кімнат з координатами
"""
try:
# Отримати конфігурацію
config_data = await repo_city.get_map_config()
config = CityMapConfig(
grid_width=config_data.get("grid_width", 6),
grid_height=config_data.get("grid_height", 3),
cell_size=config_data.get("cell_size", 100),
background_url=config_data.get("background_url")
)
# Отримати кімнати з координатами
rooms_data = await repo_city.get_rooms_for_map()
rooms = []
for room in rooms_data:
rooms.append(CityMapRoom(
id=room["id"],
slug=room["slug"],
name=room["name"],
description=room.get("description"),
room_type=room.get("room_type", "public"),
zone=room.get("zone", "central"),
icon=room.get("icon"),
color=room.get("color"),
x=room.get("map_x", 0),
y=room.get("map_y", 0),
w=room.get("map_w", 1),
h=room.get("map_h", 1),
matrix_room_id=room.get("matrix_room_id")
))
return CityMapResponse(config=config, rooms=rooms)
except Exception as e:
logger.error(f"Failed to get city map: {e}")
raise HTTPException(status_code=500, detail="Failed to get city map")
# =============================================================================
# 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_legacy(limit: int = 50, offset: int = 0):
"""
Отримати список публічних громадян DAARION City.
"""
try:
citizens, total = await repo_city.get_public_citizens(limit=limit, offset=offset)
return {"citizens": citizens, "total": total}
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:
include_admin_url = True # legacy endpoint доступний тільки з адмінської панелі
citizen = await repo_city.get_public_citizen_by_slug(slug)
if not citizen:
raise HTTPException(status_code=404, detail=f"Citizen not found: {slug}")
if not include_admin_url:
citizen["admin_panel_url"] = None
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)
# MicroDAO memberships
memberships_raw = await repo_city.get_agent_microdao_memberships(agent_id)
memberships = [
AgentMicrodaoMembership(
microdao_id=item["microdao_id"],
microdao_slug=item.get("microdao_slug"),
microdao_name=item.get("microdao_name"),
role=item.get("role"),
is_core=item.get("is_core", False)
)
for item in memberships_raw
]
# 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,
"microdao_memberships": memberships
}
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])
async def get_agents():
"""
Отримати список всіх агентів
"""
try:
agents = await repo_city.get_all_agents()
result = []
for agent in agents:
capabilities = agent.get("capabilities", [])
if isinstance(capabilities, str):
import json
capabilities = json.loads(capabilities)
result.append(AgentRead(
id=agent["id"],
display_name=agent["display_name"],
kind=agent.get("kind", "assistant"),
avatar_url=agent.get("avatar_url"),
color=agent.get("color", "cyan"),
status=agent.get("status", "offline"),
current_room_id=agent.get("current_room_id"),
capabilities=capabilities
))
return result
except Exception as e:
logger.error(f"Failed to get agents: {e}")
raise HTTPException(status_code=500, detail="Failed to get agents")
@router.get("/agents/online", response_model=List[AgentPresence])
async def get_online_agents():
"""
Отримати список онлайн агентів (для presence)
"""
try:
agents = await repo_city.get_online_agents()
result = []
for agent in agents:
result.append(AgentPresence(
agent_id=agent["id"],
display_name=agent["display_name"],
kind=agent.get("kind", "assistant"),
status=agent.get("status", "offline"),
room_id=agent.get("current_room_id"),
color=agent.get("color", "cyan")
))
return result
except Exception as e:
logger.error(f"Failed to get online agents: {e}")
raise HTTPException(status_code=500, detail="Failed to get online agents")
@router.get("/rooms/{room_id}/agents", response_model=List[AgentPresence])
async def get_room_agents(room_id: str):
"""
Отримати агентів у конкретній кімнаті
"""
try:
agents = await repo_city.get_agents_by_room(room_id)
result = []
for agent in agents:
result.append(AgentPresence(
agent_id=agent["id"],
display_name=agent["display_name"],
kind=agent.get("kind", "assistant"),
status=agent.get("status", "offline"),
room_id=room_id,
color=agent.get("color", "cyan"),
node_id=agent.get("node_id"),
model=agent.get("model"),
role=agent.get("role")
))
return result
except Exception as e:
logger.error(f"Failed to get room agents: {e}")
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)
))
public_citizens = []
for citizen in dao.get("public_citizens", []):
public_citizens.append(MicrodaoCitizenView(
slug=citizen["slug"],
display_name=citizen["display_name"],
public_title=citizen.get("public_title"),
public_tagline=citizen.get("public_tagline"),
avatar_url=citizen.get("avatar_url"),
district=citizen.get("public_district"),
primary_room_slug=citizen.get("public_primary_room_slug")
))
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,
public_citizens=public_citizens
)
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")