promptlooper/backend/schemas.py
John Lightner 42668eeeb1 MAESTRO: Create backend/schemas.py with all Pydantic request/response schemas
Create/update/response schemas for Project, Experiment, Run, Endpoint,
Webhook, Score, Auth (setup/login/token), Export, and Health. All use
Pydantic v2 ConfigDict(from_attributes=True) for ORM compatibility.
RunDetailResponse nests StageResults and Scores. ExportRunRow provides
flat scorer_name→value dict for CSV/JSON export. 30 tests added.
2026-04-07 01:54:02 -05:00

298 lines
7.7 KiB
Python

"""PromptLooper Pydantic request/response schemas."""
import uuid
from datetime import datetime
from pydantic import BaseModel, ConfigDict, Field
from models import ExperimentStatus, RunStatus
# ---------------------------------------------------------------------------
# Shared mixins
# ---------------------------------------------------------------------------
class _TimestampMixin(BaseModel):
created_at: datetime
updated_at: datetime
# ---------------------------------------------------------------------------
# Project
# ---------------------------------------------------------------------------
class ProjectCreate(BaseModel):
name: str = Field(..., min_length=1, max_length=255)
description: str | None = None
class ProjectUpdate(BaseModel):
name: str | None = Field(None, min_length=1, max_length=255)
description: str | None = None
class ProjectResponse(BaseModel):
model_config = ConfigDict(from_attributes=True)
id: uuid.UUID
name: str
description: str | None
owner_id: uuid.UUID
created_at: datetime
updated_at: datetime
class ProjectListResponse(BaseModel):
items: list[ProjectResponse]
total: int
# ---------------------------------------------------------------------------
# Experiment
# ---------------------------------------------------------------------------
class ExperimentCreate(BaseModel):
name: str = Field(..., min_length=1, max_length=255)
description: str | None = None
sample_data: dict | None = None
pipeline_stages: dict | None = None
scoring_config: dict | None = None
parameter_space: dict | None = None
class ExperimentUpdate(BaseModel):
name: str | None = Field(None, min_length=1, max_length=255)
description: str | None = None
sample_data: dict | None = None
pipeline_stages: dict | None = None
scoring_config: dict | None = None
parameter_space: dict | None = None
status: ExperimentStatus | None = None
class ExperimentResponse(BaseModel):
model_config = ConfigDict(from_attributes=True)
id: uuid.UUID
project_id: uuid.UUID
name: str
description: str | None
sample_data: dict | None
pipeline_stages: dict | None
scoring_config: dict | None
parameter_space: dict | None
status: ExperimentStatus
created_at: datetime
updated_at: datetime
class ExperimentListResponse(BaseModel):
items: list[ExperimentResponse]
total: int
# ---------------------------------------------------------------------------
# Run
# ---------------------------------------------------------------------------
class RunResponse(BaseModel):
model_config = ConfigDict(from_attributes=True)
id: uuid.UUID
experiment_id: uuid.UUID
config_hash: str
config: dict
status: RunStatus
started_at: datetime | None
completed_at: datetime | None
duration_ms: int | None
tokens_in: int | None
tokens_out: int | None
cost_estimate: float | None
class RunListResponse(BaseModel):
items: list[RunResponse]
total: int
# ---------------------------------------------------------------------------
# StageResult (read-only, returned inside Run details)
# ---------------------------------------------------------------------------
class StageResultResponse(BaseModel):
model_config = ConfigDict(from_attributes=True)
id: uuid.UUID
run_id: uuid.UUID
stage_index: int
prompt_sent: str
response_raw: str
model_used: str
parameters: dict | None
tokens_in: int | None
tokens_out: int | None
latency_ms: int | None
class RunDetailResponse(RunResponse):
"""Run with nested stage results and scores."""
stage_results: list[StageResultResponse] = []
scores: list["ScoreResponse"] = []
# ---------------------------------------------------------------------------
# Score
# ---------------------------------------------------------------------------
class ScoreInput(BaseModel):
scorer_name: str = Field(..., min_length=1, max_length=255)
value: float
metadata: dict | None = None
class ScoreResponse(BaseModel):
model_config = ConfigDict(from_attributes=True)
id: uuid.UUID
run_id: uuid.UUID
scorer_name: str
value: float
scorer_metadata: dict | None
created_at: datetime
# ---------------------------------------------------------------------------
# Endpoint (LLM endpoint configuration)
# ---------------------------------------------------------------------------
class EndpointCreate(BaseModel):
name: str = Field(..., min_length=1, max_length=255)
url: str = Field(..., min_length=1, max_length=2048)
api_key: str | None = None
default_model: str | None = Field(None, max_length=255)
class EndpointUpdate(BaseModel):
name: str | None = Field(None, min_length=1, max_length=255)
url: str | None = Field(None, min_length=1, max_length=2048)
api_key: str | None = None
default_model: str | None = Field(None, max_length=255)
class EndpointResponse(BaseModel):
model_config = ConfigDict(from_attributes=True)
id: uuid.UUID
name: str
url: str
default_model: str | None
class EndpointListResponse(BaseModel):
items: list[EndpointResponse]
total: int
# ---------------------------------------------------------------------------
# Webhook
# ---------------------------------------------------------------------------
class WebhookCreate(BaseModel):
event_type: str = Field(..., min_length=1, max_length=255)
url: str = Field(..., min_length=1, max_length=2048)
headers: dict | None = None
is_active: bool = True
class WebhookUpdate(BaseModel):
event_type: str | None = Field(None, min_length=1, max_length=255)
url: str | None = Field(None, min_length=1, max_length=2048)
headers: dict | None = None
is_active: bool | None = None
class WebhookResponse(BaseModel):
model_config = ConfigDict(from_attributes=True)
id: uuid.UUID
event_type: str
url: str
headers: dict | None
is_active: bool
class WebhookListResponse(BaseModel):
items: list[WebhookResponse]
total: int
# ---------------------------------------------------------------------------
# Auth
# ---------------------------------------------------------------------------
class SetupRequest(BaseModel):
username: str = Field(..., min_length=1, max_length=255)
password: str = Field(..., min_length=8)
class LoginRequest(BaseModel):
username: str
password: str
class TokenResponse(BaseModel):
access_token: str
token_type: str = "bearer"
class UserResponse(BaseModel):
model_config = ConfigDict(from_attributes=True)
id: uuid.UUID
username: str
is_admin: bool
created_at: datetime
# ---------------------------------------------------------------------------
# Export
# ---------------------------------------------------------------------------
class ExportRunRow(BaseModel):
"""Flat row for CSV/JSON export of run results."""
run_id: uuid.UUID
experiment_id: uuid.UUID
config_hash: str
config: dict
status: RunStatus
duration_ms: int | None = None
tokens_in: int | None = None
tokens_out: int | None = None
cost_estimate: float | None = None
scores: dict[str, float] = Field(
default_factory=dict,
description="Map of scorer_name → value",
)
class ExportResponse(BaseModel):
experiment_id: uuid.UUID
experiment_name: str
rows: list[ExportRunRow]
# ---------------------------------------------------------------------------
# Health
# ---------------------------------------------------------------------------
class HealthResponse(BaseModel):
status: str = "ok"
database: bool
redis: bool
# Rebuild forward refs for RunDetailResponse
RunDetailResponse.model_rebuild()