fix(schema): coerce null-ish strings in optional float fields

A weak model can write a placeholder ('None', 'N/A') into an optional price
field, tripping schema validation. Coerce null-ish strings to None on the
trader/PM float fields; real numeric strings still parse.
This commit is contained in:
Yijia-Xiao
2026-06-21 22:31:35 +00:00
parent 709fe2b646
commit 0405168f20
2 changed files with 55 additions and 1 deletions

View File

@@ -21,7 +21,20 @@ from __future__ import annotations
from enum import Enum
from typing import Literal
from pydantic import BaseModel, Field
from pydantic import BaseModel, Field, field_validator
# LLMs sometimes write a placeholder string ("None", "N/A", ...) into an optional
# numeric field instead of omitting it. Coerce those to None so the structured
# call validates instead of erroring (#1058). Pydantic still parses real numeric
# strings ("189.5") to float.
_NULLISH_FLOAT = {"", "none", "n/a", "na", "null", "nil", "-", "tbd", "unknown"}
def _coerce_optional_float(value):
if isinstance(value, str) and value.strip().lower() in _NULLISH_FLOAT:
return None
return value
# ---------------------------------------------------------------------------
# Shared rating types
@@ -136,6 +149,11 @@ class TraderProposal(BaseModel):
description="Optional sizing guidance, e.g. '5% of portfolio'.",
)
@field_validator("entry_price", "stop_loss", mode="before")
@classmethod
def _nullish_float_to_none(cls, v):
return _coerce_optional_float(v)
def render_trader_proposal(proposal: TraderProposal) -> str:
"""Render a TraderProposal to markdown.
@@ -204,6 +222,11 @@ class PortfolioDecision(BaseModel):
description="Optional recommended holding period, e.g. '3-6 months'.",
)
@field_validator("price_target", mode="before")
@classmethod
def _nullish_float_to_none(cls, v):
return _coerce_optional_float(v)
def render_pm_decision(decision: PortfolioDecision) -> str:
"""Render a PortfolioDecision back to the markdown shape the rest of the system expects.