"""Pydantic schemas — API request/response contracts. Mirrors the TypeScript types from the Electron app (src/shared/api-types.ts). """ from __future__ import annotations from enum import Enum from typing import Any, Literal from pydantic import BaseModel, Field # ── Billing ────────────────────────────────────────────────────────── BillingTier = Literal["free", "pro", "power", "team"] # ── Auth ───────────────────────────────────────────────────────────── class AuthTokens(BaseModel): access_token: str refresh_token: str expires_at: int class UserProfile(BaseModel): id: str email: str name: str | None = None surname: str | None = None tier: BillingTier # ── Chat ───────────────────────────────────────────────────────────── class ChatContext(BaseModel): user_profile: dict[str, Any] = Field(default_factory=dict) relevant_documents: list[str] = Field(default_factory=list) recent_tasks: list[dict[str, Any]] = Field(default_factory=list) conversation_history: list[dict[str, Any]] = Field(default_factory=list) class ChatRequest(BaseModel): message: str context: ChatContext = Field(default_factory=ChatContext) class ChatResponse(BaseModel): response: str # ── WebSocket Frame Protocol ────────────────────────────────────────── class WsFrameType(str, Enum): # ── v2 frame types (kept for backward compat) ────────────────────── chat_request = "chat_request" text_chunk = "text_chunk" tool_call = "tool_call" tool_result = "tool_result" final = "final" ping = "ping" device_hello = "device_hello" # ── v3 frame types ───────────────────────────────────────────────── home_request = "home_request" floating_request = "floating_request" stream_start = "stream_start" stream_text = "stream_text" stream_end = "stream_end" floating_domain = "floating_domain" data_request = "data_request" data_response = "data_response" mutation = "mutation" # ── v4 journey frame types ──────────────────────────────────────── journey_start = "journey_start" journey_message = "journey_message" journey_reply = "journey_reply" class WsToolCall(BaseModel): """Server → Client: requests a CRUD/vector operation on the local DB.""" type: Literal[WsFrameType.tool_call] = WsFrameType.tool_call id: str action: str table: str | None = None data: dict[str, Any] | None = None filters: dict[str, Any] | None = None vector: list[float] | None = None limit: int | None = None class WsToolResult(BaseModel): """Client → Server: result of a CRUD/vector operation.""" type: Literal[WsFrameType.tool_result] = WsFrameType.tool_result id: str row: dict[str, Any] | None = None rows: list[dict[str, Any]] | None = None results: list[dict[str, Any]] | None = None deleted: bool | None = None ok: bool | None = None error: str | None = None class WsTextChunk(BaseModel): """Server → Client: incremental LLM response text.""" type: Literal[WsFrameType.text_chunk] = WsFrameType.text_chunk text: str class WsFinal(BaseModel): """Server → Client: signals end of response with the complete text.""" type: Literal[WsFrameType.final] = WsFrameType.final response: str # ── WebSocket Agent Frame Protocol ──────────────────────────────────── class WsDeviceHello(BaseModel): """Client → Server: device identification on WS connect.""" type: Literal[WsFrameType.device_hello] = WsFrameType.device_hello device_id: str agent_ids: list[str] = Field(default_factory=list) # ── WebSocket v3 Frame Models ───────────────────────────────────────── class WsFloatingScope(BaseModel): """Scope for a floating request — narrows the agent to a specific entity.""" type: Literal["task", "project", "note", "timeline"] id: str | None = None class WsHomeRequest(BaseModel): """Client → Server: Home chat message.""" type: Literal[WsFrameType.home_request] = WsFrameType.home_request message: str conversation_history: list[dict[str, Any]] = Field(default_factory=list) class WsFloatingRequest(BaseModel): """Client → Server: Floating chat message scoped to an entity.""" type: Literal[WsFrameType.floating_request] = WsFrameType.floating_request message: str scope: WsFloatingScope class WsStreamStart(BaseModel): """Server → Client: signals start of a streaming response.""" type: Literal[WsFrameType.stream_start] = WsFrameType.stream_start request_id: str class WsStreamText(BaseModel): """Server → Client: streamed text token.""" type: Literal[WsFrameType.stream_text] = WsFrameType.stream_text request_id: str chunk: str class WsStreamEnd(BaseModel): """Server → Client: signals end of a streaming response.""" type: Literal[WsFrameType.stream_end] = WsFrameType.stream_end request_id: str class WsDomain(BaseModel): """Structured floating domain payload for UI routing decisions.""" type: Literal["task", "timeline", "project", "node"] id: str | None = None section: Literal["task", "timeline", "note"] | None = None class WsFloatingDomain(BaseModel): """Server → Client: domain determined for a floating request.""" type: Literal[WsFrameType.floating_domain] = WsFrameType.floating_domain request_id: str domain: WsDomain # ── Agent Config V2 ─────────────────────────────────────────────────── class ContentTypeConfig(BaseModel): """Per-type extraction config produced by the journey chatbot.""" id: str label: str = "" detection_hint: str = "" preprocessing: str = "generic" # handler name: "email_html", "plain_text", ... extraction_prompt: str class AgentConfig(BaseModel): """Structured agent configuration (replaces freeform prompt_template).""" content_types: list[ContentTypeConfig] = [] global_rules: list[str] = [] data_types: list[str] = [] # ── Agent Catalog ───────────────────────────────────────────────────── class AgentCatalogItem(BaseModel): type: str name: str description: str class AgentCreationCheckRequest(BaseModel): active_agents: int = Field(ge=0, default=0) class AgentCreationCheckResponse(BaseModel): allowed: bool tier: BillingTier active_agents: int limit: int class AgentTriggerRequest(BaseModel): directory: str = Field(min_length=1) device_id: str = Field(default="") agent_id: str | None = None # FE stable agent ID (electron-store UUID) what_to_extract: list[str] = Field(min_length=1) actions_by_type: dict[str, list[str]] | None = None batch_interval: str = Field(min_length=1) custom_agent_prompt: str = Field(min_length=1) active_agents: int = Field(ge=0, default=0) # ── Agent Run Log ───────────────────────────────────────────────────── class AgentRunLogResponse(BaseModel): id: str agent_id: str agent_type: Literal["local", "cloud"] status: Literal["running", "success", "error", "partial"] items_processed: int items_created: int errors: list[str] started_at: int completed_at: int | None # ── Chatbot Journey ───────────────────────────────────────────────────