"""pydantic v2 response models for the my-deepagent HTTP API. These shapes are stable contracts the Web GUI depends on. Internal ORM models in `my_deepagent.persistence.models` are NOT exposed directly; routes convert ORM rows into these DTOs. """ from __future__ import annotations from pydantic import BaseModel, ConfigDict, Field class _Strict(BaseModel): """Base for API DTOs — extra=forbid to catch typos at deserialization time.""" model_config = ConfigDict(extra="forbid", frozen=True) # --------------------------------------------------------------------------- # /api/runs # --------------------------------------------------------------------------- class RunSummary(_Strict): id: str state: str repo_path: str base_branch: str worktree_root: str created_at: str ended_at: str | None = None final_report_path: str | None = None class PhaseInfo(_Strict): id: str phase_key: str seq: int state: str attempts: int started_at: str | None = None ended_at: str | None = None class ArtifactInfo(_Strict): id: str phase_id: str schema_id: str path: str valid: bool created_at: str class EventInfo(_Strict): seq: int phase_id: str | None = None type: str ts: str payload: dict[str, object] | None = None class RunDetail(_Strict): run: RunSummary phases: list[PhaseInfo] artifacts: list[ArtifactInfo] events: list[EventInfo] # --------------------------------------------------------------------------- # /api/runs POST body # --------------------------------------------------------------------------- class StartRunRequest(BaseModel): """User-submitted body for POST /api/runs (NOT frozen — input not response).""" model_config = ConfigDict(extra="forbid") template_path: str = Field(min_length=1) repo_path: str = Field(min_length=1) base_branch: str = "main" requirements_md: str = "" override: dict[str, str] | None = None class StartRunResponse(_Strict): run_id: str state: str message: str = "started" # --------------------------------------------------------------------------- # /api/personas # --------------------------------------------------------------------------- class PersonaSummary(_Strict): name: str version: int description: str | None = None model: str capabilities: list[str] max_risk_level: str # --------------------------------------------------------------------------- # /api/workflows # --------------------------------------------------------------------------- class WorkflowRoleSummary(_Strict): id: str required_capabilities: list[str] class WorkflowPhaseSummary(_Strict): key: str title: str risk: str role: str class WorkflowSummary(_Strict): path: str # relative path under docs/schemas/workflows/ name: str version: int description: str | None = None roles: list[WorkflowRoleSummary] phases: list[WorkflowPhaseSummary] # v0.4 — workflow generator UI (POST /api/workflows) class WorkflowRoleSpec(_Strict): """Input shape for one role inside a CreateWorkflowRequest.""" id: str = Field(min_length=1, pattern=r"^[a-z][a-z0-9_]*$") required_capabilities: list[str] = Field(min_length=1) preferred_backends: list[str] = Field(default_factory=list) fallback_personas: list[str] = Field(default_factory=list) class WorkflowArtifactSpec(_Strict): """Input shape for one phase's expected_artifact (optional).""" path: str = Field(min_length=1) # YAML key is `schema`; pydantic attribute aliased to avoid BaseModel.schema clash schema_id: str = Field(min_length=1, alias="schema") class WorkflowPhaseSpec(_Strict): """Input shape for one phase inside a CreateWorkflowRequest.""" key: str = Field(min_length=1, pattern=r"^[a-z][a-z0-9_]*$") title: str = Field(min_length=1) risk: str = Field(min_length=1) # low|medium|high — validated by WorkflowTemplate role: str = Field(min_length=1) instructions: str = Field(min_length=10) expected_artifact: WorkflowArtifactSpec | None = None gates: list[str] = Field(default_factory=list) timeout_seconds: int | None = Field(default=None, ge=1) max_budget_usd: float | None = Field(default=None, ge=0) class CreateWorkflowRequest(_Strict): """Body for POST /api/workflows — saves a new template YAML on disk.""" name: str = Field(min_length=1) version: int = Field(ge=1) description: str | None = None roles: list[WorkflowRoleSpec] = Field(min_length=1) phases: list[WorkflowPhaseSpec] = Field(min_length=1) default_gates: list[str] = Field(default_factory=list) max_total_budget_usd: float | None = Field(default=None, ge=0) class CreateWorkflowResponse(_Strict): """Returned by POST /api/workflows.""" path: str # absolute path of the saved YAML name: str version: int # --------------------------------------------------------------------------- # /api/budget # --------------------------------------------------------------------------- class BudgetScopeEntry(_Strict): scope: str spent_usd: float cap_usd: float | None warn_usd: float | None = None class BudgetSummary(_Strict): day: BudgetScopeEntry | None runs: list[BudgetScopeEntry] personas: list[BudgetScopeEntry] # --------------------------------------------------------------------------- # /api/sessions (v0.3 PR #1) # --------------------------------------------------------------------------- class SessionSummary(_Strict): id: str state: str persona_id: str model: str | None title: str | None started_at: str | None last_message_at: str | None ended_at: str | None total_input_tokens: int total_output_tokens: int parent_session_id: str | None depth: int class MessageInfo(_Strict): seq: int role: str content: str tool_calls: dict[str, object] | None = None token_count: int is_summary: bool archived: bool ts: str class SessionDetail(_Strict): session: SessionSummary messages: list[MessageInfo] class CreateSessionRequest(BaseModel): """POST /api/sessions body.""" model_config = ConfigDict(extra="forbid") persona_name: str | None = Field(default=None, min_length=1) model_override: str | None = Field(default=None, min_length=1) repo_path: str = Field(default=".", min_length=1) class PostMessageRequest(BaseModel): """POST /api/sessions/{id}/messages body.""" model_config = ConfigDict(extra="forbid") content: str = Field(min_length=1) class SessionAck(_Strict): session_id: str state: str message: str = "ok"