186 lines
6.8 KiB
Python
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
|