mirror of
https://github.com/TauricResearch/TradingAgents.git
synced 2026-06-29 19:26:24 +03:00
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:
@@ -15,6 +15,7 @@ from pydantic import ValidationError
|
||||
from tradingagents.agents.analysts.sentiment_analyst import create_sentiment_analyst
|
||||
from tradingagents.agents.managers.research_manager import create_research_manager
|
||||
from tradingagents.agents.schemas import (
|
||||
PortfolioDecision,
|
||||
PortfolioRating,
|
||||
ResearchPlan,
|
||||
SentimentBand,
|
||||
@@ -67,6 +68,36 @@ class TestRenderTraderProposal:
|
||||
assert "FINAL TRANSACTION PROPOSAL: **SELL**" in md
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestNullishFloatCoercion:
|
||||
"""A weak LLM may write "None"/"N/A" into an optional float field (#1058);
|
||||
coerce those to None so the structured call validates instead of erroring."""
|
||||
|
||||
def test_trader_nullish_strings_coerce_to_none(self):
|
||||
for sentinel in ("None", "N/A", "null", "-", "", "TBD"):
|
||||
p = TraderProposal(
|
||||
action=TraderAction.HOLD,
|
||||
reasoning="x",
|
||||
entry_price=sentinel,
|
||||
stop_loss=sentinel,
|
||||
)
|
||||
assert p.entry_price is None
|
||||
assert p.stop_loss is None
|
||||
|
||||
def test_trader_real_numeric_string_still_parses(self):
|
||||
p = TraderProposal(action=TraderAction.BUY, reasoning="x", entry_price="189.5")
|
||||
assert p.entry_price == 189.5
|
||||
|
||||
def test_pm_nullish_price_target_coerces_to_none(self):
|
||||
d = PortfolioDecision(
|
||||
rating=PortfolioRating.OVERWEIGHT,
|
||||
executive_summary="s",
|
||||
investment_thesis="t",
|
||||
price_target="N/A",
|
||||
)
|
||||
assert d.price_target is None
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestRenderResearchPlan:
|
||||
def test_required_fields(self):
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user