SurfSense/surfsense_backend/app/automations/schemas/actions/agent_task.py
CREDO23 be4d43d6c9 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.
2026-05-26 22:50:52 +02:00

66 lines
2.3 KiB
Python

"""``AgentTaskActionConfig`` — config for the ``agent_task`` action type."""
from __future__ import annotations
from typing import Any
from pydantic import BaseModel, ConfigDict, Field
class AgentTaskActionConfig(BaseModel):
"""Config for an ``agent_task`` plan step.
Validated against ``PlanStep.config`` whenever the step's
``action`` is ``agent_task``. The step instructs the LangGraph
Deep Agent runtime to:
1. Receive ``prompt`` (with all preceding-step outputs and inputs
already rendered by the template engine).
2. Run the agent with access to *exactly* the capabilities named
in ``tools`` — nothing else from the registry is visible to
this agent invocation.
3. Return a JSON object matching ``output_schema`` (recommended;
the executor validates and re-prompts on mismatch).
``output_schema`` is the design's "dynamic output contract"
instead of locking the output shape on the ActionDefinition (as
tight actions do), the user declares the shape they want for this
specific step, and the agent has to match it.
"""
model_config = ConfigDict(extra="forbid")
prompt: str = Field(
...,
description=(
"The task prompt rendered through the Jinja sandbox. May "
"reference automation inputs and prior-step outputs."
),
min_length=1,
)
tools: list[str] = Field(
default_factory=list,
description=(
"Allowlist of capability IDs the agent may call (e.g., "
"'search_space.query'). Empty list = no tool access; the "
"agent must answer from the prompt alone."
),
)
model: str | None = Field(
default=None,
description=(
"Optional LiteLLM model identifier (e.g., "
"'anthropic/claude-sonnet-4-7'). Omitted means the "
"automation falls back to the search space's default "
"agent_llm_id."
),
)
output_schema: dict[str, Any] | None = Field(
default=None,
description=(
"Optional JSON Schema declaring the shape the agent must "
"return. Strongly recommended; the editor warns when "
"missing. Validated by the executor before binding to "
"``output_as``."
),
)