diff --git a/shared/schemas/meet_kevin.py b/shared/schemas/meet_kevin.py index 543148a..b6434a3 100644 --- a/shared/schemas/meet_kevin.py +++ b/shared/schemas/meet_kevin.py @@ -77,17 +77,15 @@ class MeetKevinTickerMention(BaseModel): """ symbol: str = Field( - ..., description="Stock ticker symbol (A-Z, 1-6 chars, auto-uppercased)" + ..., min_length=1, max_length=10, description="Stock ticker symbol (A-Z, 1-6 chars, auto-uppercased)" ) - action: Literal["buy", "sell", "hold", "watch", "avoid"] = Field( + action: TickerAction = Field( ..., description="Recommendation action" ) conviction: float = Field( ..., ge=0.0, le=1.0, description="Confidence in recommendation (0.0-1.0)" ) - time_horizon: Literal[ - "intraday", "days", "weeks", "months", "long_term", "unspecified" - ] = Field(..., description="Time horizon for the recommendation") + time_horizon: TimeHorizon = Field(..., description="Time horizon for the recommendation") rationale_quote: str = Field( ..., description="Short verbatim or paraphrased quote from video" ) @@ -110,7 +108,7 @@ class MeetKevinAnalysis(BaseModel): Used as tool-input for the LLM analyzer and persisted as kevin_analyses. """ - market_outlook_direction: Literal["bullish", "neutral", "bearish", "mixed"] = ( + market_outlook_direction: MarketOutlook = ( Field(..., description="Overall market sentiment direction") ) market_outlook_reasoning: str = Field( @@ -136,8 +134,6 @@ class MeetKevinAnalysis(BaseModel): class TranscriptSegment(BaseModel): - """Single segment from a video transcript with timing.""" - start_seconds: float = Field(..., description="Segment start time in seconds") end_seconds: float = Field(..., description="Segment end time in seconds") text: str = Field(..., description="Segment text content") @@ -153,7 +149,7 @@ class VideoSummary(BaseModel): title: str = Field(..., description="Video title") published_at: datetime = Field(..., description="Publication timestamp") thumbnail_url: str = Field(..., description="Thumbnail image URL") - status: Literal["discovered", "captioned", "analyzed", "failed", "skipped"] = ( + status: VideoStatus = ( Field(..., description="Processing status") ) failure_reason: str | None = Field( @@ -176,13 +172,13 @@ class VideoDetail(BaseModel): published_at: datetime = Field(..., description="Publication timestamp") duration_seconds: int | None = Field(default=None, description="Video duration") thumbnail_url: str = Field(..., description="Thumbnail image URL") - status: Literal["discovered", "captioned", "analyzed", "failed", "skipped"] = ( + status: VideoStatus = ( Field(..., description="Processing status") ) failure_reason: str | None = Field( default=None, description="Failure reason if status=failed" ) - transcript_source: Literal["captions_manual", "captions_auto", "none"] | None = ( + transcript_source: TranscriptSource | None = ( Field(default=None, description="Source of captions") ) transcript_segments: list[TranscriptSegment] = Field( @@ -204,15 +200,13 @@ class StockMention(BaseModel): video_id: int = Field(..., description="Database ID of video") youtube_video_id: str = Field(..., description="YouTube video ID for linking") published_at: datetime = Field(..., description="Video publication date") - action: Literal["buy", "sell", "hold", "watch", "avoid"] = Field( + action: TickerAction = Field( ..., description="Recommendation action" ) conviction: float = Field( ..., ge=0.0, le=1.0, description="Confidence in recommendation" ) - time_horizon: Literal[ - "intraday", "days", "weeks", "months", "long_term", "unspecified" - ] = Field(..., description="Time horizon for recommendation") + time_horizon: TimeHorizon = Field(..., description="Time horizon for recommendation") rationale_quote: str = Field( ..., description="Quote or summary of rationale" ) @@ -224,14 +218,12 @@ class StockMention(BaseModel): class StockSummary(BaseModel): - """Summary of a stock across all mentions.""" - symbol: str = Field(..., description="Stock ticker") mention_count: int = Field(..., description="Total mention count") last_mentioned_at: datetime = Field( ..., description="Timestamp of last mention" ) - latest_action: Literal["buy", "sell", "hold", "watch", "avoid"] = Field( + latest_action: TickerAction = Field( ..., description="Most recent recommendation" ) avg_conviction: float = Field( @@ -251,10 +243,8 @@ class StockSummary(BaseModel): class TimelineBucket(BaseModel): - """Single time bucket in a sentiment timeline.""" - bucket_date: str = Field(..., description="Date string (YYYY-MM-DD or YYYY-Www)") - action: Literal["buy", "sell", "hold", "watch", "avoid"] | None = Field( + action: TickerAction | None = Field( default=None, description="Most common action in bucket" ) avg_conviction: float = Field( @@ -268,8 +258,6 @@ class TimelineBucket(BaseModel): class StockTimeline(BaseModel): - """Timeline of mentions for a single stock ticker.""" - symbol: str = Field(..., description="Stock ticker") buckets: list[TimelineBucket] = Field( default_factory=list, description="Time-bucketed data" diff --git a/tests/test_schemas.py b/tests/test_schemas.py index d00775a..039cd64 100644 --- a/tests/test_schemas.py +++ b/tests/test_schemas.py @@ -593,7 +593,7 @@ class TestTokenResponse: class TestMeetKevinTickerMention: def test_valid_ticker_mention(self) -> None: - from shared.schemas.meet_kevin import MeetKevinTickerMention + from shared.schemas.meet_kevin import MeetKevinTickerMention, TickerAction, TimeHorizon mention = MeetKevinTickerMention( symbol="AAPL", @@ -604,7 +604,11 @@ class TestMeetKevinTickerMention: video_timestamp_seconds=120, ) assert mention.symbol == "AAPL" + assert mention.action == TickerAction.BUY assert mention.conviction == 0.85 + assert mention.time_horizon == TimeHorizon.MONTHS + assert mention.rationale_quote == "Strong earnings growth expected" + assert mention.video_timestamp_seconds == 120 def test_symbol_auto_uppercases(self) -> None: from shared.schemas.meet_kevin import MeetKevinTickerMention @@ -642,28 +646,29 @@ class TestMeetKevinTickerMention: rationale_quote="Negative conviction", ) - def test_conviction_edge_cases(self) -> None: + def test_conviction_boundary_zero_valid(self) -> None: from shared.schemas.meet_kevin import MeetKevinTickerMention - # Test 0.0 - m1 = MeetKevinTickerMention( + m = MeetKevinTickerMention( symbol="GOOG", action="avoid", conviction=0.0, time_horizon="unspecified", rationale_quote="No confidence", ) - assert m1.conviction == 0.0 + assert m.conviction == 0.0 - # Test 1.0 - m2 = MeetKevinTickerMention( + def test_conviction_boundary_one_valid(self) -> None: + from shared.schemas.meet_kevin import MeetKevinTickerMention + + m = MeetKevinTickerMention( symbol="MSFT", action="buy", conviction=1.0, time_horizon="long_term", rationale_quote="Maximum confidence", ) - assert m2.conviction == 1.0 + assert m.conviction == 1.0 def test_timestamp_optional(self) -> None: from shared.schemas.meet_kevin import MeetKevinTickerMention