"""Pydantic schemas for request/response validation.""" import uuid from datetime import datetime from pydantic import BaseModel, EmailStr, Field # ─── Auth ─────────────────────────────────────────────── class UserRegister(BaseModel): """Legacy combined registration - kept for backward compatibility.""" username: str = Field(min_length=1, max_length=100) email: EmailStr password: str = Field(min_length=6, max_length=128) display_name: str | None = None invitation_code: str | None = None # SSO registration fields provider: str | None = Field(None, description="Provider type for SSO registration (feishu, dingtalk, etc.)") provider_code: str | None = Field(None, description="OAuth code for SSO registration") class RegisterInitRequest(BaseModel): """Step 1: Initialize registration with account credentials.""" username: str = Field(min_length=1, max_length=100) email: EmailStr password: str = Field(min_length=6, max_length=128) display_name: str | None = None target_tenant_id: uuid.UUID | None = None class RegisterInitResponse(BaseModel): """Response after step 1 - user created, needs email verification.""" user_id: uuid.UUID email: str access_token: str message: str = "Registration initiated. Please verify your email." user: "UserOut" # Include full user info needs_company_setup: bool = True target_tenant_id: uuid.UUID | None = None class RegisterCompleteRequest(BaseModel): """Step 3: Complete registration after email verification.""" token: str = Field(min_length=6, max_length=512, description="Email verification code") class RegisterCompleteResponse(BaseModel): """Response after successful registration completion.""" access_token: str token_type: str = "bearer" user: "UserOut" needs_company_setup: bool = False class SSORegisterRequest(BaseModel): """SSO registration - completely separate from normal registration.""" provider: str = Field(description="Provider type (feishu, dingtalk, etc.)") code: str = Field(description="OAuth authorization code from provider") invitation_code: str | None = None class UserLogin(BaseModel): login_identifier: str = Field(description="Email address for login") password: str tenant_id: uuid.UUID | None = None # Optional: when set, restrict login to users of this tenant class ForgotPasswordRequest(BaseModel): email: EmailStr class ResetPasswordRequest(BaseModel): token: str = Field(min_length=20, max_length=512) new_password: str = Field(min_length=6, max_length=128) class VerifyEmailRequest(BaseModel): token: str = Field(min_length=6, max_length=512) class ResendVerificationRequest(BaseModel): email: EmailStr class NeedsVerificationResponse(BaseModel): """Response when user needs to verify email before continuing.""" needs_verification: bool = True email: str message: str = "Email already registered but not verified. Please enter the verification code." class TokenResponse(BaseModel): access_token: str token_type: str = "bearer" user: "UserOut" identity: "IdentityOut | None" = None needs_company_setup: bool = False tenant_name: str | None = None class TenantChoice(BaseModel): """Multi-tenant login: tenant selection info.""" tenant_id: uuid.UUID | None tenant_name: str tenant_slug: str class MultiTenantResponse(BaseModel): """Response when multiple tenants match the same login identifier.""" requires_tenant_selection: bool = True login_identifier: str tenants: list[TenantChoice] class TenantSwitchRequest(BaseModel): tenant_id: uuid.UUID class TenantSwitchResponse(BaseModel): access_token: str token_type: str = "bearer" redirect_url: str | None = None message: str | None = None class IdentityOut(BaseModel): """Global identity information.""" id: uuid.UUID email: str | None = None phone: str | None = None username: str | None = None is_active: bool is_platform_admin: bool email_verified: bool created_at: datetime updated_at: datetime model_config = {"from_attributes": True} class UserOut(BaseModel): id: uuid.UUID identity_id: uuid.UUID | None = None username: str | None = None email: str | None = None display_name: str avatar_url: str | None = None role: str tenant_id: uuid.UUID | None = None title: str | None = None primary_mobile: str | None = None registration_source: str | None = None is_active: bool email_verified: bool = True created_at: datetime model_config = {"from_attributes": True} class IdentityProviderOut(BaseModel): id: uuid.UUID provider_type: str name: str is_active: bool sso_login_enabled: bool = False config: dict | None = None tenant_id: uuid.UUID | None = None updated_at: datetime | None = None created_at: datetime sso_domain: str | None = None model_config = {"from_attributes": True} class OAuthAuthorizeResponse(BaseModel): authorization_url: str class OAuthCallbackRequest(BaseModel): code: str state: str class IdentityBindRequest(BaseModel): provider_type: str code: str # OAuth code for binding class IdentityUnbindRequest(BaseModel): provider_type: str class UserUpdate(BaseModel): username: str | None = None email: EmailStr | None = None display_name: str | None = None avatar_url: str | None = None title: str | None = None primary_mobile: str | None = None # ─── Agent ────────────────────────────────────────────── class AgentCreate(BaseModel): name: str = Field(min_length=2, max_length=100, description="Agent name, 2-100 characters") agent_type: str = "native" # native | openclaw role_description: str = Field(default="", max_length=500, description="Role description, max 500 characters") bio: str | None = None welcome_message: str | None = None avatar_url: str | None = None # Soul personality: str = "" boundaries: str = "" # Model primary_model_id: uuid.UUID | None = None fallback_model_id: uuid.UUID | None = None # Permissions permission_scope_type: str = "company" # company | user permission_scope_ids: list[uuid.UUID] = [] permission_access_level: str = "use" # use | manage # Target tenant (admin-only override; otherwise ignored) tenant_id: uuid.UUID | None = None # Template template_id: uuid.UUID | None = None # Autonomy autonomy_policy: dict | None = None # Token limits max_tokens_per_day: int | None = None max_tokens_per_month: int | None = None # Skills to copy into agent workspace skill_ids: list[uuid.UUID] = [] class AgentOut(BaseModel): id: uuid.UUID name: str avatar_url: str | None = None role_description: str bio: str | None = None welcome_message: str | None = None status: str creator_id: uuid.UUID creator_username: str | None = None # Populated by API layer; not in ORM model directly primary_model_id: uuid.UUID | None = None fallback_model_id: uuid.UUID | None = None autonomy_policy: dict tokens_used_today: int tokens_used_month: int tokens_used_total: int = 0 max_tokens_per_day: int | None = None max_tokens_per_month: int | None = None context_window_size: int = 100 max_tool_rounds: int = 50 max_triggers: int = 20 min_poll_interval_min: int = 5 webhook_rate_limit: int = 5 heartbeat_enabled: bool = True heartbeat_interval_minutes: int = 240 heartbeat_active_hours: str = "09:00-18:00" last_heartbeat_at: datetime | None = None timezone: str | None = None expires_at: datetime | None = None is_expired: bool = False llm_calls_today: int = 0 max_llm_calls_per_day: int = 100 agent_type: str = "native" openclaw_last_seen: datetime | None = None has_api_key: bool = False api_key_hash: str | None = None created_at: datetime last_active_at: datetime | None = None model_config = {"from_attributes": True} class AgentUpdate(BaseModel): name: str | None = None role_description: str | None = None bio: str | None = None welcome_message: str | None = None avatar_url: str | None = None autonomy_policy: dict | None = None primary_model_id: uuid.UUID | None = None fallback_model_id: uuid.UUID | None = None context_window_size: int | None = Field(default=None, ge=1, le=500) max_tokens_per_day: int | None = None max_tokens_per_month: int | None = None max_tool_rounds: int | None = None max_triggers: int | None = None min_poll_interval_min: int | None = None webhook_rate_limit: int | None = None heartbeat_enabled: bool | None = None heartbeat_interval_minutes: int | None = None heartbeat_active_hours: str | None = None timezone: str | None = None expires_at: datetime | None = None # Admin only — extend agent expiry class AgentStatusOut(BaseModel): """Agent status from state.json.""" agent_id: uuid.UUID name: str status: str current_task: str | None = None last_active: datetime | None = None channel_status: dict = {} stats: dict = {} # ─── Task ─────────────────────────────────────────────── class TaskCreate(BaseModel): title: str = Field(min_length=1, max_length=500) description: str | None = None type: str = "todo" # todo | supervision priority: str = "medium" due_date: datetime | None = None # Supervision fields supervision_target_name: str | None = None supervision_channel: str | None = None remind_schedule: str | None = None class TaskOut(BaseModel): id: uuid.UUID agent_id: uuid.UUID title: str description: str | None = None type: str status: str priority: str assignee: str created_by: uuid.UUID creator_username: str | None = None due_date: datetime | None = None supervision_target_name: str | None = None supervision_channel: str | None = None remind_schedule: str | None = None created_at: datetime updated_at: datetime completed_at: datetime | None = None model_config = {"from_attributes": True} class TaskUpdate(BaseModel): title: str | None = None description: str | None = None status: str | None = None priority: str | None = None due_date: datetime | None = None supervision_target_name: str | None = None remind_schedule: str | None = None class TaskLogCreate(BaseModel): content: str class TaskLogOut(BaseModel): id: uuid.UUID task_id: uuid.UUID content: str created_at: datetime model_config = {"from_attributes": True} # ─── LLM ──────────────────────────────────────────────── class LLMModelCreate(BaseModel): provider: str model: str api_key: str base_url: str | None = None label: str temperature: float | None = Field(None, ge=0.0, le=2.0) max_tokens_per_day: int | None = None enabled: bool = True supports_vision: bool = False max_output_tokens: int | None = None request_timeout: int | None = None class LLMModelUpdate(BaseModel): provider: str | None = None model: str | None = None api_key: str | None = None base_url: str | None = None label: str | None = None temperature: float | None = Field(None, ge=0.0, le=2.0) max_tokens_per_day: int | None = None enabled: bool | None = None supports_vision: bool | None = None max_output_tokens: int | None = None request_timeout: int | None = None class LLMModelOut(BaseModel): id: uuid.UUID provider: str model: str base_url: str | None = None label: str temperature: float | None = None api_key_masked: str = "" max_tokens_per_day: int | None = None enabled: bool supports_vision: bool = False max_output_tokens: int | None = None request_timeout: int | None = None created_at: datetime model_config = {"from_attributes": True} # ─── Channel Config ───────────────────────────────────── class ChannelConfigCreate(BaseModel): channel_type: str = "feishu" app_id: str app_secret: str encrypt_key: str | None = None verification_token: str | None = None extra_config: dict | None = None class ChannelConfigOut(BaseModel): id: uuid.UUID agent_id: uuid.UUID channel_type: str app_id: str | None = None app_secret: str | None = None encrypt_key: str | None = None is_configured: bool is_connected: bool last_tested_at: datetime | None = None extra_config: dict | None = None created_at: datetime model_config = {"from_attributes": True} # ─── Approval ─────────────────────────────────────────── class ApprovalRequestOut(BaseModel): id: uuid.UUID agent_id: uuid.UUID agent_name: str | None = None action_type: str details: dict status: str created_at: datetime resolved_at: datetime | None = None resolved_by: uuid.UUID | None = None model_config = {"from_attributes": True} class ApprovalAction(BaseModel): action: str # "approve" | "reject" # ─── Enterprise Info ──────────────────────────────────── class UserInviteRequest(BaseModel): emails: list[EmailStr] = Field(..., description="List of emails to invite") class EnterpriseInfoUpdate(BaseModel): content: dict visible_roles: list[str] = [] class EnterpriseInfoOut(BaseModel): id: uuid.UUID info_type: str content: dict version: int visible_roles: list updated_at: datetime model_config = {"from_attributes": True} # ─── Chat ─────────────────────────────────────────────── class ChatMessageOut(BaseModel): id: uuid.UUID agent_id: uuid.UUID user_id: uuid.UUID role: str content: str thinking: str | None = None created_at: datetime model_config = {"from_attributes": True} class ChatSend(BaseModel): content: str = Field(min_length=1) # ─── Audit Log ────────────────────────────────────────── class AuditLogOut(BaseModel): id: uuid.UUID user_id: uuid.UUID | None = None agent_id: uuid.UUID | None = None action: str details: dict ip_address: str | None = None created_at: datetime model_config = {"from_attributes": True} # ─── Generic ──────────────────────────────────────────── class PaginatedResponse(BaseModel): items: list total: int page: int = 1 page_size: int = 20 class HealthResponse(BaseModel): status: str = "ok" version: str # ─── Gateway (OpenClaw) ───────────────────────────────── class GatewayHistoryItem(BaseModel): role: str # "user" or "assistant" content: str sender_name: str | None = None created_at: datetime class GatewayRelationshipItem(BaseModel): name: str type: str # "human" or "agent" role: str | None = None # e.g. "collaborator", "supervisor" description: str | None = None channels: list[str] = [] # e.g. ["feishu"], ["agent"] class GatewayMessageOut(BaseModel): id: uuid.UUID conversation_id: str | None = None sender_agent_name: str | None = None sender_user_name: str | None = None sender_user_id: str | None = None content: str created_at: datetime history: list[GatewayHistoryItem] = [] class GatewayPollResponse(BaseModel): messages: list[GatewayMessageOut] = [] relationships: list[GatewayRelationshipItem] = [] class GatewayReportRequest(BaseModel): message_id: uuid.UUID result: str = Field(min_length=1) class GatewaySendMessageRequest(BaseModel): target: str # Name of target person or agent content: str = Field(min_length=1) channel: str | None = None # Optional: "feishu", "agent", etc. Auto-detected if omitted.