Spaces:
Running
Running
File size: 9,305 Bytes
914024f | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 | """Runtime-mutable configuration — RSS feeds and operational thresholds exposed via /config/*.
In-memory only (module-level state): the app runs on HuggingFace Spaces where
restarts are frequent, so no persistence layer is introduced for this. Every
default below is the exact value that was previously hardcoded as a module
constant elsewhere in the pipeline — see PARAMETER_SPECS for the original
file:line each one came from.
NOT included here (intentionally out of scope): SEVERITY_WEIGHTS and the
0.85/0.15 coverage-bonus coefficients (pipeline/scoring/tcs.py) — the TCS
formula itself stays hardcoded and non-editable from the UI.
"""
from __future__ import annotations
from dataclasses import dataclass
from threading import Lock
from urllib.parse import urlparse
# RSS feeds
# Moved here (previously module constants in rss_verifier.py) so the effective
# feed list can be mutated at runtime without rss_verifier importing back into
# a config module that itself needs the feed list.
DEFAULT_FEEDS: list[str] = [
"http://feeds.bbci.co.uk/news/politics/rss.xml",
"https://feeds.npr.org/1014/rss.xml",
"https://rss.nytimes.com/services/xml/rss/nyt/Politics.xml",
"https://thehill.com/homenews/feed/",
"https://rss.politico.com/politics-news.xml",
"https://www.theguardian.com/politics/rss",
"https://www.theguardian.com/us-news/rss",
"https://feeds.skynews.com/feeds/rss/politics.xml",
]
FEED_NAMES: dict[str, str] = {
"http://feeds.bbci.co.uk/news/politics/rss.xml": "BBC Politics",
"https://feeds.npr.org/1014/rss.xml": "NPR Politics",
"https://rss.nytimes.com/services/xml/rss/nyt/Politics.xml": "NYT Politics",
"https://thehill.com/homenews/feed/": "The Hill",
"https://rss.politico.com/politics-news.xml": "Politico",
"https://www.theguardian.com/politics/rss": "The Guardian Politics",
"https://www.theguardian.com/us-news/rss": "The Guardian US",
"https://feeds.skynews.com/feeds/rss/politics.xml": "Sky News Politics",
}
_rss_lock = Lock()
_predefined_enabled: dict[str, bool] = {url: True for url in DEFAULT_FEEDS}
_custom_feeds: list[str] = []
def feed_name(url: str) -> str:
return FEED_NAMES.get(url, url.split("/")[2] if "/" in url else url)
def get_rss_feeds() -> dict:
"""Returns {"predefined": [{"url","name","enabled"}, ...8], "custom": [urls]}."""
with _rss_lock:
return {
"predefined": [
{"url": url, "name": feed_name(url), "enabled": _predefined_enabled[url]}
for url in DEFAULT_FEEDS
],
"custom": list(_custom_feeds),
}
def get_effective_feed_urls() -> list[str]:
"""(enabled predefined) union (custom) — the list RSSVerifier actually queries."""
with _rss_lock:
enabled = [url for url in DEFAULT_FEEDS if _predefined_enabled.get(url, True)]
return enabled + list(_custom_feeds)
def set_predefined_enabled(flags: dict[str, bool]) -> None:
"""flags must map exactly the 8 predefined URLs to booleans."""
if set(flags.keys()) != set(DEFAULT_FEEDS):
raise ValueError("Must specify the enabled flag for exactly the 8 predefined feed URLs.")
with _rss_lock:
for url, enabled in flags.items():
_predefined_enabled[url] = bool(enabled)
def add_custom_feed(url: str) -> None:
parsed = urlparse(url)
if parsed.scheme not in ("http", "https") or not parsed.netloc:
raise ValueError(f"Invalid feed URL: '{url}'. Must be a well-formed http(s) URL.")
with _rss_lock:
_custom_feeds.append(url)
def remove_custom_feed(index: int) -> None:
with _rss_lock:
if index < 0 or index >= len(_custom_feeds):
raise IndexError(f"Custom feed index {index} out of range (0..{len(_custom_feeds) - 1}).")
_custom_feeds.pop(index)
def reset_rss_feeds() -> None:
with _rss_lock:
for url in DEFAULT_FEEDS:
_predefined_enabled[url] = True
_custom_feeds.clear()
# Operational parameters (Table 5.4 minus severity weights / Wikidata cache TTL / batch size)
@dataclass(frozen=True)
class ParameterSpec:
key: str
label: str
group: str # "classification" | "internal" | "external"
unit: str # "score" | "days" | "years"
default: float
min: float
max: float
step: float
PARAMETER_SPECS: dict[str, ParameterSpec] = {
"fake_threshold": ParameterSpec(
"fake_threshold", "Classification Threshold FAKE/TRUE (θ)", "classification", "score",
default=0.75, min=0.0, max=1.0, step=0.05,
),
"tcs_very_consistent": ParameterSpec(
"tcs_very_consistent", 'TCS Threshold "Highly Consistent"', "classification", "score",
default=0.80, min=0.0, max=1.0, step=0.05,
),
"tcs_moderate": ParameterSpec(
"tcs_moderate", 'TCS Threshold "Moderate"', "classification", "score",
default=0.50, min=0.0, max=1.0, step=0.05,
),
"tcs_suspicious": ParameterSpec(
"tcs_suspicious", 'TCS Threshold "Suspicious"', "classification", "score",
default=0.20, min=0.0, max=1.0, step=0.05,
),
"inverted_interval_tolerance_days": ParameterSpec(
"inverted_interval_tolerance_days", "Inverted Interval Tolerance (V3)", "internal", "days",
default=30, min=0, max=90, step=10,
),
"max_plausible_tenure_years": ParameterSpec(
"max_plausible_tenure_years", "Maximum Plausible Duration (V3)", "internal", "years",
default=50, min=0, max=100, step=10,
),
"absurd_duration_years": ParameterSpec(
"absurd_duration_years", "Duration Excluded from Analysis (V3)", "internal", "years",
default=80, min=0, max=150, step=10,
),
"election_to_office_buffer_days": ParameterSpec(
"election_to_office_buffer_days", "Election-to-Office Buffer (V5)", "internal", "days",
default=180, min=0, max=365, step=10,
),
"action_before_office_buffer_days": ParameterSpec(
"action_before_office_buffer_days", "Action-Before-Office Buffer (V8)", "internal", "days",
default=90, min=0, max=180, step=10,
),
"text_similarity_threshold_v4": ParameterSpec(
"text_similarity_threshold_v4", "Text Similarity Threshold (V4)", "internal", "score",
default=0.85, min=0.0, max=1.0, step=0.05,
),
"min_fact_confidence": ParameterSpec(
"min_fact_confidence", "Minimum Fact Confidence Threshold (C2)", "internal", "score",
default=0.30, min=0.0, max=1.0, step=0.05,
),
"external_date_tolerance_days": ParameterSpec(
"external_date_tolerance_days", "External Date Tolerance (L1, L3)", "external", "days",
default=200, min=0, max=400, step=10,
),
"canonical_event_tolerance_days": ParameterSpec(
"canonical_event_tolerance_days", "Canonical Event Tolerance (L1)", "external", "days",
default=100, min=0, max=200, step=10,
),
"canonical_event_similarity_threshold": ParameterSpec(
"canonical_event_similarity_threshold", "Canonical Event Lexical Similarity Threshold (L1)", "external", "score",
default=0.75, min=0.0, max=1.0, step=0.05,
),
"cross_article_similarity_threshold": ParameterSpec(
"cross_article_similarity_threshold", "Cross-Article Similarity Threshold (L2)", "external", "score",
default=0.80, min=0.0, max=1.0, step=0.05,
),
}
_param_lock = Lock()
_param_values: dict[str, float] = {key: spec.default for key, spec in PARAMETER_SPECS.items()}
def get_value(key: str) -> float:
"""Read a parameter's current value — call at point of use, not at import time."""
return _param_values[key]
def get_parameters() -> list[dict]:
with _param_lock:
return [
{
"key": key,
"label": spec.label,
"group": spec.group,
"unit": spec.unit,
"value": _param_values[key],
"default": spec.default,
"min": spec.min,
"max": spec.max,
"step": spec.step,
}
for key, spec in PARAMETER_SPECS.items()
]
def _validate(key: str, value: float) -> None:
if key not in PARAMETER_SPECS:
raise ValueError(f"Unknown parameter: '{key}'.")
spec = PARAMETER_SPECS[key]
if not isinstance(value, (int, float)) or isinstance(value, bool):
raise ValueError(f"'{key}' must be numeric (got {type(value).__name__}).")
if not (spec.min <= value <= spec.max):
raise ValueError(f"'{key}' must be between {spec.min} and {spec.max} (got {value}).")
steps = (value - spec.min) / spec.step
if abs(steps - round(steps)) > 1e-9:
raise ValueError(
f"'{key}' must land on the increment grid: {spec.min} + n*{spec.step} (got {value})."
)
def set_parameters(updates: dict[str, float]) -> None:
"""Validates every update against [min,max] and the increment grid before applying any of them."""
for key, value in updates.items():
_validate(key, value)
with _param_lock:
_param_values.update(updates)
def reset_parameters() -> None:
with _param_lock:
_param_values.clear()
_param_values.update({key: spec.default for key, spec in PARAMETER_SPECS.items()})
|