| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160 |
- """Deterministic candidate gates owned by find_agent_v2."""
- from __future__ import annotations
- import json
- import os
- import re
- from datetime import date, datetime, time
- from decimal import Decimal
- from typing import Any, Mapping
- from zoneinfo import ZoneInfo
- def _env_number(name: str, default: float) -> float:
- try:
- return float(os.getenv(name, str(default)))
- except ValueError:
- return default
- def build_rule_snapshot(now: datetime | None = None) -> dict[str, Any]:
- timezone_name = os.getenv("FIND_AGENT_V2_TIMEZONE", "Asia/Shanghai")
- timezone = ZoneInfo(timezone_name)
- current = now or datetime.now(timezone)
- current = current.replace(tzinfo=timezone) if current.tzinfo is None else current.astimezone(timezone)
- return {
- "rule_version": os.getenv("FIND_AGENT_V2_RULE_VERSION", "find-agent-v2-gate-v1"),
- "timezone": timezone_name,
- "min_duration_seconds": int(_env_number("FIND_AGENT_V2_MIN_DURATION_SECONDS", 30)),
- "min_share_count": int(_env_number("FIND_AGENT_V2_MIN_SHARE_COUNT", 1000)),
- "min_content_50_plus_ratio": _env_number("FIND_AGENT_V2_MIN_CONTENT_50_PLUS_RATIO", 0.20),
- "min_account_50_plus_ratio": _env_number("FIND_AGENT_V2_MIN_ACCOUNT_50_PLUS_RATIO", 0.20),
- "event_max_age_days": int(_env_number("FIND_AGENT_V2_EVENT_MAX_AGE_DAYS", 7)),
- "seasonal_max_age_days": int(_env_number("FIND_AGENT_V2_SEASONAL_MAX_AGE_DAYS", 180)),
- "current_datetime": current.isoformat(timespec="seconds"),
- "current_date": current.strftime("%Y-%m-%d"),
- }
- def _rules(value: Mapping[str, Any] | str | None) -> dict[str, Any]:
- if isinstance(value, str):
- try:
- loaded = json.loads(value)
- except (TypeError, ValueError):
- loaded = {}
- else:
- loaded = dict(value or {})
- return {**build_rule_snapshot(), **loaded}
- def parse_datetime_value(value: Any, *, timezone_name: str = "Asia/Shanghai") -> datetime | None:
- if value in (None, "") or isinstance(value, bool):
- return None
- timezone = ZoneInfo(timezone_name)
- if isinstance(value, datetime):
- parsed = value
- elif isinstance(value, date):
- parsed = datetime.combine(value, time.min)
- elif isinstance(value, (int, float, Decimal)):
- number = float(value)
- if number > 10_000_000_000:
- number /= 1000
- try:
- parsed = datetime.fromtimestamp(number, timezone)
- except (OSError, OverflowError, ValueError):
- return None
- else:
- text = str(value).strip()
- if re.fullmatch(r"\d{10,13}", text):
- return parse_datetime_value(int(text), timezone_name=timezone_name)
- parsed = None
- for candidate in (text.replace("Z", "+00:00"), text.replace("/", "-")):
- try:
- parsed = datetime.fromisoformat(candidate)
- break
- except ValueError:
- continue
- if parsed is None:
- return None
- return parsed.replace(tzinfo=timezone) if parsed.tzinfo is None else parsed.astimezone(timezone)
- def _number(value: Any) -> float | None:
- if value in (None, "") or isinstance(value, bool):
- return None
- try:
- return float(value)
- except (TypeError, ValueError):
- return None
- def _strong(candidate: Mapping[str, Any]) -> bool:
- scores = [_number(candidate.get(key)) for key in ("relevance_score", "elder_score", "share_score")]
- value = _number(candidate.get("value_score"))
- return value is not None and value >= 0.65 or all(score is not None and score >= floor for score, floor in zip(scores, (0.70, 0.70, 0.65), strict=True))
- def _temporal(candidate: Mapping[str, Any], rules: Mapping[str, Any]) -> dict[str, Any]:
- timezone_name = str(rules["timezone"])
- current = parse_datetime_value(rules.get("current_datetime"), timezone_name=timezone_name) or datetime.now(ZoneInfo(timezone_name))
- published = parse_datetime_value(candidate.get("publish_at"), timezone_name=timezone_name)
- title = str(candidate.get("title") or "")
- temporal_type = str(candidate.get("temporal_type") or "evergreen")
- if published is None:
- compensated = _strong(candidate) and not any(word in title for word in ("今天", "今日", "明天", "刚刚", "突发"))
- return {"status": "pass" if compensated else "unknown", "reason_code": None if compensated else "TEMPORAL_UNKNOWN", "temporal_type": temporal_type, "evidence": {"publish_at": None, "compensated": compensated}}
- age_days = max(0.0, (current - published).total_seconds() / 86400)
- reason_code = None
- if any(word in title for word in ("今天", "今日", "明天", "昨日", "昨天")) and published.date() != current.date():
- reason_code = "RELATIVE_DATE_EXPIRED"
- elif temporal_type == "event" and age_days > int(rules["event_max_age_days"]):
- reason_code = "EVENT_EXPIRED"
- elif temporal_type == "seasonal" and age_days > int(rules["seasonal_max_age_days"]):
- reason_code = "SEASONAL_EXPIRED"
- return {"status": "fail" if reason_code else "pass", "reason_code": reason_code, "temporal_type": temporal_type, "evidence": {"publish_at": published.isoformat(timespec="seconds"), "content_age_days": round(age_days, 3)}}
- def evaluate_candidate_gate(candidate: Mapping[str, Any], rule_snapshot: Mapping[str, Any] | str | None) -> dict[str, Any]:
- rules = _rules(rule_snapshot)
- temporal = _temporal(candidate, rules)
- duration = _number(candidate.get("duration_seconds"))
- shares = _number(candidate.get("share_count"))
- content_ratio = _number(candidate.get("content_50_plus_ratio"))
- account_ratio = _number(candidate.get("account_50_plus_ratio"))
- checks: list[dict[str, Any]] = [{"name": "temporal", **temporal}]
- def threshold(name: str, actual: float | None, minimum: float, missing: str, low: str, compensate: bool = False) -> None:
- if actual is None:
- status, reason = ("pass", None) if compensate else ("fail", missing)
- else:
- status, reason = ("pass", None) if actual >= minimum else ("fail", low)
- checks.append({"name": name, "status": status, "reason_code": reason, "actual": actual, "threshold": minimum, "compensated": actual is None and compensate})
- min_duration = float(rules["min_duration_seconds"])
- min_shares = float(rules["min_share_count"])
- threshold("duration_seconds", duration, min_duration, "DURATION_UNKNOWN", "DURATION_TOO_SHORT", compensate=_strong(candidate) or shares is not None and shares >= min_shares * 1.5)
- likes, plays = _number(candidate.get("like_count")), _number(candidate.get("play_count"))
- threshold("share_count", shares, min_shares, "SHARE_COUNT_UNKNOWN", "SHARE_COUNT_TOO_LOW", compensate=_strong(candidate) or bool(likes and likes >= 5000) or bool(plays and plays >= 50000))
- min_content = float(rules["min_content_50_plus_ratio"])
- min_account = float(rules["min_account_50_plus_ratio"])
- portrait_pass = bool(content_ratio is not None and content_ratio >= min_content or account_ratio is not None and account_ratio >= min_account)
- portrait_compensated = content_ratio is None and account_ratio is None and _strong(candidate)
- checks.append({
- "name": "elder_portrait", "status": "pass" if portrait_pass or portrait_compensated else "fail",
- "reason_code": None if portrait_pass or portrait_compensated else "CONTENT_PORTRAIT_MISSING" if content_ratio is None and account_ratio is None else "PORTRAIT_50_PLUS_TOO_LOW",
- "actual": {"content_50_plus_ratio": content_ratio, "account_50_plus_ratio": account_ratio},
- "threshold": {"min_content_50_plus_ratio": min_content, "min_account_50_plus_ratio": min_account},
- "compensated": portrait_compensated,
- })
- failed = [str(check["reason_code"]) for check in checks if check.get("status") != "pass" and check.get("reason_code")]
- content_status = "missing" if content_ratio is None else "pass" if content_ratio >= min_content else "fail"
- account_status = "missing" if account_ratio is None else "pass" if account_ratio >= min_account else "fail"
- return {
- "rule_version": str(rules["rule_version"]), "status": "pass" if not failed else "fail",
- "primary_eligible": not failed, "failed_reason_codes": failed, "checks": checks,
- "temporal": temporal, "content_portrait_status": content_status,
- "account_portrait_status": account_status,
- "portrait_conflict": content_status in {"pass", "fail"} and account_status in {"pass", "fail"} and content_status != account_status,
- }
|