mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-05-29 19:35:20 +02:00
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.
89 lines
2.9 KiB
Python
89 lines
2.9 KiB
Python
"""``AutomationDefinition`` — the top-level envelope persisted in ``automations.definition``."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from pydantic import BaseModel, ConfigDict, Field
|
|
|
|
from .execution import ExecutionBlock
|
|
from .inputs import InputsBlock
|
|
from .metadata import MetadataBlock
|
|
from .plan_step import PlanStep
|
|
from .trigger_spec import TriggerSpec
|
|
|
|
|
|
class AutomationDefinition(BaseModel):
|
|
"""The top-level JSON shape stored in ``automations.definition``.
|
|
|
|
This is the editable spec a user authors (or the NL generator
|
|
produces). The envelope is structural only — every nested
|
|
discriminator (``triggers[].type``, ``plan[].action``) is resolved
|
|
against the registries at validation time, so adding a new
|
|
trigger or action type does not require touching this schema.
|
|
|
|
See ``automation-design-plan.md`` §5 for the worked example and
|
|
rationale.
|
|
"""
|
|
|
|
model_config = ConfigDict(extra="forbid")
|
|
|
|
schema_version: str = Field(
|
|
default="1.0",
|
|
description=(
|
|
"Schema version of the envelope itself. Migrations bump "
|
|
"this when the envelope shape changes; nested per-type "
|
|
"configs evolve independently via the registries."
|
|
),
|
|
)
|
|
name: str = Field(
|
|
...,
|
|
description="Short, user-facing name shown in lists.",
|
|
min_length=1,
|
|
max_length=200,
|
|
)
|
|
goal: str | None = Field(
|
|
default=None,
|
|
description=(
|
|
"Optional plain-language statement of what the "
|
|
"automation is for. Used by the NL generator's review "
|
|
"pass and by the UI's run dialog."
|
|
),
|
|
)
|
|
inputs: InputsBlock | None = Field(
|
|
default=None,
|
|
description=(
|
|
"Optional input contract. When omitted, the automation "
|
|
"accepts no inputs at fire time."
|
|
),
|
|
)
|
|
triggers: list[TriggerSpec] = Field(
|
|
default_factory=list,
|
|
description=(
|
|
"Triggers that fire this automation. Empty list means "
|
|
"the automation is only runnable via the manual "
|
|
"``Run now`` path."
|
|
),
|
|
)
|
|
plan: list[PlanStep] = Field(
|
|
...,
|
|
description=(
|
|
"Ordered sequence of steps. Executed in array order — "
|
|
"no parallelism, no DAGs, no loops at the envelope "
|
|
"level."
|
|
),
|
|
min_length=1,
|
|
)
|
|
execution: ExecutionBlock = Field(
|
|
default_factory=ExecutionBlock,
|
|
description=(
|
|
"Execution defaults (timeouts, retries, concurrency, "
|
|
"budget). All fields default to safe values; the block "
|
|
"may be omitted entirely."
|
|
),
|
|
)
|
|
metadata: MetadataBlock = Field(
|
|
default_factory=MetadataBlock,
|
|
description=(
|
|
"Free-form metadata (tags, NL-generator breadcrumbs, "
|
|
"UI annotations). Tolerates unknown keys by design."
|
|
),
|
|
)
|