Source code for veupath_chatbot.transport.http.schemas.plan
"""Strategy plan (DSL) request/response DTOs.
These models make the plan contract explicit in OpenAPI instead of using
`dict[str, Any]`, which is a major source of drift.
"""
from typing import Literal
from pydantic import BaseModel, Field, model_validator
from veupath_chatbot.platform.types import JSONArray, JSONObject, JSONValue
[docs]
class StepFilterSpec(BaseModel):
name: str
value: JSONValue
disabled: bool = False
[docs]
class StepAnalysisSpec(BaseModel):
analysisType: str
parameters: JSONObject = Field(default_factory=dict)
customName: str | None = None
[docs]
class StepReportSpec(BaseModel):
reportName: str = "standard"
config: JSONObject = Field(default_factory=dict)
[docs]
class BasePlanNode(BaseModel):
id: str | None = None
displayName: str | None = None
filters: list[StepFilterSpec] | None = None
analyses: list[StepAnalysisSpec] | None = None
reports: list[StepReportSpec] | None = None
# Allow extra fields so the AI agent can store arbitrary WDK-originated
# keys (e.g. ``wdkStepId``, ``estimatedSize``) without schema breakage.
model_config = {"extra": "allow"}
[docs]
class ColocationParams(BaseModel):
upstream: int = Field(ge=0)
downstream: int = Field(ge=0)
strand: Literal["same", "opposite", "both"] = "both"
[docs]
class PlanNode(BasePlanNode):
"""Untyped recursive plan node (WDK-aligned).
Kind is inferred from structure:
- combine: primaryInput + secondaryInput
- transform: primaryInput only
- search: no inputs
"""
searchName: str
parameters: JSONObject = Field(default_factory=dict)
primaryInput: "PlanNode | None" = Field(default=None)
secondaryInput: "PlanNode | None" = Field(default=None)
# Required iff secondaryInput present
operator: str | None = None
colocationParams: ColocationParams | None = None
model_config = {"extra": "allow"}
@model_validator(mode="after")
def _validate_structure(self) -> "PlanNode":
# secondary requires primary
if self.secondaryInput is not None and self.primaryInput is None:
raise ValueError("secondaryInput requires primaryInput")
# operator required when secondary present
if self.secondaryInput is not None and not self.operator:
raise ValueError("operator is required when secondaryInput is present")
# colocationParams constraints
if self.operator == "COLOCATE" and self.colocationParams is None:
raise ValueError("colocationParams is required when operator is COLOCATE")
if self.operator != "COLOCATE" and self.colocationParams is not None:
raise ValueError(
"colocationParams is only allowed when operator is COLOCATE"
)
return self
[docs]
class StrategyPlan(BaseModel):
recordType: str
root: PlanNode
metadata: PlanMetadata | None = None
model_config = {"extra": "allow"}
[docs]
class PlanNormalizeRequest(BaseModel):
siteId: str
plan: StrategyPlan
[docs]
class PlanNormalizeResponse(BaseModel):
plan: StrategyPlan
warnings: JSONArray | None = None
# Resolve forward references for recursive node types.
PlanNode.model_rebuild()
StrategyPlan.model_rebuild()