feat: add rectification event decision contract v2
This commit is contained in:
@@ -3,16 +3,54 @@ from __future__ import annotations
|
||||
import hashlib
|
||||
import json
|
||||
from collections import defaultdict
|
||||
from collections.abc import Callable, Sequence
|
||||
from datetime import date, timedelta
|
||||
from functools import lru_cache
|
||||
from typing import Any, Callable, Sequence
|
||||
from typing import Any
|
||||
|
||||
from scripts.active_rectification_event_engine import compute_candidate_static_contexts, compute_event_candidate_rows
|
||||
from scripts.active_rectification_events import CandidateScoreRow
|
||||
from scripts.rectification.contracts import LifeEvent, RectificationRequest
|
||||
from scripts.rectification.contracts import LifeEvent, RectificationRequest, is_scoreable_event
|
||||
|
||||
ALGORITHM_VERSION = "rectification-v5-matrix-scoring-2"
|
||||
INPUT_CONTRACT_VERSION = "rectification-calculation-spec-v4"
|
||||
PRECISION_WEIGHTS = {
|
||||
"day": 1.0,
|
||||
"month": 0.8,
|
||||
"quarter": 0.65,
|
||||
"year": 0.5,
|
||||
"range": 0.35,
|
||||
}
|
||||
|
||||
_ENGINE_KIND_BY_NATIVE_KIND: dict[str, tuple[str, str]] = {
|
||||
"education_start": ("education", "education_milestone"),
|
||||
"education_completion": ("education", "education_milestone"),
|
||||
"education_interruption": ("education", "education_milestone"),
|
||||
"education_change": ("education", "education_milestone"),
|
||||
"education_milestone": ("education", "education_milestone"),
|
||||
"career_entry": ("career", "career_change"),
|
||||
"career_change": ("career", "career_change"),
|
||||
"promotion": ("career", "career_change"),
|
||||
"career_pressure": ("career", "career_change"),
|
||||
"career_exit": ("career", "career_change"),
|
||||
"business_start": ("career", "career_change"),
|
||||
"relationship_start": ("relationship", "relationship_start"),
|
||||
"relationship_commitment": ("relationship", "relationship_start"),
|
||||
"relationship_separation": ("relationship", "relationship_change"),
|
||||
"relationship_end": ("relationship", "relationship_change"),
|
||||
"relationship_change": ("relationship", "relationship_change"),
|
||||
"relocation": ("relocation", "relocation"),
|
||||
"foreign_move": ("relocation", "relocation"),
|
||||
"return": ("relocation", "relocation"),
|
||||
"home_change": ("relocation", "relocation"),
|
||||
"finance_gain": ("finance", "finance_change"),
|
||||
"finance_loss": ("finance", "finance_change"),
|
||||
"income_change": ("finance", "finance_change"),
|
||||
"asset_change": ("finance", "finance_change"),
|
||||
"finance_change": ("finance", "finance_change"),
|
||||
"self_health_event": ("health_pressure", "self_health_event"),
|
||||
"pressure_period": ("health_pressure", "self_health_event"),
|
||||
}
|
||||
|
||||
|
||||
def _parse(value: str) -> date:
|
||||
@@ -58,6 +96,7 @@ def sample_event_dates(event: LifeEvent) -> list[str]:
|
||||
|
||||
|
||||
def _legacy_request(request: RectificationRequest, event: LifeEvent, sampled_date: str) -> dict[str, Any]:
|
||||
engine_domain, _ = _ENGINE_KIND_BY_NATIVE_KIND[event["event_kind"]]
|
||||
return {
|
||||
"birth_date": request["birth_date"],
|
||||
"start_time": request["start_time"],
|
||||
@@ -66,8 +105,8 @@ def _legacy_request(request: RectificationRequest, event: LifeEvent, sampled_dat
|
||||
"lon": request["lon"],
|
||||
"tz": request["tz"],
|
||||
"events": [{
|
||||
"id": event["id"], "domain": event["domain"],
|
||||
"event_kind": event.get("event_kind", event["domain"]),
|
||||
"id": event["id"], "domain": engine_domain,
|
||||
"event_kind": event["event_kind"],
|
||||
"date": sampled_date, "precision": "day", "summary": event.get("summary", ""),
|
||||
}],
|
||||
}
|
||||
@@ -82,57 +121,103 @@ def _cached_rows(serialized: str) -> tuple[CandidateScoreRow, ...]:
|
||||
return tuple(compute_event_candidate_rows(json.loads(serialized)))
|
||||
|
||||
|
||||
_RELATIONSHIP_SUPPORT_RULES = (
|
||||
_SUPPORT_RULES = (
|
||||
"functional_benefic_auxiliary",
|
||||
"arudha_auxiliary",
|
||||
"ashtakavarga_target_house_support_auxiliary",
|
||||
"shadbala_sthana_drik_naisargika_support_auxiliary",
|
||||
"controlled_transit_jupiter_domain_house",
|
||||
)
|
||||
_RELATIONSHIP_CHANGE_RULES = (
|
||||
_PRESSURE_RULES = (
|
||||
"functional_malefic_auxiliary",
|
||||
"ashtakavarga_target_house_pressure_auxiliary",
|
||||
"shadbala_sthana_drik_naisargika_pressure_auxiliary",
|
||||
"controlled_transit_saturn_domain_house",
|
||||
)
|
||||
|
||||
_KIND_SEMANTICS: dict[str, tuple[int, float]] = {
|
||||
"education_start": (1, 1.0),
|
||||
"education_completion": (1, 1.2),
|
||||
"education_interruption": (-1, 1.0),
|
||||
"education_change": (0, 1.0),
|
||||
"career_entry": (1, 1.0),
|
||||
"career_change": (0, 1.0),
|
||||
"promotion": (1, 1.2),
|
||||
"career_pressure": (-1, 1.0),
|
||||
"career_exit": (-1, 1.2),
|
||||
"business_start": (1, 1.2),
|
||||
"relationship_start": (1, 1.0),
|
||||
"relationship_commitment": (1, 1.2),
|
||||
"relationship_separation": (-1, 1.0),
|
||||
"relationship_end": (-1, 1.2),
|
||||
"relationship_change": (-1, 1.0),
|
||||
"relocation": (0, 1.0),
|
||||
"foreign_move": (1, 1.0),
|
||||
"return": (1, 0.8),
|
||||
"home_change": (0, 1.0),
|
||||
"finance_gain": (1, 1.0),
|
||||
"finance_loss": (-1, 1.0),
|
||||
"income_change": (0, 1.0),
|
||||
"asset_change": (0, 1.0),
|
||||
"self_health_event": (-1, 1.0),
|
||||
"pressure_period": (-1, 1.2),
|
||||
}
|
||||
|
||||
def _relationship_kind_factor(event_kind: str, rule_ids: Sequence[str]) -> float:
|
||||
if event_kind not in {"relationship_start", "relationship_change"}:
|
||||
return 1.0
|
||||
support = sum(any(rule.endswith(marker) for marker in _RELATIONSHIP_SUPPORT_RULES) for rule in rule_ids)
|
||||
change = sum(any(rule.endswith(marker) for marker in _RELATIONSHIP_CHANGE_RULES) for rule in rule_ids)
|
||||
direction = support - change if event_kind == "relationship_start" else change - support
|
||||
return max(0.8, min(1.2, 1 + 0.08 * direction))
|
||||
|
||||
def precision_weight(precision: str) -> float:
|
||||
return PRECISION_WEIGHTS[precision]
|
||||
|
||||
|
||||
def _event_kind_factor(event_kind: str, rule_ids: Sequence[str]) -> float:
|
||||
direction, intensity = _KIND_SEMANTICS.get(event_kind, (0, 1.0))
|
||||
support = sum(any(rule.endswith(marker) for marker in _SUPPORT_RULES) for rule in rule_ids)
|
||||
pressure = sum(any(rule.endswith(marker) for marker in _PRESSURE_RULES) for rule in rule_ids)
|
||||
semantic_signal = direction * (support - pressure) * intensity
|
||||
return max(0.8, min(1.2, 1 + 0.08 * semantic_signal))
|
||||
|
||||
|
||||
def _kind_adjusted_evidence(event: LifeEvent, evidence: dict[str, Any]) -> dict[str, Any]:
|
||||
if event["domain"] != "relationship":
|
||||
return evidence
|
||||
event_kind = event["event_kind"]
|
||||
rules = list(evidence["rule_ids"])
|
||||
direction, _ = _KIND_SEMANTICS.get(event_kind, (0, 1.0))
|
||||
semantic_label = "support" if direction > 0 else "pressure" if direction < 0 else "change"
|
||||
return {
|
||||
**evidence,
|
||||
"rule_ids": [*rules, f"event_kind_profile:{event_kind}"],
|
||||
"points": round(float(evidence["points"]) * _relationship_kind_factor(event_kind, rules), 4),
|
||||
"rule_ids": [*rules, f"event_kind_profile:{event_kind}:{semantic_label}"],
|
||||
"points": round(
|
||||
float(evidence["points"])
|
||||
* _event_kind_factor(event_kind, rules)
|
||||
* precision_weight(event["precision"]),
|
||||
4,
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
def scoreable_request(request: RectificationRequest) -> RectificationRequest:
|
||||
return {**request, "events": [event for event in request["events"] if is_scoreable_event(event)]}
|
||||
|
||||
|
||||
def build_event_contribution_matrix(
|
||||
request: RectificationRequest,
|
||||
row_provider: Callable[[dict[str, Any]], Sequence[CandidateScoreRow]] | None = None,
|
||||
) -> dict[str, Any]:
|
||||
static_contexts = None if row_provider is not None else compute_candidate_static_contexts(request)
|
||||
scoring_request = scoreable_request(request)
|
||||
if not scoring_request["events"]:
|
||||
return {
|
||||
"candidate_times": [], "matrix": {}, "date_sensitivity": [],
|
||||
"missing_layers": [], "static_contexts": None,
|
||||
}
|
||||
static_contexts = None if row_provider is not None else compute_candidate_static_contexts(scoring_request)
|
||||
provider = row_provider or (lambda value: compute_event_candidate_rows(value, static_contexts=static_contexts))
|
||||
matrix: dict[str, dict[str, dict[str, Any]]] = defaultdict(dict)
|
||||
missing_layers: set[str] = set()
|
||||
date_sensitivity: list[dict[str, Any]] = []
|
||||
candidate_grid: list[str] | None = None
|
||||
for event in request["events"]:
|
||||
for event in scoring_request["events"]:
|
||||
samples = sample_event_dates(event)
|
||||
sample_rows = []
|
||||
for sampled in samples:
|
||||
rows = list(provider(_legacy_request(request, event, sampled)))
|
||||
rows = list(provider(_legacy_request(scoring_request, event, sampled)))
|
||||
sample_rows.append([
|
||||
{**row, "score": adjusted["points"], "evidence": [adjusted]}
|
||||
for row in rows
|
||||
@@ -184,6 +269,8 @@ def score_from_matrix(request: RectificationRequest, built: dict[str, Any]) -> l
|
||||
for candidate_time in built["candidate_times"]:
|
||||
evidence = []
|
||||
for event in request["events"]:
|
||||
if not is_scoreable_event(event):
|
||||
continue
|
||||
contribution = built["matrix"][event["id"]][candidate_time]
|
||||
evidence.append({
|
||||
"event_id": event["id"], "domain": event["domain"], "candidate_time": candidate_time,
|
||||
|
||||
Reference in New Issue
Block a user