"""The strictly validated decision schema returned by every LLM policy (and the mock).""" from __future__ import annotations from typing import Literal from pydantic import BaseModel, Field, ValidationError, field_validator class Decision(BaseModel): action: Literal["hold", "place", "cancel"] side: Literal["buy", "sell"] | None = None order_type: Literal["limit", "market"] | None = None quantity: int | None = Field(default=None, ge=0) limit_price: float | None = Field(default=None, gt=0) # currency units (e.g. 199.50) cancel_order_ids: list[int] = Field(default_factory=list) confidence: float | None = Field(default=None, ge=0.0, le=1.0) rationale: str = Field(default="", max_length=400) @field_validator("rationale") @classmethod def _trim(cls, v: str) -> str: return v.strip()[:400] DECISION_JSON_SCHEMA: dict = { "type": "object", "properties": { "action": {"type": "string", "enum": ["hold", "place", "cancel"]}, "side": {"type": ["string", "null"], "enum": ["buy", "sell", None]}, "order_type": {"type": ["string", "null"], "enum": ["limit", "market", None]}, "quantity": {"type": ["integer", "null"]}, "limit_price": {"type": ["number", "null"]}, "cancel_order_ids": {"type": "array", "items": {"type": "integer"}}, "confidence": {"type": ["number", "null"]}, "rationale": {"type": "string"}, }, "required": ["action", "side", "order_type", "quantity", "limit_price", "cancel_order_ids", "confidence", "rationale"], "additionalProperties": False, } def parse_decision(text: str) -> tuple[Decision | None, str]: """Parse the model's JSON text; returns (decision, error). Lenient about code fences.""" import json s = text.strip() if s.startswith("```"): s = s.strip("`") if s.lower().startswith("json"): s = s[4:] try: obj = json.loads(s) except json.JSONDecodeError as e: # try to locate the first {...} block i, j = s.find("{"), s.rfind("}") if i >= 0 and j > i: try: obj = json.loads(s[i:j + 1]) except json.JSONDecodeError: return None, f"json: {e.msg}" else: return None, f"json: {e.msg}" try: return Decision.model_validate(obj), "" except ValidationError as e: return None, "schema: " + "; ".join(f"{'.'.join(map(str, err['loc']))}: {err['msg']}" for err in e.errors()[:3])