Download orchestrator/strategy.py from ThejasRao/ModPilot: direct link, hf CLI and curl.
- Browser
- Download file 6.67 kB
-
https://huggingface.co/spaces/ThejasRao/ModPilot/resolve/main/orchestrator/strategy.py
- Command line
-
hf download hf://spaces/ThejasRao/ModPilot/orchestrator/strategy.py
-
curl -L -o strategy.py https://huggingface.co/spaces/ThejasRao/ModPilot/resolve/main/orchestrator/strategy.py
6.67 kB
| """Strategy Selector — picks an investigation tier from cheap signals. | |
| Pure function. Sub-50ms latency budget. Spec: docs/Specs.md §7.1, | |
| docs/04-InvestigationEngine.md §2. | |
| Decision order: | |
| 1. Moderator `tier_override` setting (escape hatch). Cold-start floors | |
| a FAST override up to STANDARD per docs/05-Memory.md §cold-start. | |
| 2. DEEP escalation signals — any one trips DEEP. Personality nudges | |
| the thresholds: strict subs escalate sooner, lenient subs later. | |
| 3. FAST shortcut — only when *every* signal says "obvious, low-risk." | |
| 4. Otherwise STANDARD. | |
| Budgets are pinned in Specs §7.1; the open question of "STANDARD = 4 or 5 | |
| tools" defaults to 4 here and will be revisited via ADR. | |
| """ | |
| from __future__ import annotations | |
| from dataclasses import dataclass | |
| from typing import TYPE_CHECKING, Literal | |
| if TYPE_CHECKING: | |
| from store.types import Personality, StrategyTier, TierOverride, UserRiskTier | |
| # === Inputs / decision dataclasses ===================================== | |
| class StrategyInputs: | |
| """Everything the selector needs. Built by the orchestrator from the | |
| report payload + cached subreddit profile + a cheap rule-match precheck.""" | |
| reporter_count: int | |
| velocity_zscore: float | |
| user_risk_tier: UserRiskTier | |
| rule_match_score: float # 0..1 — how strongly the precheck embedding matched a rule | |
| personality: Personality = "balanced" | |
| tier_override: TierOverride = "auto" | |
| cold_start: bool = False | |
| # I-3.9: cached signal — true when prior mod attention or a prior | |
| # thread_context summary recorded escalation on this thread. The | |
| # selector treats this as an additional escalation signal that | |
| # lowers DEEP thresholds by 1 (stacks with personality=strict). | |
| thread_escalated: bool = False | |
| class StrategyDecision: | |
| tier: StrategyTier | |
| tool_budget: int | |
| time_budget_ms: int | |
| cost_budget_usd: float | |
| reasoner_required: bool | |
| rationale: str # plain English; surfaced in logs + investigation audit | |
| # === Budgets — Specs §7.1 ============================================== | |
| class _Budget: | |
| tool_budget: int | |
| time_budget_ms: int | |
| cost_budget_usd: float | |
| reasoner_required: bool | |
| _BUDGETS: dict[str, _Budget] = { | |
| "FAST": _Budget(2, 800, 0.003, reasoner_required=False), | |
| "STANDARD": _Budget(4, 3_000, 0.012, reasoner_required=True), | |
| "DEEP": _Budget(6, 6_000, 0.030, reasoner_required=True), | |
| } | |
| # === Thresholds ======================================================== | |
| _DEEP_REPORTER_COUNT_DEFAULT = 4 | |
| _DEEP_VELOCITY_Z_DEFAULT = 3.0 | |
| _FAST_RULE_MATCH = 0.9 | |
| _FAST_VELOCITY_Z = 0.5 | |
| # === Public API ======================================================== | |
| def select_strategy(inputs: StrategyInputs) -> StrategyDecision: | |
| # 1. Moderator override. | |
| if inputs.tier_override != "auto": | |
| forced = _override_tier(inputs.tier_override) | |
| if inputs.cold_start and forced == "FAST": | |
| return _decision("STANDARD", "cold-start floors FAST override -> standard") | |
| return _decision(forced, f"override -> {forced.lower()}") | |
| # 2. DEEP escalation signals. | |
| deep_signals = _deep_signals(inputs) | |
| if deep_signals: | |
| return _decision("DEEP", "; ".join(deep_signals)) | |
| # 3. FAST shortcut. Cold-start always vetoes FAST per Specs §12.1. | |
| if _fast_eligible(inputs): | |
| return _decision( | |
| "FAST", "single report + strong rule match + trusted/new user + no escalation" | |
| ) | |
| # 4. Default. | |
| return _decision("STANDARD", "no escalation signals, no fast-shortcut conditions met") | |
| # === Internals ========================================================= | |
| def _override_tier(override: TierOverride) -> StrategyTier: | |
| if override == "fast": | |
| return "FAST" | |
| if override == "standard": | |
| return "STANDARD" | |
| if override == "deep": | |
| return "DEEP" | |
| raise ValueError(f"unsupported override {override!r}") | |
| def _deep_signals(inputs: StrategyInputs) -> list[str]: | |
| """Returns the human-readable reasons DEEP triggered, or [] if it didn't.""" | |
| threshold_reporters = _DEEP_REPORTER_COUNT_DEFAULT | |
| threshold_velocity = _DEEP_VELOCITY_Z_DEFAULT | |
| if inputs.personality == "strict": | |
| threshold_reporters -= 1 | |
| threshold_velocity -= 1.0 | |
| elif inputs.personality == "lenient": | |
| threshold_reporters += 1 | |
| threshold_velocity += 1.0 | |
| # I-3.9: cached thread escalation lowers the same thresholds by another | |
| # step. Stacks with personality=strict; a strict sub on an already- | |
| # escalating thread escalates at reporter_count=2 instead of 4. | |
| if inputs.thread_escalated: | |
| threshold_reporters -= 1 | |
| threshold_velocity -= 1.0 | |
| signals: list[str] = [] | |
| if inputs.reporter_count >= threshold_reporters: | |
| signals.append(f"reporter_count={inputs.reporter_count}>={threshold_reporters}") | |
| if inputs.velocity_zscore >= threshold_velocity: | |
| signals.append(f"velocity_z={inputs.velocity_zscore:.1f}>={threshold_velocity:.1f}") | |
| if inputs.user_risk_tier == "watched": | |
| signals.append("user_risk_tier=watched") | |
| # A user-trust + thread-escalation combo is itself a DEEP trigger even | |
| # if neither reporter_count nor velocity is at threshold yet. | |
| if inputs.thread_escalated and inputs.user_risk_tier in ("neutral", "watched"): | |
| signals.append("thread_escalated+user_risk") | |
| return signals | |
| def _fast_eligible(inputs: StrategyInputs) -> bool: | |
| if inputs.cold_start: | |
| return False | |
| # I-3.9: never short-circuit a known-escalating thread with FAST. | |
| if inputs.thread_escalated: | |
| return False | |
| return ( | |
| inputs.reporter_count == 1 | |
| and inputs.velocity_zscore < _FAST_VELOCITY_Z | |
| and inputs.rule_match_score >= _FAST_RULE_MATCH | |
| and inputs.user_risk_tier in ("new", "trusted") | |
| ) | |
| def _decision(tier: StrategyTier, rationale: str) -> StrategyDecision: | |
| b = _BUDGETS[tier] | |
| return StrategyDecision( | |
| tier=tier, | |
| tool_budget=b.tool_budget, | |
| time_budget_ms=b.time_budget_ms, | |
| cost_budget_usd=b.cost_budget_usd, | |
| reasoner_required=b.reasoner_required, | |
| rationale=rationale, | |
| ) | |
| # Re-export the literal tier names for downstream typing. | |
| __all__ = [ | |
| "StrategyDecision", | |
| "StrategyInputs", | |
| "select_strategy", | |
| ] | |
| # Catches "added a tier, forgot the budget" at import time. | |
| _TIER_VALUES: tuple[Literal["FAST", "STANDARD", "DEEP"], ...] = ("FAST", "STANDARD", "DEEP") | |
| assert set(_BUDGETS.keys()) == set(_TIER_VALUES), "_BUDGETS must cover every StrategyTier value" | |