From 396fa279cd459cae5b49595b99d0a6c65be84057 Mon Sep 17 00:00:00 2001 From: Lilith Date: Sun, 15 Feb 2026 03:55:57 -0800 Subject: [PATCH] =?UTF-8?q?chore(ml-service):=20=F0=9F=94=A7=20Add/modify?= =?UTF-8?q?=20Pydantic=20dataclass/type=20definitions=20for=20sales=20oper?= =?UTF-8?q?ations=20data=20structures?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Lilith Autocommit --- .../ml-service/src/sales_types.py | 245 ++++++++++++++++++ 1 file changed, 245 insertions(+) diff --git a/features/conversation-assistant/ml-service/src/sales_types.py b/features/conversation-assistant/ml-service/src/sales_types.py index 93206d204..9de76c00b 100755 --- a/features/conversation-assistant/ml-service/src/sales_types.py +++ b/features/conversation-assistant/ml-service/src/sales_types.py @@ -357,6 +357,11 @@ class SalesClassificationResponse(BaseModel): description="True if creator should prioritize this message", ) + reasoning: "MessageReasoning | None" = Field( + None, + description="Chain-of-reasoning trace showing how this classification was reached", + ) + model_config = ConfigDict(arbitrary_types_allowed=True) @@ -735,6 +740,10 @@ class ScreeningResult(BaseModel): None, description="Contact identifier if provided", ) + conversation_reasoning: "ConversationReasoning | None" = Field( + None, + description="Full chain-of-reasoning trace across all messages in the conversation", + ) class ScreeningGateRequest(BaseModel): @@ -821,3 +830,239 @@ class ScreeningGateResult(BaseModel): None, description="Contact identifier if provided", ) + + +# --- Chain-of-Reasoning Models --- + + +class PatternMatch(BaseModel): + """A single pattern that was checked during classification. + + Attributes: + pattern_category: Which pattern group this belongs to (e.g. "booking", "scam", "free_request") + pattern_text: The regex pattern description + matched_text: The actual text that matched (empty if no match) + significance: How significant this match is for the classification (0.0-1.0) + """ + + pattern_category: str = Field(..., description="Pattern group name") + pattern_text: str = Field(..., description="Pattern description or regex summary") + matched_text: str = Field("", description="Actual text that matched (empty if no match)") + significance: float = Field( + ..., + ge=0.0, + le=1.0, + description="Significance of this pattern for classification", + ) + + +class MessageReasoning(BaseModel): + """Chain-of-reasoning trace for a single message classification. + + Explains which patterns were checked, which matched, and how + the final intent determination was reached. + + Attributes: + patterns_checked: All patterns that were evaluated + patterns_matched: Patterns that successfully matched + intent_determination: Explanation of how the primary intent was chosen + context_factors: Contextual factors that influenced classification + risk_contribution: How this message contributes to overall risk + signal_summary: One-line summary of the signals detected + """ + + patterns_checked: list[PatternMatch] = Field( + default_factory=list, + description="All patterns evaluated against this message", + ) + patterns_matched: list[PatternMatch] = Field( + default_factory=list, + description="Patterns that successfully matched", + ) + intent_determination: str = Field( + ..., + description="Explanation of how the primary intent was chosen", + ) + context_factors: list[str] = Field( + default_factory=list, + description="Contextual factors that influenced classification", + ) + risk_contribution: str = Field( + ..., + description="How this message contributes to overall risk assessment", + ) + signal_summary: str = Field( + ..., + description="One-line summary of detected signals", + ) + + +class MessageReasoningBrief(BaseModel): + """Condensed per-message reasoning for conversation-level analysis. + + Attributes: + message_index: Index of this message in the conversation + sender: Who sent the message ("creator" or "contact") + text_preview: First 60 characters of the message text + signals: List of signal names detected in this message + cumulative_risk: Running risk score after this message (0.0-1.0) + """ + + message_index: int = Field(..., ge=0, description="Message index in conversation") + sender: str = Field(..., description="Message sender: creator or contact") + text_preview: str = Field(..., description="First 60 characters of message text") + signals: list[str] = Field(default_factory=list, description="Signal names detected") + cumulative_risk: float = Field( + ..., + ge=0.0, + le=1.0, + description="Running risk score after this message", + ) + + +class RiskSnapshot(BaseModel): + """Risk state snapshot at a specific point in the conversation. + + Attributes: + after_message: Message index after which this snapshot was taken + score: Risk score at this point (0.0-1.0) + status: Traffic-light status at this point + trigger: What caused a status change (None if unchanged) + """ + + after_message: int = Field(..., ge=0, description="Message index for this snapshot") + score: float = Field(..., ge=0.0, le=1.0, description="Risk score at this point") + status: str = Field( + ..., + pattern="^(green|yellow|red)$", + description="Traffic-light status: green, yellow, red", + ) + trigger: str | None = Field(None, description="What caused a status change") + + +class ConversationReasoning(BaseModel): + """Full chain-of-reasoning trace for a conversation screening. + + Provides message-by-message analysis showing how risk evolved + across the entire conversation. + + Attributes: + message_by_message: Per-message analysis with signals and running risk + trajectory: Overall conversation trajectory description + pattern_evolution: How patterns evolved through the conversation + booking_intent_analysis: Analysis of booking intent signals + risk_progression: Snapshots of risk at key points + comparison_to_real_clients: How this compares to genuine client patterns + final_assessment: Summary assessment of the conversation + """ + + message_by_message: list[MessageReasoningBrief] = Field( + default_factory=list, + description="Per-message analysis with signals and cumulative risk", + ) + trajectory: str = Field( + ..., + description="Overall conversation trajectory description", + ) + pattern_evolution: list[str] = Field( + default_factory=list, + description="How detected patterns evolved through the conversation", + ) + booking_intent_analysis: str = Field( + ..., + description="Analysis of booking intent signals across conversation", + ) + risk_progression: list[RiskSnapshot] = Field( + default_factory=list, + description="Risk snapshots at key conversation points", + ) + comparison_to_real_clients: str = Field( + ..., + description="How this conversation compares to genuine client interaction patterns", + ) + final_assessment: str = Field( + ..., + description="Summary assessment of the conversation screening", + ) + + +class ConversationEndingAnalysis(BaseModel): + """Analysis of how and why a conversation ended. + + Attributes: + ending_type: Classification of the conversation ending + confidence: Confidence in the ending type classification (0.0-1.0) + last_message_analysis: Analysis of the final messages + ending_dynamics: Description of the dynamics that led to ending + who_disengaged: Which party disengaged + conversation_lifespan: Description of conversation duration/activity + was_productive: Whether the conversation achieved any productive outcome + lessons_learned: Actionable takeaways for future conversations + similar_pattern_frequency: How common this ending pattern is + """ + + ending_type: str = Field( + ..., + description="Ending classification: ghosted_by_contact, blocked_by_provider, " + "booking_completed, price_declined, time_waster_disengaged, scam_detected, " + "mutual_fadeout, still_active", + ) + confidence: float = Field( + ..., + ge=0.0, + le=1.0, + description="Confidence in ending type classification", + ) + last_message_analysis: str = Field( + ..., + description="Analysis of the final messages in the conversation", + ) + ending_dynamics: str = Field( + ..., + description="Dynamics that led to the conversation ending", + ) + who_disengaged: str = Field( + ..., + pattern="^(contact|provider|mutual|unknown)$", + description="Which party disengaged: contact, provider, mutual, unknown", + ) + conversation_lifespan: str = Field( + ..., + description="Description of conversation duration and activity level", + ) + was_productive: bool = Field( + ..., + description="Whether the conversation achieved a productive outcome", + ) + lessons_learned: list[str] = Field( + default_factory=list, + description="Actionable takeaways for future conversations", + ) + similar_pattern_frequency: str = Field( + ..., + description="How common this ending pattern is among conversations", + ) + + +class ConversationEndingRequest(BaseModel): + """Request to analyze how a conversation ended. + + Attributes: + conversation: Full conversation history to analyze + contact_id: Optional contact identifier for tracking + """ + + conversation: list[ConversationMessage] = Field( + ..., + min_length=1, + description="Full conversation history to analyze", + ) + contact_id: str | None = Field( + None, + description="Contact identifier for tracking", + ) + + +# Resolve forward references for models that reference types defined later in the file +SalesClassificationResponse.model_rebuild() +ScreeningResult.model_rebuild()