from lead_scoring.schemas.snapshot import CanonicalSnapshot from lead_scoring.scoring import ( BehavioralMetric, RuleMetric, ScoringConfig, ScoringRule, compute_behavioral_metrics, evaluate_rule_score, extract_normalized_features, ) def make_snapshot() -> CanonicalSnapshot: return CanonicalSnapshot.model_validate( { "captured_at": "2026-08-27T12:00:00Z", "lead_profile": { "ems_lead_id": "lead-1", "name": "Lead One", "phone": "+84900000000", }, "conversations": [ { "conversation_id": "conversation-1", "messages": [ { "message_id": "message-1", "sender_type": "contact", "content": "Tôi cần tư vấn", "sent_at": "2026-08-27T11:00:00Z", } ], } ], } ) def test_rule_score_is_deterministic_and_bounded_by_immutable_config() -> None: snapshot = make_snapshot() config = ScoringConfig( version="fixture-1", base_score=95, confidence=0.75, confidence_threshold=0.8, hitl_policy_version="fixture-policy-1", rules=( ScoringRule(metric=RuleMetric.PROFILE_FIELD_COUNT, minimum=2, points=20), ScoringRule(metric=RuleMetric.MESSAGE_COUNT, minimum=2, points=-10), ), ) assert evaluate_rule_score(snapshot, config) == 100 assert evaluate_rule_score(snapshot, config) == 100 assert config.model_config.get("frozen") is True def test_rule_score_clamps_negative_total_to_zero() -> None: config = ScoringConfig( version="fixture-2", confidence=0.5, confidence_threshold=0.8, hitl_policy_version="fixture-policy-2", rules=(ScoringRule(metric=RuleMetric.CONVERSATION_COUNT, minimum=1, points=-100),), ) assert evaluate_rule_score(make_snapshot(), config) == 0 def test_compute_behavioral_metrics_deterministic_and_selective() -> None: snapshot = CanonicalSnapshot.model_validate( { "captured_at": "2026-08-27T12:00:00Z", "lead_profile": { "ems_lead_id": "lead-behavioral-1", "name": "Test Lead", }, "conversations": [ { "conversation_id": "conv-1", "messages": [ { "message_id": "m1", "sender_type": "agent", "content": "Chào anh", "sent_at": "2026-08-27T08:00:00Z", }, { "message_id": "m2", "sender_type": "contact", "content": "Tôi muốn đăng ký", "sent_at": "2026-08-27T08:05:00Z", }, { "message_id": "m3", "sender_type": "agent", "content": "Vâng ạ", "sent_at": "2026-08-27T08:06:00Z", }, ], }, { "conversation_id": "conv-2", "messages": [ { "message_id": "m4", "sender_type": "contact", "content": "Hôm nay tôi muốn hỏi thêm học phí", "sent_at": "2026-08-27T10:00:00Z", }, ], }, ], } ) all_metrics = compute_behavioral_metrics(snapshot) assert all_metrics["message_count"] == 4 assert all_metrics["conversation_count"] == 2 assert ( all_metrics["conversation_turn_count"] == 4 ) # conv1: 3 turns (agent->contact->agent) + conv2: 1 turn (contact) = 4 assert all_metrics["customer_message_count"] == 2 assert all_metrics["customer_initiated_return"] is True selected_metrics = compute_behavioral_metrics( snapshot, [ BehavioralMetric.CONVERSATION_TURN_COUNT, BehavioralMetric.CUSTOMER_INITIATED_RETURN, ], ) assert set(selected_metrics.keys()) == { "conversation_turn_count", "customer_initiated_return", } assert selected_metrics["conversation_turn_count"] == 4 assert selected_metrics["customer_initiated_return"] is True features = extract_normalized_features(snapshot) assert features.message_count == 4 assert features.conversation_count == 2 assert features.conversation_turn_count == 4 assert features.customer_message_count == 2 assert features.customer_initiated_return is True