dotb_ai-platform/tests/test_derived_metrics.py
hungtran24 6577bb74be refactor: simplify scoring service, AI profile revisions and test suite
Phase 2 and 3 of the platform rework.

- Neutral request/response contract: external_id, metadata, X-AI-Key,
  run_id in the response; client-specific fields removed.
- AI profile is a revision table: saving creates an inactive revision,
  activation is separate, try-run does not store a run. One
  chat-completions gateway serves AIHub and OpenAI-compatible profiles.
- Add between and in rule operators.
- Contracts moved to contracts/lead-scoring/ with a score-response fixture.
- Admin Web updated for revisions, runs and the neutral fixture.
- Consolidate tests; remove rewrite_architect.md and the stale handoff doc;
  rewrite spec, CONTEXT and ARCHITECTURE for the current design.

Not verified: live AIHub/OpenAI behavior, Omni/EMS integration.
2026-10-09 08:23:21 +07:00

105 lines
3.9 KiB
Python

import pytest
from ai_platform.lead_scoring.domain.scoring import (
Rule,
ScoringConfiguration,
derive_metrics,
score_rules,
)
CAPTURED = "2025-09-25T10:00:00Z"
def _msg(sender, sent_at):
return {"sender_type": sender, "sent_at": sent_at}
def _conv(*messages):
return {"messages": list(messages)}
def _metrics(*conversations):
return derive_metrics({"captured_at": CAPTURED, "conversations": list(conversations)})
def test_counts_and_turns_use_timestamps_not_array_order():
conversations = [
_conv(
_msg("agent", "2025-09-25T11:00:00Z"),
_msg("contact", "2025-09-25T09:00:00Z"),
_msg("agent", "2025-09-25T10:00:00Z"),
),
_conv(_msg("contact", "2025-09-25T11:00:00Z"), _msg("agent", "2025-09-25T10:00:00Z")),
]
metrics = _metrics(*conversations)
for conversation in conversations:
conversation["messages"].reverse()
assert metrics["conversation_turn_count"] == 4
assert metrics["contact_started_conversation_count"] == 1
assert metrics["contact_message_count"] == 2
assert _metrics(*conversations) == metrics
@pytest.mark.parametrize(
("sent_at", "hours", "days"),
[
("2025-09-25T08:46:00Z", 1.23, 0.05),
("2025-09-25T08:00:00+02:00", 4.0, 0.17),
("2025-09-25T11:00:00Z", 0.0, 0.0),
],
)
def test_contact_recency_is_rounded_clamped_and_timezone_aware(sent_at, hours, days):
metrics = _metrics(_conv(_msg("contact", "not-a-timestamp"), _msg("contact", sent_at)))
assert metrics["contact_message_recency_hours"] == hours
assert metrics["contact_message_recency_days"] == days
def test_no_contact_messages_means_no_recency_and_no_return():
metrics = _metrics(_conv(), _conv(_msg("agent", "2025-09-25T09:00:00Z")))
assert metrics["conversation_count"] == 2
assert metrics["contact_message_count"] == 0
assert metrics["contact_message_recency_hours"] is None
assert metrics["customer_initiated_return"] is False
def test_customer_initiated_return_uses_chronology_and_breaks_ties_by_position():
agent = _conv(_msg("agent", "2025-09-25T08:00:00Z"))
contact = _conv(_msg("contact", "2025-09-25T09:00:00Z"))
later = _conv(_msg("contact", "2025-09-25T10:00:00Z"))
tie_agent = _conv(_msg("agent", "2025-09-25T07:00:00Z"))
tie_contact = _conv(_msg("contact", "2025-09-25T07:00:00Z"))
assert _metrics(later, _conv(), agent, contact)["customer_initiated_return"] is True
assert _metrics(contact, later, agent)["contact_started_conversation_count"] == 2
assert _metrics(tie_agent, tie_contact)["customer_initiated_return"] is True
assert _metrics(tie_contact, tie_agent)["customer_initiated_return"] is False
def test_caller_rules_see_the_derived_metrics_and_booleans_never_match_numbers():
snapshot = {
"captured_at": CAPTURED,
"lead_profile": {"name": "Example", "email": "a@example.invalid", "custom_fields": {}},
"conversations": [
_conv(_msg("contact", "2025-09-24T10:00:00Z"), _msg("agent", "2025-09-25T09:00:00Z")),
_conv(_msg("bot", "2025-09-25T09:30:00Z")),
],
}
rules = (
Rule("profile", "metrics.profile_field_count", "eq", 2, 1),
Rule("conversations", "metrics.conversation_count", "eq", 2, 1),
Rule("messages", "metrics.message_count", "eq", 3, 1),
Rule("contacts", "metrics.contact_message_count", "eq", 1, 1),
Rule("turns", "metrics.conversation_turn_count", "eq", 3, 1),
Rule("started", "metrics.contact_started_conversation_count", "eq", 1, 1),
Rule("recency", "metrics.contact_message_recency_days", "eq", 1.0, 1),
Rule("bool-as-number", "metrics.customer_initiated_return", "eq", 1, 1),
)
score, breakdown = score_rules(snapshot, ScoringConfiguration(rules, 1.0, 0.0))
assert score == 7
assert [item["matched"] for item in breakdown] == [True] * 7 + [False]