dotb_ai-platform/tests/test_derived_metrics.py
2026-09-25 09:55:25 +07:00

186 lines
6.8 KiB
Python

from copy import deepcopy
from ai_platform.lead_scoring.domain.scoring import (
Rule,
ScoringConfiguration,
derive_metrics,
score_rules,
)
def test_profile_and_conversation_metrics_are_available_to_caller_rules():
snapshot = {
"captured_at": "2025-09-25T10:00:00Z",
"lead_profile": {
"name": "Example",
"email": "example@example.invalid",
"custom_fields": {"region": "north"},
},
"conversations": [
{
"messages": [
{"sender_type": "contact", "sent_at": "2025-09-24T10:00:00Z"},
{"sender_type": "agent", "sent_at": "2025-09-25T09:00:00Z"},
]
},
{"messages": [{"sender_type": "bot", "sent_at": "2025-09-25T09:30:00Z"}]},
],
}
before = deepcopy(snapshot)
policy = ScoringConfiguration(
version="omni-8",
rules=(
Rule("region", "custom_fields.region", "eq", "north", 10),
Rule("profile-fields", "metrics.profile_field_count", "eq", 2, 1),
Rule("conversations", "metrics.conversation_count", "eq", 2, 20),
Rule("messages", "metrics.message_count", "eq", 3, 20),
Rule("contacts", "metrics.contact_message_count", "eq", 1, 20),
Rule("legacy-customer-messages", "metrics.customer_message_count", "eq", 1, 1),
Rule("turns", "metrics.conversation_turn_count", "eq", 3, 1),
Rule("contact-started", "metrics.contact_started_conversation_count", "eq", 1, 1),
Rule("recency", "metrics.contact_message_recency_days", "eq", 1.0, 20),
),
rule_weight=1.0,
ai_weight=0.0,
)
score, breakdown = score_rules(snapshot, policy)
assert score == 94
assert all(item["matched"] for item in breakdown)
assert snapshot == before
assert derive_metrics(snapshot)["customer_initiated_return"] is False
def test_turn_and_contact_start_metrics_use_message_timestamps_not_array_order():
conversations = [
{
"messages": [
{"sender_type": "agent", "sent_at": "2025-09-25T11:00:00Z"},
{"sender_type": "contact", "sent_at": "2025-09-25T09:00:00Z"},
{"sender_type": "agent", "sent_at": "2025-09-25T10:00:00Z"},
]
},
{
"messages": [
{"sender_type": "contact", "sent_at": "2025-09-25T11:00:00Z"},
{"sender_type": "agent", "sent_at": "2025-09-25T10:00:00Z"},
]
},
]
snapshot = {"conversations": conversations}
metrics = derive_metrics(snapshot)
for conversation in conversations:
conversation["messages"].reverse()
assert metrics["conversation_turn_count"] == 4
assert metrics["contact_started_conversation_count"] == 1
assert metrics["customer_message_count"] == metrics["contact_message_count"] == 2
assert derive_metrics(snapshot) == metrics
def test_contact_recency_hours_uses_newest_valid_message_and_rounds_to_two_decimals():
metrics = derive_metrics(
{
"captured_at": "2025-09-25T10:00:00Z",
"conversations": [
{
"messages": [
{"sender_type": "contact", "sent_at": "not-a-timestamp"},
{"sender_type": "contact", "sent_at": "2025-09-25T08:46:00Z"},
]
}
],
}
)
assert metrics["contact_message_recency_hours"] == 1.23
assert metrics["contact_message_recency_days"] == 0.05
assert metrics["contact_message_count"] == 2
def test_contact_recency_hours_clamps_future_contact_timestamp_to_zero():
metrics = derive_metrics(
{
"captured_at": "2025-09-25T10:00:00Z",
"conversations": [
{"messages": [{"sender_type": "contact", "sent_at": "2025-09-25T11:00:00Z"}]}
],
}
)
assert metrics["contact_message_recency_hours"] == 0.0
assert metrics["contact_message_recency_days"] == 0.0
def test_empty_conversations_and_no_contact_messages_have_no_recency_or_return():
metrics = derive_metrics(
{
"captured_at": "2025-09-25T10:00:00Z",
"conversations": [
{"messages": []},
{"messages": [{"sender_type": "agent", "sent_at": "2025-09-25T09:00:00Z"}]},
],
}
)
assert metrics["conversation_count"] == 2
assert metrics["contact_started_conversation_count"] == 0
assert metrics["contact_message_count"] == 0
assert metrics["contact_message_recency_hours"] is None
assert metrics["contact_message_recency_days"] is None
assert metrics["customer_initiated_return"] is False
def test_customer_initiated_return_uses_chronology_not_array_order_and_skips_empty_conversations():
agent_start = {"messages": [{"sender_type": "agent", "sent_at": "2025-09-25T08:00:00Z"}]}
contact_middle = {"messages": [{"sender_type": "contact", "sent_at": "2025-09-25T09:00:00Z"}]}
contact_latest = {"messages": [{"sender_type": "contact", "sent_at": "2025-09-25T10:00:00Z"}]}
empty = {"messages": []}
first = derive_metrics({"conversations": [contact_latest, empty, agent_start, contact_middle]})
shuffled = derive_metrics(
{"conversations": [contact_middle, contact_latest, agent_start, empty]}
)
assert first["customer_initiated_return"] is True
assert shuffled["customer_initiated_return"] is True
assert first["contact_started_conversation_count"] == 2
assert shuffled["contact_started_conversation_count"] == 2
def test_customer_initiated_return_breaks_equal_start_times_by_snapshot_position():
agent_start = {"messages": [{"sender_type": "agent", "sent_at": "2025-09-25T08:00:00Z"}]}
contact_start = {"messages": [{"sender_type": "contact", "sent_at": "2025-09-25T08:00:00Z"}]}
assert (
derive_metrics({"conversations": [agent_start, contact_start]})["customer_initiated_return"]
is True
)
assert (
derive_metrics({"conversations": [contact_start, agent_start]})["customer_initiated_return"]
is False
)
def test_boolean_metric_does_not_match_integer_rule_value():
config = ScoringConfiguration(
version="omni-15",
rules=(Rule("boolean-as-number", "metrics.customer_initiated_return", "eq", 1, 10),),
rule_weight=1.0,
ai_weight=0.0,
)
snapshot = {
"conversations": [
{"messages": [{"sender_type": "agent", "sent_at": "2025-09-25T08:00:00Z"}]},
{"messages": [{"sender_type": "contact", "sent_at": "2025-09-25T09:00:00Z"}]},
]
}
score, breakdown = score_rules(snapshot, config)
assert derive_metrics(snapshot)["customer_initiated_return"] is True
assert score == 0
assert breakdown[0]["matched"] is False