This commit is contained in:
@@ -0,0 +1,157 @@
|
||||
"""Strict, evidence-grounded, review-only opportunity assessment normalization."""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Callable
|
||||
|
||||
ASSESSMENT_SCHEMA_VERSION = "opportunity-assessment-v3"
|
||||
DETERMINISTIC_ASSESSMENT_THRESHOLD = 70
|
||||
RECOMMENDATIONS = frozenset({"contact", "review", "low_priority", "do_not_contact", "insufficient_evidence"})
|
||||
PRIORITIES = frozenset({"high", "medium", "low"})
|
||||
WEBSITE_STATUSES = frozenset({"healthy", "outdated", "broken", "missing", "parked", "unknown"})
|
||||
DOMAIN_STATUSES = frozenset({"registered", "missing", "likely_available", "unknown"})
|
||||
CONTACT_TYPES = frozenset({"none", "general_business", "named_business", "free_mail", "unknown"})
|
||||
_ALLOWED_FIELDS = frozenset({
|
||||
"opportunity_score", "confidence_score", "recommendation", "priority", "reasons", "missing_evidence",
|
||||
"website_assessment", "domain_assessment", "contactability", "recommended_services",
|
||||
"human_review_required", "evidence_references",
|
||||
})
|
||||
|
||||
|
||||
def _score(value: Any, *, confidence: bool = False) -> int:
|
||||
if isinstance(value, bool) or not isinstance(value, (int, float)):
|
||||
return 0
|
||||
numeric = float(value)
|
||||
if numeric != numeric or numeric in (float("inf"), float("-inf")):
|
||||
return 0
|
||||
if confidence and 0 <= numeric <= 1:
|
||||
numeric *= 100
|
||||
return max(0, min(100, int(round(numeric))))
|
||||
|
||||
|
||||
def _enum(value: Any, allowed: frozenset[str], default: str) -> str:
|
||||
item = value.strip().lower() if isinstance(value, str) else ""
|
||||
return item if item in allowed else default
|
||||
|
||||
|
||||
def _text_list(value: Any) -> list[str]:
|
||||
if value is None:
|
||||
return []
|
||||
if not isinstance(value, list) or len(value) > 20 or any(not isinstance(item, str) for item in value):
|
||||
raise ValueError("invalid_assessment_list")
|
||||
result: list[str] = []
|
||||
for item in value:
|
||||
item = item.strip()
|
||||
if not item or len(item) > 300 or item in result:
|
||||
continue
|
||||
result.append(item)
|
||||
return result
|
||||
|
||||
|
||||
def _website(value: Any) -> dict[str, Any]:
|
||||
default = {"status": "unknown", "broken": False, "outdated": False, "mobile_issue": False, "https_issue": False, "performance_issue": False}
|
||||
if value is None:
|
||||
return default
|
||||
if not isinstance(value, dict) or set(value) - set(default):
|
||||
raise ValueError("invalid_assessment_schema")
|
||||
result = dict(default)
|
||||
result["status"] = _enum(value.get("status"), WEBSITE_STATUSES, "unknown")
|
||||
for key in set(default) - {"status"}:
|
||||
if key in value:
|
||||
if not isinstance(value[key], bool):
|
||||
raise ValueError("invalid_assessment_schema")
|
||||
result[key] = value[key]
|
||||
return result
|
||||
|
||||
|
||||
def _domain(value: Any) -> dict[str, str]:
|
||||
if value is None:
|
||||
return {"status": "unknown"}
|
||||
if not isinstance(value, dict) or set(value) != {"status"}:
|
||||
raise ValueError("invalid_assessment_schema")
|
||||
return {"status": _enum(value.get("status"), DOMAIN_STATUSES, "unknown")}
|
||||
|
||||
|
||||
def _contactability(value: Any) -> dict[str, Any]:
|
||||
default = {"public_business_contact_found": False, "contact_type": "unknown"}
|
||||
if value is None:
|
||||
return default
|
||||
if not isinstance(value, dict) or set(value) - set(default):
|
||||
raise ValueError("invalid_assessment_schema")
|
||||
result = dict(default)
|
||||
if "public_business_contact_found" in value:
|
||||
if not isinstance(value["public_business_contact_found"], bool):
|
||||
raise ValueError("invalid_assessment_schema")
|
||||
result["public_business_contact_found"] = value["public_business_contact_found"]
|
||||
result["contact_type"] = _enum(value.get("contact_type"), CONTACT_TYPES, "unknown")
|
||||
return result
|
||||
|
||||
|
||||
def normalize_assessment(raw: dict[str, Any] | None, known_evidence_ids: set[int], *, suppressed: bool = False) -> dict[str, Any]:
|
||||
"""Return exactly the assessment contract; reject invented evidence IDs."""
|
||||
raw = {} if raw is None else raw
|
||||
if not isinstance(raw, dict) or set(raw) - _ALLOWED_FIELDS:
|
||||
raise ValueError("invalid_assessment_schema")
|
||||
references = raw.get("evidence_references", [])
|
||||
if not isinstance(references, list) or len(references) > 100:
|
||||
raise ValueError("invalid_evidence_references")
|
||||
evidence_references: list[int] = []
|
||||
for reference in references:
|
||||
if isinstance(reference, bool) or not isinstance(reference, int):
|
||||
raise ValueError("invalid_evidence_reference")
|
||||
if reference not in known_evidence_ids:
|
||||
raise ValueError("unknown_evidence_reference")
|
||||
if reference not in evidence_references:
|
||||
evidence_references.append(reference)
|
||||
evidence_references.sort()
|
||||
confidence_score = _score(raw.get("confidence_score"), confidence=True)
|
||||
recommendation = _enum(raw.get("recommendation"), RECOMMENDATIONS, "insufficient_evidence")
|
||||
weak_evidence = len(evidence_references) < 2 or confidence_score < 70 or recommendation == "insufficient_evidence"
|
||||
contactability = _contactability(raw.get("contactability"))
|
||||
if suppressed:
|
||||
recommendation = "do_not_contact"
|
||||
contactability = {"public_business_contact_found": False, "contact_type": "none"}
|
||||
return {
|
||||
"opportunity_score": _score(raw.get("opportunity_score")),
|
||||
"confidence_score": confidence_score,
|
||||
"recommendation": recommendation,
|
||||
"priority": _enum(raw.get("priority"), PRIORITIES, "low"),
|
||||
"reasons": _text_list(raw.get("reasons")),
|
||||
"missing_evidence": _text_list(raw.get("missing_evidence")),
|
||||
"website_assessment": _website(raw.get("website_assessment")),
|
||||
"domain_assessment": _domain(raw.get("domain_assessment")),
|
||||
"contactability": contactability,
|
||||
"recommended_services": _text_list(raw.get("recommended_services")),
|
||||
"human_review_required": bool(suppressed or weak_evidence or raw.get("human_review_required", False)),
|
||||
"evidence_references": evidence_references,
|
||||
}
|
||||
|
||||
|
||||
def deterministic_assessment(business: dict[str, Any], evidence: list[dict[str, Any]]) -> dict[str, Any]:
|
||||
try:
|
||||
score = max(0, min(100, int(business.get("score", 0) or 0)))
|
||||
except (TypeError, ValueError):
|
||||
score = 0
|
||||
references = [item["id"] for item in evidence if isinstance(item.get("id"), int)][:100]
|
||||
has_website = bool(business.get("website") or business.get("website_domain"))
|
||||
website_class = str(business.get("website_class", "")).lower()
|
||||
website_status = "missing" if not has_website else website_class if website_class in WEBSITE_STATUSES else "unknown"
|
||||
has_contact = bool(business.get("email") or business.get("phone"))
|
||||
return {
|
||||
"opportunity_score": score,
|
||||
"confidence_score": min(95, 35 + 20 * len(references)),
|
||||
"recommendation": "review" if references and score >= DETERMINISTIC_ASSESSMENT_THRESHOLD else "insufficient_evidence",
|
||||
"priority": "high" if score >= 70 else "medium" if score >= 40 else "low",
|
||||
"reasons": ["Stored evidence requires human review."] if references else [],
|
||||
"missing_evidence": [item for item, present in (("website evidence", has_website), ("corroborating evidence", len(references) >= 2)) if not present],
|
||||
"website_assessment": {"status": website_status, "broken": website_status == "broken", "outdated": website_status == "outdated", "mobile_issue": False, "https_issue": has_website and not str(business.get("website", "")).startswith("https://"), "performance_issue": False},
|
||||
"domain_assessment": {"status": "registered" if business.get("website_domain") else "missing"},
|
||||
"contactability": {"public_business_contact_found": has_contact, "contact_type": "general_business" if has_contact else "none"},
|
||||
"recommended_services": ["website" if not has_website else "website_repair"],
|
||||
"human_review_required": True,
|
||||
"evidence_references": references,
|
||||
}
|
||||
|
||||
|
||||
def assess_opportunity(business: dict[str, Any], evidence: list[dict[str, Any]], *, suppressed: bool = False, provider: Callable[[dict[str, Any], list[dict[str, Any]],], dict[str, Any]] | None = None) -> dict[str, Any]:
|
||||
raw = provider(business, evidence) if provider else deterministic_assessment(business, evidence)
|
||||
return normalize_assessment(raw, {item["id"] for item in evidence if isinstance(item.get("id"), int)}, suppressed=suppressed)
|
||||
Reference in New Issue
Block a user