feat(automation): add Pydantic schemas for the automation definition

Three layers of Pydantic models under app/automations/schemas/, one
file per concern (SRP), matching the envelope in
automation-design-plan.md §5.

definition/ — the editable envelope persisted in
automations.definition:
  - envelope.py       AutomationDefinition (top-level shape)
  - plan_step.py      PlanStep (one step in the sequential plan)
  - inputs.py         InputsBlock (the inputs JSON Schema wrapper)
  - execution.py      ExecutionBlock (timeouts, retries, concurrency,
                                      budget cap, on_failure plan)
  - metadata.py       MetadataBlock (tags + created_from_nl + extras)
  - trigger_spec.py   TriggerSpec (one entry in triggers[])

triggers/ — per-trigger config schemas, dispatched by registry on the
TriggerSpec.type discriminator:
  - schedule.py       ScheduleTriggerConfig(cron, timezone)
  - manual.py         ManualTriggerConfig() — empty in v1

actions/ — per-action config schemas, dispatched by registry on the
PlanStep.action discriminator:
  - agent_task.py     AgentTaskActionConfig(prompt, tools, model,
                                            output_schema)

Design properties verified by an inline smoke test:
  - The §5 worked example round-trips through model_validate_json /
    model_dump_json byte-for-byte (InputsBlock uses
    serialize_by_alias so the JSON key stays "schema" not
    "schema_").
  - Envelope rejects unknown top-level keys (extra="forbid").
  - MetadataBlock tolerates unknown keys (extra="allow").
  - ExecutionBlock defaults apply when the block is omitted.
  - retry_backoff and concurrency are typed as Literal — bogus
    values rejected at validation time.
  - Per-type configs enforce their required fields (cron + timezone
    on schedule; non-empty prompt on agent_task).

The envelope keeps trigger and action configs as untyped dicts on
purpose — per-type validation is a registry-driven dispatch (commit
10), keeping the envelope free of every-type-knows-every-type
coupling.
This commit is contained in:
CREDO23 2026-05-26 22:50:52 +02:00
parent d9183464d9
commit be4d43d6c9
13 changed files with 539 additions and 4 deletions

View file

@ -2,4 +2,10 @@
from __future__ import annotations
__all__: list[str] = []
from .manual import ManualTriggerConfig
from .schedule import ScheduleTriggerConfig
__all__ = [
"ManualTriggerConfig",
"ScheduleTriggerConfig",
]

View file

@ -0,0 +1,21 @@
"""``ManualTriggerConfig`` — config for the ``manual`` trigger type (empty in v1)."""
from __future__ import annotations
from pydantic import BaseModel, ConfigDict
class ManualTriggerConfig(BaseModel):
"""Config for the UI-driven ``manual`` trigger.
Validated against ``AutomationTrigger.config`` whenever the
persisted ``type`` is ``manual``. v1 carries no configurable
fields the "Run now" affordance simply fires this trigger with
an empty config object. The model exists so the registry dispatch
is uniform across all trigger types.
Future versions may add fields here (e.g., a fixed prompt to
pre-fill the run dialog with) without breaking v1 payloads.
"""
model_config = ConfigDict(extra="forbid")

View file

@ -0,0 +1,33 @@
"""``ScheduleTriggerConfig`` — config for the ``schedule`` trigger type."""
from __future__ import annotations
from pydantic import BaseModel, ConfigDict, Field
class ScheduleTriggerConfig(BaseModel):
"""Config for a cron-driven trigger.
Validated against ``AutomationTrigger.config`` whenever the
persisted ``type`` is ``schedule``. The cron expression is
evaluated by Celery Beat's source; the timezone is an IANA name
(e.g., ``Africa/Kigali``) and is required so the user's cron is
unambiguous across DST boundaries.
"""
model_config = ConfigDict(extra="forbid")
cron: str = Field(
...,
description=(
"Five-field cron expression. Minimum resolution is one "
"minute; the form editor warns when intervals tighter "
"than 15 minutes are used."
),
examples=["0 9 * * 1-5"],
)
timezone: str = Field(
...,
description="IANA timezone name (e.g., 'Africa/Kigali', 'UTC').",
examples=["Africa/Kigali"],
)