feat(microdao-rooms): Add MicroDAO rooms creation/deletion and agent chat
Backend:
- POST /city/microdao/{slug}/rooms - create new room for MicroDAO
- DELETE /city/microdao/{slug}/rooms/{room_id} - soft-delete room
- POST /city/agents/{agent_id}/ensure-room - create personal agent room
Frontend:
- MicrodaoRoomsSection: Added create room modal with name, description, type
- MicrodaoRoomsSection: Added delete room functionality for managers
- Agent page: Added 'Поговорити' button to open chat in City Room
Models:
- Added CreateMicrodaoRoomRequest model
Task: TASK_PHASE_MICRODAO_ROOMS_AND_PUBLIC_CHAT_v3
This commit is contained in:
@@ -526,6 +526,15 @@ class AttachExistingRoomRequest(BaseModel):
|
||||
sort_order: int = 100
|
||||
|
||||
|
||||
class CreateMicrodaoRoomRequest(BaseModel):
|
||||
"""Request to create a new room for a MicroDAO"""
|
||||
name: str
|
||||
description: Optional[str] = None
|
||||
room_role: str = "general" # primary, lobby, team, research, governance, etc.
|
||||
is_public: bool = True
|
||||
zone_key: Optional[str] = None
|
||||
|
||||
|
||||
class MicrodaoDetail(BaseModel):
|
||||
"""Full MicroDAO detail view"""
|
||||
id: str
|
||||
|
||||
@@ -50,7 +50,8 @@ from models_city import (
|
||||
NodeSwapperDetail,
|
||||
CreateAgentRequest,
|
||||
CreateAgentResponse,
|
||||
DeleteAgentResponse
|
||||
DeleteAgentResponse,
|
||||
CreateMicrodaoRoomRequest
|
||||
)
|
||||
import repo_city
|
||||
from common.redis_client import PresenceRedis, get_redis
|
||||
@@ -2652,6 +2653,73 @@ async def delete_agent(agent_id: str):
|
||||
raise HTTPException(status_code=500, detail=f"Failed to delete agent: {str(e)}")
|
||||
|
||||
|
||||
@router.post("/agents/{agent_id}/ensure-room")
|
||||
async def ensure_agent_room_endpoint(agent_id: str):
|
||||
"""
|
||||
Забезпечити існування персональної кімнати агента (Task v3).
|
||||
Якщо кімнати немає - створює нову.
|
||||
Повертає room_slug для переходу в чат.
|
||||
"""
|
||||
try:
|
||||
pool = await repo_city.get_pool()
|
||||
|
||||
# Get agent
|
||||
agent = await pool.fetchrow("""
|
||||
SELECT id, display_name, primary_room_slug, district
|
||||
FROM agents
|
||||
WHERE id = $1 AND deleted_at IS NULL
|
||||
""", agent_id)
|
||||
|
||||
if not agent:
|
||||
raise HTTPException(status_code=404, detail=f"Agent '{agent_id}' not found")
|
||||
|
||||
# If agent already has a room, return it
|
||||
if agent["primary_room_slug"]:
|
||||
return {"room_slug": agent["primary_room_slug"], "created": False}
|
||||
|
||||
# Create personal room for agent
|
||||
import re
|
||||
room_slug = f"agent-{re.sub(r'[^a-z0-9]+', '-', agent_id.lower()).strip('-')}"
|
||||
|
||||
# Check if slug exists
|
||||
existing = await pool.fetchrow("SELECT id FROM city_rooms WHERE slug = $1", room_slug)
|
||||
if existing:
|
||||
room_slug = f"{room_slug}-{str(uuid.uuid4())[:8]}"
|
||||
|
||||
# Create room
|
||||
await pool.execute("""
|
||||
INSERT INTO city_rooms (
|
||||
slug, name, description, owner_type, owner_id,
|
||||
room_type, room_role, is_public, zone, space_scope
|
||||
) VALUES (
|
||||
$1, $2, $3, 'agent', $4,
|
||||
'agent', 'personal', FALSE, $5, 'personal'
|
||||
)
|
||||
""",
|
||||
room_slug,
|
||||
f"Чат з {agent['display_name']}",
|
||||
f"Персональна кімната агента {agent['display_name']}",
|
||||
agent_id,
|
||||
agent.get("district") or "agents"
|
||||
)
|
||||
|
||||
# Update agent with room_slug
|
||||
await pool.execute("""
|
||||
UPDATE agents SET primary_room_slug = $1, updated_at = NOW()
|
||||
WHERE id = $2
|
||||
""", room_slug, agent_id)
|
||||
|
||||
logger.info(f"Created personal room {room_slug} for agent {agent_id}")
|
||||
|
||||
return {"room_slug": room_slug, "created": True}
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to ensure room for agent {agent_id}: {e}")
|
||||
raise HTTPException(status_code=500, detail=f"Failed to ensure room: {str(e)}")
|
||||
|
||||
|
||||
@router.get("/agents/online", response_model=List[AgentPresence])
|
||||
async def get_online_agents():
|
||||
"""
|
||||
@@ -2944,6 +3012,109 @@ async def get_microdao_rooms_endpoint(slug: str):
|
||||
raise HTTPException(status_code=500, detail="Failed to get microdao rooms")
|
||||
|
||||
|
||||
@router.post("/microdao/{slug}/rooms", response_model=CityRoomSummary)
|
||||
async def create_microdao_room_endpoint(slug: str, payload: CreateMicrodaoRoomRequest):
|
||||
"""
|
||||
Створити нову кімнату для MicroDAO (Task v3).
|
||||
Створює city_room та прив'язує до MicroDAO.
|
||||
"""
|
||||
try:
|
||||
# Get microdao by slug
|
||||
dao = await repo_city.get_microdao_by_slug(slug)
|
||||
if not dao:
|
||||
raise HTTPException(status_code=404, detail=f"MicroDAO not found: {slug}")
|
||||
|
||||
pool = await repo_city.get_pool()
|
||||
|
||||
# Generate slug from name
|
||||
import re
|
||||
room_slug = re.sub(r'[^a-z0-9]+', '-', payload.name.lower()).strip('-')
|
||||
room_slug = f"{slug}-{room_slug}"
|
||||
|
||||
# Check if slug already exists
|
||||
existing = await pool.fetchrow("SELECT id FROM city_rooms WHERE slug = $1", room_slug)
|
||||
if existing:
|
||||
room_slug = f"{room_slug}-{str(uuid.uuid4())[:8]}"
|
||||
|
||||
# Create room in city_rooms
|
||||
row = await pool.fetchrow("""
|
||||
INSERT INTO city_rooms (
|
||||
slug, name, description, owner_type, owner_id,
|
||||
room_type, room_role, is_public, zone, space_scope
|
||||
) VALUES (
|
||||
$1, $2, $3, 'microdao', $4,
|
||||
'microdao', $5, $6, $7, 'microdao'
|
||||
)
|
||||
RETURNING id, slug, name, description, room_role, is_public, zone
|
||||
""",
|
||||
room_slug,
|
||||
payload.name,
|
||||
payload.description,
|
||||
dao["id"],
|
||||
payload.room_role,
|
||||
payload.is_public,
|
||||
payload.zone_key
|
||||
)
|
||||
|
||||
logger.info(f"Created room {room_slug} for MicroDAO {slug}")
|
||||
|
||||
return CityRoomSummary(
|
||||
id=str(row["id"]),
|
||||
slug=row["slug"],
|
||||
name=row["name"],
|
||||
microdao_id=dao["id"],
|
||||
microdao_slug=slug,
|
||||
room_role=row["room_role"],
|
||||
is_public=row["is_public"],
|
||||
sort_order=100
|
||||
)
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to create room for microdao {slug}: {e}")
|
||||
raise HTTPException(status_code=500, detail=f"Failed to create room: {str(e)}")
|
||||
|
||||
|
||||
@router.delete("/microdao/{slug}/rooms/{room_id}")
|
||||
async def delete_microdao_room_endpoint(slug: str, room_id: str):
|
||||
"""
|
||||
Видалити кімнату MicroDAO (Task v3).
|
||||
Soft-delete: встановлює deleted_at.
|
||||
"""
|
||||
try:
|
||||
# Get microdao by slug
|
||||
dao = await repo_city.get_microdao_by_slug(slug)
|
||||
if not dao:
|
||||
raise HTTPException(status_code=404, detail=f"MicroDAO not found: {slug}")
|
||||
|
||||
pool = await repo_city.get_pool()
|
||||
|
||||
# Check if room belongs to this microdao
|
||||
room = await pool.fetchrow("""
|
||||
SELECT id, slug FROM city_rooms
|
||||
WHERE id = $1 AND owner_id = $2 AND owner_type = 'microdao'
|
||||
""", room_id, dao["id"])
|
||||
|
||||
if not room:
|
||||
raise HTTPException(status_code=404, detail="Room not found or not owned by this MicroDAO")
|
||||
|
||||
# Soft delete
|
||||
await pool.execute("""
|
||||
UPDATE city_rooms
|
||||
SET deleted_at = NOW(), updated_at = NOW()
|
||||
WHERE id = $1
|
||||
""", room_id)
|
||||
|
||||
logger.info(f"Deleted room {room['slug']} from MicroDAO {slug}")
|
||||
|
||||
return {"ok": True, "message": f"Room '{room['slug']}' deleted"}
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to delete room {room_id} from microdao {slug}: {e}")
|
||||
raise HTTPException(status_code=500, detail=f"Failed to delete room: {str(e)}")
|
||||
|
||||
|
||||
@router.get("/microdao/{slug}/agents")
|
||||
async def get_microdao_agents_endpoint(slug: str):
|
||||
"""
|
||||
|
||||
Reference in New Issue
Block a user