1|""" 2|Pydantic Models для City Backend 3|""" 4| 5|from pydantic import BaseModel, Field 6|from typing import Optional, List, Dict, Any 7|from datetime import datetime 8| 9| 10|# ============================================================================= 11|# City Rooms 12|# ============================================================================= 13| 14|class CityRoomBase(BaseModel): 15| slug: str 16| name: str 17| description: Optional[str] = None 18| 19| 20|class CityRoomCreate(CityRoomBase): 21| pass 22| 23| 24|class CityRoomRead(CityRoomBase): 25| id: str 26| is_default: bool 27| created_at: datetime 28| created_by: Optional[str] = None 29| members_online: int = 0 30| last_event: Optional[str] = None 31| # Branding 32| logo_url: Optional[str] = None 33| banner_url: Optional[str] = None 34| # Context 35| microdao_id: Optional[str] = None 36| microdao_name: Optional[str] = None 37| microdao_slug: Optional[str] = None 38| microdao_logo_url: Optional[str] = None 39| # Matrix integration 40| matrix_room_id: Optional[str] = None 41| matrix_room_alias: Optional[str] = None 42| 43| 44|# ============================================================================= 45|# City Room Messages 46|# ============================================================================= 47| 48|class CityRoomMessageBase(BaseModel): 49| body: str = Field(..., min_length=1, max_length=10000) 50| 51| 52|class CityRoomMessageCreate(CityRoomMessageBase): 53| pass 54| 55| 56|class CityRoomMessageRead(CityRoomMessageBase): 57| id: str 58| room_id: str 59| author_user_id: Optional[str] = None 60| author_agent_id: Optional[str] = None 61| username: Optional[str] = "Anonymous" # Для frontend 62| created_at: datetime 63| 64| 65|# ============================================================================= 66|# City Room Detail (з повідомленнями) 67|# ============================================================================= 68| 69|class CityRoomDetail(CityRoomRead): 70| messages: List[CityRoomMessageRead] = [] 71| online_members: List[str] = [] # user_ids 72| 73| 74|# ============================================================================= 75|# City Feed Events 76|# ============================================================================= 77| 78|class CityFeedEventRead(BaseModel): 79| id: str 80| kind: str # 'room_message', 'agent_reply', 'system', 'dao_event' 81| room_id: Optional[str] = None 82| user_id: Optional[str] = None 83| agent_id: Optional[str] = None 84| payload: dict 85| created_at: datetime 86| 87| 88|# ============================================================================= 89|# Presence 90|# ============================================================================= 91| 92|class PresenceUpdate(BaseModel): 93| user_id: str 94| status: str # 'online', 'offline', 'away' 95| last_seen: Optional[datetime] = None 96| 97| 98|class PresenceBulkUpdate(BaseModel): 99| users: List[PresenceUpdate] 100| 101| 102|# ============================================================================= 103|# WebSocket Messages 104|# ============================================================================= 105| 106|class WSRoomMessage(BaseModel): 107| event: str # 'room.message', 'room.join', 'room.leave' 108| room_id: Optional[str] = None 109| user_id: Optional[str] = None 110| message: Optional[CityRoomMessageRead] = None 111| 112| 113|class WSPresenceMessage(BaseModel): 114| event: str # 'presence.heartbeat', 'presence.update' 115| user_id: str 116| status: Optional[str] = None 117| 118| 119|# ============================================================================= 120|# City Map (2D Map) 121|# ============================================================================= 122| 123|class CityMapRoom(BaseModel): 124| """Room representation on 2D city map""" 125| id: str 126| slug: str 127| name: str 128| description: Optional[str] = None 129| room_type: str = "public" 130| zone: str = "central" 131| icon: Optional[str] = None 132| color: Optional[str] = None 133| # Map coordinates 134| x: int = 0 135| y: int = 0 136| w: int = 1 137| h: int = 1 138| # Matrix integration 139| matrix_room_id: Optional[str] = None 140| 141| 142|class CityMapConfig(BaseModel): 143| """Global city map configuration""" 144| grid_width: int = 6 145| grid_height: int = 3 146| cell_size: int = 100 147| background_url: Optional[str] = None 148| 149| 150|class CityMapResponse(BaseModel): 151| """Full city map response""" 152| config: CityMapConfig 153| rooms: List[CityMapRoom] 154| 155| 156|# ============================================================================= 157|# Branding & Assets 158|# ============================================================================= 159| 160|class BrandingUpdatePayload(BaseModel): 161| logo_url: Optional[str] = None 162| banner_url: Optional[str] = None 163| 164| 165|class AssetUploadResponse(BaseModel): 166| original_url: str 167| processed_url: str 168| thumb_url: Optional[str] = None 169| 170| 171|# ============================================================================= 172|# Agents (for Agent Presence) 173|# ============================================================================= 174| 175|class AgentRead(BaseModel): 176| """Agent representation""" 177| id: str 178| display_name: str 179| kind: str = "assistant" # assistant, civic, oracle, builder 180| avatar_url: Optional[str] = None 181| color: str = "cyan" 182| status: str = "offline" # online, offline, busy 183| current_room_id: Optional[str] = None 184| capabilities: List[str] = [] 185| 186| 187|class AgentPresence(BaseModel): 188| """Agent presence in a room""" 189| agent_id: str 190| display_name: str 191| kind: str 192| status: str 193| room_id: Optional[str] = None 194| color: Optional[str] = None 195| node_id: Optional[str] = None 196| district: Optional[str] = None 197| model: Optional[str] = None 198| role: Optional[str] = None 199| avatar_url: Optional[str] = None 200| 201| 202|# ============================================================================= 203|# Citizens 204|# ============================================================================= 205| 206|class CityPresenceRoomView(BaseModel): 207| room_id: Optional[str] = None 208| slug: Optional[str] = None 209| name: Optional[str] = None 210| 211| 212|class CityPresenceView(BaseModel): 213| primary_room_slug: Optional[str] = None 214| rooms: List[CityPresenceRoomView] = [] 215| 216| 217|class HomeNodeView(BaseModel): 218| """Home node information for agent/citizen""" 219| id: Optional[str] = None 220| name: Optional[str] = None 221| hostname: Optional[str] = None 222| roles: List[str] = [] 223| environment: Optional[str] = None 224| 225| 226|class NodeAgentSummary(BaseModel): 227| """Summary of a node agent (Guardian or Steward)""" 228| id: str 229| name: Optional[str] = None 230| kind: Optional[str] = None 231| slug: Optional[str] = None 232| 233| 234|class NodeMicrodaoSummary(BaseModel): 235| """Summary of a MicroDAO hosted on a node (via orchestrator)""" 236| id: str 237| slug: str 238| name: str 239| rooms_count: int = 0 240| 241| 242|class NodeMetrics(BaseModel): 243| """Node metrics for Node Directory cards""" 244| cpu_model: Optional[str] = None 245| cpu_cores: int = 0 246| cpu_usage: float = 0.0 247| gpu_model: Optional[str] = None 248| gpu_vram_total: int = 0 249| gpu_vram_used: int = 0 250| ram_total: int = 0 251| ram_used: int = 0 252| disk_total: int = 0 253| disk_used: int = 0 254| agent_count_router: int = 0 255| agent_count_system: int = 0 256| dagi_router_url: Optional[str] = None 257| swapper_healthy: bool = False 258| swapper_models_loaded: int = 0 259| swapper_models_total: int = 0 260| 261| 262|class NodeProfile(BaseModel): 263| """Node profile for Node Directory""" 264| node_id: str 265| name: str 266| hostname: Optional[str] = None 267| roles: List[str] = [] 268| environment: str = "unknown" 269| status: str = "offline" 270| gpu_info: Optional[str] = None 271| agents_total: int = 0 272| agents_online: int = 0 273| last_heartbeat: Optional[str] = None 274| guardian_agent_id: Optional[str] = None 275| steward_agent_id: Optional[str] = None 276| guardian_agent: Optional[NodeAgentSummary] = None 277| steward_agent: Optional[NodeAgentSummary] = None 278| microdaos: List[NodeMicrodaoSummary] = [] 279| metrics: Optional[NodeMetrics] = None 280| 281| 282|class ModelBindings(BaseModel): 283| """Agent model bindings for AI capabilities""" 284| primary_model: Optional[str] = None # e.g., "qwen3:8b" 285| supported_kinds: List[str] = [] # e.g., ["text", "vision", "audio"] 286| 287| 288|class UsageStats(BaseModel): 289| """Agent usage statistics""" 290| tokens_total_24h: Optional[int] = None 291| calls_total_24h: Optional[int] = None 292| last_active: Optional[str] = None 293| 294| 295|class MicrodaoBadge(BaseModel): 296| """MicroDAO badge for agent display""" 297| id: str 298| name: str 299| slug: Optional[str] = None 300| role: Optional[str] = None # orchestrator, member, etc. 301| is_public: bool = True 302| is_platform: bool = False 303| logo_url: Optional[str] = None 304| banner_url: Optional[str] = None 305| 306| 307|class AgentCrewInfo(BaseModel): 308| """Information about agent's CrewAI team""" 309| has_crew_team: bool 310| crew_team_key: Optional[str] = None 311| matrix_room_id: Optional[str] = None 312| 313| 314|class AgentSummary(BaseModel): 315| """Unified Agent summary for Agent Console and Citizens""" 316| id: str 317| slug: Optional[str] = None 318| display_name: str 319| title: Optional[str] = None # public_title 320| tagline: Optional[str] = None # public_tagline 321| kind: str = "assistant" 322| avatar_url: Optional[str] = None 323| status: str = "offline" 324| 325| # Node info 326| node_id: Optional[str] = None 327| node_label: Optional[str] = None # "НОДА1" / "НОДА2" 328| home_node: Optional[HomeNodeView] = None 329| 330| # Governance & DAIS (A1, A2) 331| gov_level: Optional[str] = None # personal, core_team, orchestrator, district_lead, city_governance 332| dais_identity_id: Optional[str] = None # DAIS identity reference 333| 334| # Visibility & roles 335| visibility_scope: str = "city" # global, microdao, private 336| is_listed_in_directory: bool = True 337| is_system: bool = False 338| is_public: bool = False 339| is_orchestrator: bool = False # Can create/manage microDAOs 340| 341| # MicroDAO (A3) 342| primary_microdao_id: Optional[str] = None 343| primary_microdao_name: Optional[str] = None 344| primary_microdao_slug: Optional[str] = None 345| home_microdao_id: Optional[str] = None # Owner microDAO 346| home_microdao_name: Optional[str] = None 347| home_microdao_slug: Optional[str] = None 348| district: Optional[str] = None 349| microdaos: List[MicrodaoBadge] = [] 350| microdao_memberships: List[Dict[str, Any]] = [] # backward compatibility 351| 352| # Skills 353| public_skills: List[str] = [] 354| 355| # CrewAI 356| crew_info: Optional[AgentCrewInfo] = None 357| 358| # Future: model bindings and usage stats 359| model_bindings: Optional[ModelBindings] = None 360| usage_stats: Optional[UsageStats] = None 361| 362| 363|class PublicCitizenSummary(BaseModel): 364| slug: str 365| display_name: str 366| public_title: Optional[str] = None 367| public_tagline: Optional[str] = None 368| avatar_url: Optional[str] = None 369| kind: Optional[str] = None 370| district: Optional[str] = None 371| primary_room_slug: Optional[str] = None 372| public_skills: List[str] = [] 373| online_status: Optional[str] = "unknown" 374| status: Optional[str] = None # backward compatibility 375| # Home node info 376| home_node: Optional[HomeNodeView] = None 377| node_id: Optional[str] = None 378| 379| # TASK 037A: Alignment 380| home_microdao_slug: Optional[str] = None 381| home_microdao_name: Optional[str] = None 382| primary_city_room: Optional["CityRoomSummary"] = None 383| 384| 385|class PublicCitizenProfile(BaseModel): 386| slug: str 387| display_name: str 388| kind: Optional[str] = None 389| public_title: Optional[str] = None 390| public_tagline: Optional[str] = None 391| district: Optional[str] = None 392| avatar_url: Optional[str] = None 393| status: Optional[str] = None 394| node_id: Optional[str] = None 395| public_skills: List[str] = [] 396| city_presence: Optional[CityPresenceView] = None 397| dais_public: Dict[str, Any] 398| interaction: Dict[str, Any] 399| metrics_public: Dict[str, Any] 400| admin_panel_url: Optional[str] = None 401| microdao: Optional[Dict[str, Any]] = None 402| # Home node info 403| home_node: Optional[HomeNodeView] = None 404| 405| 406|class CitizenInteractionInfo(BaseModel): 407| slug: str 408| display_name: str 409| primary_room_slug: Optional[str] = None 410| primary_room_id: Optional[str] = None 411| primary_room_name: Optional[str] = None 412| matrix_user_id: Optional[str] = None 413| district: Optional[str] = None 414| microdao_slug: Optional[str] = None 415| microdao_name: Optional[str] = None 416| 417| 418|class CitizenAskRequest(BaseModel): 419| question: str 420| context: Optional[str] = None 421| 422| 423|class CitizenAskResponse(BaseModel): 424| answer: str 425| agent_display_name: str 426| agent_id: str 427| 428| 429|# ============================================================================= 430|# MicroDAO 431|# ============================================================================= 432| 433|class MicrodaoCitizenView(BaseModel): 434| slug: str 435| display_name: str 436| public_title: Optional[str] = None 437| public_tagline: Optional[str] = None 438| avatar_url: Optional[str] = None 439| district: Optional[str] = None 440| primary_room_slug: Optional[str] = None 441| 442| 443|class MicrodaoSummary(BaseModel): 444| """MicroDAO summary for list view""" 445| id: str 446| slug: str 447| name: str 448| description: Optional[str] = None 449| district: Optional[str] = None 450| 451| # Visibility & type 452| is_public: bool = True 453| is_platform: bool = False # Is a platform/district 454| is_active: bool = True 455| 456| # Orchestrator 457| orchestrator_agent_id: Optional[str] = None 458| orchestrator_agent_name: Optional[str] = None 459| 460| # Hierarchy 461| parent_microdao_id: Optional[str] = None 462| parent_microdao_slug: Optional[str] = None 463| 464| # Stats 465| logo_url: Optional[str] = None 466| banner_url: Optional[str] = None 467| member_count: int = 0 # alias for agents_count 468| agents_count: int = 0 # backward compatibility 469| room_count: int = 0 # alias for rooms_count 470| rooms_count: int = 0 # backward compatibility 471| channels_count: int = 0 472| 473| 474|class MicrodaoChannelView(BaseModel): 475| """Channel/integration view for MicroDAO""" 476| kind: str # 'matrix' | 'telegram' | 'city_room' | 'crew' 477| ref_id: str 478| display_name: Optional[str] = None 479| is_primary: bool 480| 481| 482|class MicrodaoAgentView(BaseModel): 483| """Agent view within MicroDAO""" 484| agent_id: str 485| display_name: str 486| role: Optional[str] = None 487| is_core: bool 488| 489| 490|class CityRoomSummary(BaseModel): 491| """Summary of a city room for chat embedding and multi-room support""" 492| id: str 493| slug: str 494| name: str 495| matrix_room_id: Optional[str] = None 496| microdao_id: Optional[str] = None 497| microdao_slug: Optional[str] = None 498| room_role: Optional[str] = None # 'primary', 'lobby', 'team', 'research', 'security', 'governance', 'orchestrator_team' 499| is_public: bool = True 500| sort_order: int = 100 501| logo_url: Optional[str] = None 502| banner_url: Optional[str] = None 503| 504| 505|class MicrodaoRoomsList(BaseModel): 506| """List of rooms belonging to a MicroDAO""" 507| microdao_id: str 508| microdao_slug: str 509| rooms: List[CityRoomSummary] = [] 510| 511| 512|class MicrodaoRoomUpdate(BaseModel): 513| """Update request for MicroDAO room settings""" 514| room_role: Optional[str] = None 515| is_public: Optional[bool] = None 516| sort_order: Optional[int] = None 517| set_primary: Optional[bool] = None # if true, mark as primary 518| 519| 520|class AttachExistingRoomRequest(BaseModel): 521| """Request to attach an existing city room to a MicroDAO""" 522| room_id: str 523| room_role: Optional[str] = None 524| is_public: bool = True 525| sort_order: int = 100 526| 527| 528|class MicrodaoDetail(BaseModel): 529| """Full MicroDAO detail view""" 530| id: str 531| slug: str 532| name: str 533| description: Optional[str] = None 534| district: Optional[str] = None 535| 536| # Visibility & type 537| is_public: bool = True 538| is_platform: bool = False 539| is_active: bool = True 540| 541| # Orchestrator 542| orchestrator_agent_id: Optional[str] = None 543| orchestrator_display_name: Optional[str] = None 544| 545| # Hierarchy 546| parent_microdao_id: Optional[str] = None 547| parent_microdao_slug: Optional[str] = None 548| child_microdaos: List["MicrodaoSummary"] = [] 549| 550| # Content 551| logo_url: Optional[str] = None 552| banner_url: Optional[str] = None 553| agents: List[MicrodaoAgentView] = [] 554| channels: List[MicrodaoChannelView] = [] 555| 556| # Multi-room support 557| rooms: List[CityRoomSummary] = [] 558| public_citizens: List[MicrodaoCitizenView] = [] 559| 560| # Primary city room for chat 561| primary_city_room: Optional[CityRoomSummary] = None 562| 563| 564|class AgentMicrodaoMembership(BaseModel): 565| microdao_id: str 566| microdao_slug: str 567| microdao_name: str 568| logo_url: Optional[str] = None 569| role: Optional[str] = None 570| is_core: bool = False 571| 572| 573|class MicrodaoOption(BaseModel): 574| id: str 575| slug: str 576| name: str 577| district: Optional[str] = None 578| is_active: bool = True 579| 580| 581|# ============================================================================= 582|# Visibility Updates (Task 029) 583|# ============================================================================= 584| 585|class AgentVisibilityUpdate(BaseModel): 586| """Update agent visibility settings""" 587| is_public: bool 588| visibility_scope: Optional[str] = None # 'global' | 'microdao' | 'private' 589| 590| 591|class MicrodaoVisibilityUpdate(BaseModel): 592| """Update MicroDAO visibility settings""" 593| is_public: bool 594| is_platform: Optional[bool] = None # Upgrade to platform/district 595| 596| 597|class MicrodaoCreateRequest(BaseModel): 598| """Request to create MicroDAO from agent (orchestrator flow)""" 599| name: str 600| slug: str 601| description: Optional[str] = None 602| make_platform: bool = False # If true -> is_platform = true 603| is_public: bool = True 604| parent_microdao_id: Optional[str] = None 605| 606| 607|class SwapperModel(BaseModel): 608| """Model info from Swapper service""" 609| name: str 610| loaded: bool 611| type: Optional[str] = None 612| vram_gb: Optional[float] = None 613| 614| 615|class NodeSwapperDetail(BaseModel): 616| """Detailed Swapper info for Node Cabinet""" 617| node_id: str 618| healthy: bool 619| models_loaded: int 620| models_total: int 621| models: List[SwapperModel] = []