1"""The strictly validated decision schema returned by every LLM policy (and the mock)."""2from __future__ import annotations34from typing import Literal56from pydantic import BaseModel, Field, ValidationError, field_validator789class Decision(BaseModel):10 action: Literal["hold", "place", "cancel"]11 side: Literal["buy", "sell"] | None = None12 order_type: Literal["limit", "market"] | None = None13 quantity: int | None = Field(default=None, ge=0)14 limit_price: float | None = Field(default=None, gt=0) # currency units (e.g. 199.50)15 cancel_order_ids: list[int] = Field(default_factory=list)16 confidence: float | None = Field(default=None, ge=0.0, le=1.0)17 rationale: str = Field(default="", max_length=400)1819 @field_validator("rationale")20 @classmethod21 def _trim(cls, v: str) -> str:22 return v.strip()[:400]232425DECISION_JSON_SCHEMA: dict = {26 "type": "object",27 "properties": {28 "action": {"type": "string", "enum": ["hold", "place", "cancel"]},29 "side": {"type": ["string", "null"], "enum": ["buy", "sell", None]},30 "order_type": {"type": ["string", "null"], "enum": ["limit", "market", None]},31 "quantity": {"type": ["integer", "null"]},32 "limit_price": {"type": ["number", "null"]},33 "cancel_order_ids": {"type": "array", "items": {"type": "integer"}},34 "confidence": {"type": ["number", "null"]},35 "rationale": {"type": "string"},36 },37 "required": ["action", "side", "order_type", "quantity", "limit_price", "cancel_order_ids", "confidence", "rationale"],38 "additionalProperties": False,39}404142def parse_decision(text: str) -> tuple[Decision | None, str]:43 """Parse the model's JSON text; returns (decision, error). Lenient about code fences."""44 import json45 s = text.strip()46 if s.startswith("```"):47 s = s.strip("`")48 if s.lower().startswith("json"):49 s = s[4:]50 try:51 obj = json.loads(s)52 except json.JSONDecodeError as e:53 # try to locate the first {...} block54 i, j = s.find("{"), s.rfind("}")55 if i >= 0 and j > i:56 try:57 obj = json.loads(s[i:j + 1])58 except json.JSONDecodeError:59 return None, f"json: {e.msg}"60 else:61 return None, f"json: {e.msg}"62 try:63 return Decision.model_validate(obj), ""64 except ValidationError as e:65 return None, "schema: " + "; ".join(f"{'.'.join(map(str, err['loc']))}: {err['msg']}" for err in e.errors()[:3])66