mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-05-31 19:45:15 +02:00
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:
parent
d9183464d9
commit
be4d43d6c9
13 changed files with 539 additions and 4 deletions
|
|
@ -0,0 +1,76 @@
|
|||
"""``ExecutionBlock`` — the ``execution`` section of the automation definition."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Literal
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
|
||||
from .plan_step import PlanStep
|
||||
|
||||
|
||||
class ExecutionBlock(BaseModel):
|
||||
"""The ``execution`` block of an ``AutomationDefinition``.
|
||||
|
||||
Carries automation-wide defaults that individual ``PlanStep``s
|
||||
can override. Every field has a sane default so an automation
|
||||
definition may omit the block entirely; in that case all defaults
|
||||
apply.
|
||||
|
||||
``on_failure`` is a secondary plan that runs only when the main
|
||||
``plan`` fails after retries exhaust. It uses the same
|
||||
``PlanStep`` shape as the main plan and shares the same execution
|
||||
semantics.
|
||||
"""
|
||||
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
timeout_seconds: int = Field(
|
||||
default=600,
|
||||
gt=0,
|
||||
description=(
|
||||
"Hard wall-clock cap for the entire run. The executor "
|
||||
"transitions the run to ``timed_out`` when this is "
|
||||
"exceeded."
|
||||
),
|
||||
)
|
||||
max_retries: int = Field(
|
||||
default=2,
|
||||
ge=0,
|
||||
description=(
|
||||
"Per-step retry budget applied when a step raises a "
|
||||
"retryable error. Steps may override per-step."
|
||||
),
|
||||
)
|
||||
retry_backoff: Literal["exponential", "linear", "none"] = Field(
|
||||
default="exponential",
|
||||
description="Backoff policy between retries.",
|
||||
)
|
||||
concurrency: Literal[
|
||||
"drop_if_running", "queue", "always"
|
||||
] = Field(
|
||||
default="drop_if_running",
|
||||
description=(
|
||||
"Behaviour when a new fire arrives while a previous run "
|
||||
"is still in progress. ``drop_if_running`` skips the new "
|
||||
"fire, ``queue`` enqueues it, ``always`` runs it in "
|
||||
"parallel."
|
||||
),
|
||||
)
|
||||
budget_cap_usd: float | None = Field(
|
||||
default=None,
|
||||
gt=0,
|
||||
description=(
|
||||
"Optional mid-flight cost cap in USD. The executor kills "
|
||||
"the run when accumulated cost exceeds this value. v1 "
|
||||
"treats this as an advisory because cost tracking lands "
|
||||
"with the executor in a later step."
|
||||
),
|
||||
)
|
||||
on_failure: list[PlanStep] = Field(
|
||||
default_factory=list,
|
||||
description=(
|
||||
"Secondary plan executed only when the main plan fails "
|
||||
"after retries exhaust. Empty list means no fallback."
|
||||
),
|
||||
)
|
||||
Loading…
Add table
Add a link
Reference in a new issue