Files
Shadowbroker/backend/routers/analytics.py
T
Shadowbroker cfbeabda1e Feat/gt analytics openclaw (#392)
* feat(telegram): auto-translate OSINT channel posts to English

Cherry-picked from @Bobpick PR #391 (telegram-only slice): server-side translation during fetch, SHOW ORIGINAL toggle in TelegramOsintPopup, and on-demand /api/telegram-feed?lang=.

Co-authored-by: Robert Pickett <bobpickettsr@yahoo.com>
Co-authored-by: Cursor <cursoragent@cursor.com>

* feat(gt): experimental Derived OSINT analytics with lean-node safeguards

Cherry-picked from @Bobpick PR #391 (GT + OpenClaw slice): Bayesian strategic-risk engine, map overlay, OpenClaw commands, and telegram_rhetoric watchdog. Off by default (GT_ANALYTICS_ENABLED=false, gt_risk layer false). 1 vCPU nodes get cgroup detection, UI warning on layer toggle, and lean profile that skips scheduled ingest/Louvain unless GT_ANALYTICS_ACK_LOW_CPU=true. Backtest HUD removed from dashboard (OpenClaw/API regression only).

Co-authored-by: Robert Pickett <bobpickettsr@yahoo.com>
Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Robert Pickett <bobpickettsr@yahoo.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-16 17:05:46 -06:00

339 lines
10 KiB
Python

"""Strategic Risk Analytics API — game-theoretic early warning overlays."""
from __future__ import annotations
import logging
from typing import Any
from fastapi import APIRouter, Depends, HTTPException, Request
from pydantic import BaseModel, Field
from auth import require_local_operator
from limiter import limiter
from analytics.backtest import (
DEFAULT_BACKTEST_ALERT_THRESHOLD,
run_historical_backtest,
tune_alert_threshold,
)
from analytics.feed_adapter import normalize_feed_item
from analytics.integration import get_gt_engine, refresh_from_latest_data
from analytics.gt_alerts import top_gt_alerts
from analytics.micro_rolling import micro_rolling_report
from analytics.rolling_backtest import (
freeze_weekly_snapshot,
label_region,
label_regions,
rolling_alert_threshold,
rolling_report,
score_week,
)
from analytics.weekly_store import load_week
from analytics.settings import gt_analytics_enabled
from services.fetchers._store import _data_lock, get_latest_data_subset_refs, latest_data
logger = logging.getLogger(__name__)
router = APIRouter()
class RiskHeatmapRequest(BaseModel):
"""Optional batch ingest + refresh controls for POST /api/analytics/risk_heatmap."""
refresh: bool = True
items: list[dict[str, Any]] = Field(default_factory=list)
class RollingFreezeRequest(BaseModel):
week_id: str | None = None
force: bool = False
class RollingLabelEntry(BaseModel):
region: str
label: str
notes: str = ""
class RollingLabelRequest(BaseModel):
week_id: str
labels: list[RollingLabelEntry] = Field(default_factory=list)
def _empty_heatmap() -> dict[str, Any]:
return {
"enabled": False,
"type": "FeatureCollection",
"features": [],
"clusters": [],
"processed": 0,
"timestamp": None,
}
def _gt_risk_payload() -> dict[str, Any]:
snap = get_latest_data_subset_refs("gt_risk")
payload = snap.get("gt_risk")
if not isinstance(payload, dict):
return _empty_heatmap()
heatmap = payload.get("heatmap") or {"type": "FeatureCollection", "features": []}
return {
"enabled": bool(payload.get("enabled")),
"type": heatmap.get("type", "FeatureCollection"),
"features": list(heatmap.get("features") or []),
"clusters": list(payload.get("clusters") or []),
"processed": int(payload.get("processed") or 0),
"timestamp": payload.get("timestamp"),
}
@router.get("/api/analytics/risk_heatmap")
@limiter.limit("60/minute")
async def risk_heatmap_get(request: Request) -> dict[str, Any]:
"""Return cached GeoJSON risk overlay (posterior scores per region)."""
if not gt_analytics_enabled():
return _empty_heatmap()
return _gt_risk_payload()
@router.post("/api/analytics/risk_heatmap")
@limiter.limit("12/minute")
async def risk_heatmap_post(
request: Request,
body: RiskHeatmapRequest,
_: None = Depends(require_local_operator),
) -> dict[str, Any]:
"""
Ingest optional feed items and/or refresh beliefs from latest intel layers.
Requires local operator auth — intended for OpenClaw agents and admin tooling.
"""
if not gt_analytics_enabled():
raise HTTPException(status_code=503, detail="Strategic Risk Analytics is disabled")
engine = get_gt_engine()
if engine is None:
raise HTTPException(status_code=503, detail="Strategic Risk Analytics engine unavailable")
ingested = 0
for raw in body.items:
if not isinstance(raw, dict):
continue
source_type = str(raw.get("source_type") or "manual")
item = normalize_feed_item(raw, source_type=source_type)
result = engine.process_feed_item(item)
if result and not result.get("skipped"):
ingested += 1
summary: dict[str, Any] = {"ingested": ingested}
if body.refresh:
with _data_lock:
snapshot = dict(latest_data)
summary.update(refresh_from_latest_data(snapshot, persist=True))
payload = _gt_risk_payload()
payload["ingested"] = ingested
payload["refresh"] = bool(body.refresh)
return payload
@router.get("/api/analytics/dossier/{region}")
@limiter.limit("30/minute")
async def analytics_dossier(request: Request, region: str) -> dict[str, Any]:
"""Game-theoretic rationale, recent costly signals, and scenario sketches."""
region_key = str(region or "").strip().lower()
if not region_key or len(region_key) > 120:
raise HTTPException(status_code=400, detail="Invalid region identifier")
if not gt_analytics_enabled():
return {
"enabled": False,
"region": region_key,
"current_risk": 0.0,
"interpretation": "Strategic Risk Analytics is disabled.",
"recent_signals": [],
"scenarios": [],
}
engine = get_gt_engine()
if engine is None:
raise HTTPException(status_code=503, detail="Strategic Risk Analytics engine unavailable")
dossier = engine.get_dossier(region_key)
dossier["enabled"] = True
return dossier
@router.get("/api/analytics/backtest")
@limiter.limit("6/minute")
async def analytics_backtest(
request: Request,
expanded: bool = True,
tune: bool = False,
target_confidence: float = 0.95,
) -> dict[str, Any]:
"""
Run labeled historical backtest and return accuracy + Wilson 95% CI.
``confidence_rate`` is the Wilson lower bound (conservative pass metric).
"""
if not gt_analytics_enabled():
return {
"enabled": False,
"message": "Strategic Risk Analytics is disabled.",
}
if tune:
threshold, report = tune_alert_threshold(target_confidence=target_confidence)
else:
threshold = DEFAULT_BACKTEST_ALERT_THRESHOLD
report = run_historical_backtest(
use_expanded_suite=expanded,
alert_threshold=threshold,
target_confidence=target_confidence,
)
payload = report.to_dict()
payload["enabled"] = True
payload["expanded_suite"] = expanded
payload["tuned"] = tune
payload["recommended_alert_threshold"] = threshold
return payload
@router.get("/api/analytics/rolling")
@limiter.limit("12/minute")
async def analytics_rolling(
request: Request,
weeks: int = 8,
target_confidence: float = 0.80,
) -> dict[str, Any]:
"""Rolling weekly operational validation — accuracy trend with delayed labels."""
if not gt_analytics_enabled():
return {
"enabled": False,
"message": "Strategic Risk Analytics is disabled.",
}
report = rolling_report(weeks=max(1, min(weeks, 52)), target_confidence=target_confidence)
report["enabled"] = True
return report
@router.get("/api/analytics/alerts")
@limiter.limit("30/minute")
async def analytics_top_alerts(
request: Request,
limit: int = 8,
) -> dict[str, Any]:
"""Top GT risk regions ranked by score — fly-to targets for the map."""
if not gt_analytics_enabled():
return {
"enabled": False,
"message": "Strategic Risk Analytics is disabled.",
}
report = top_gt_alerts(limit=max(1, min(limit, 25)))
report["enabled"] = True
return report
@router.get("/api/analytics/rolling/micro")
@limiter.limit("30/minute")
async def analytics_rolling_micro(
request: Request,
window_days: int = 3,
limit: int = 15,
) -> dict[str, Any]:
"""Rolling 3-day micro average — spot vs baseline, ignition detection."""
if not gt_analytics_enabled():
return {
"enabled": False,
"message": "Strategic Risk Analytics is disabled.",
}
report = micro_rolling_report(
window_days=max(2, min(window_days, 7)),
limit=max(1, min(limit, 50)),
)
report["enabled"] = True
return report
@router.get("/api/analytics/rolling/{week_id}")
@limiter.limit("12/minute")
async def analytics_rolling_week(request: Request, week_id: str) -> dict[str, Any]:
"""Return a single frozen week snapshot and its score."""
if not gt_analytics_enabled():
return {"enabled": False, "message": "Strategic Risk Analytics is disabled."}
snapshot = load_week(str(week_id).strip())
if snapshot is None:
raise HTTPException(status_code=404, detail=f"Week {week_id} not found")
score = score_week(snapshot)
return {
"enabled": True,
"week_id": snapshot.week_id,
"snapshot": snapshot.to_dict(),
"score": score.to_dict(),
"alert_threshold": rolling_alert_threshold(),
}
@router.post("/api/analytics/rolling/freeze")
@limiter.limit("6/minute")
async def analytics_rolling_freeze(
request: Request,
body: RollingFreezeRequest,
_: None = Depends(require_local_operator),
) -> dict[str, Any]:
"""Freeze current GT scores for the ISO week (idempotent unless force=true)."""
if not gt_analytics_enabled():
raise HTTPException(status_code=503, detail="Strategic Risk Analytics is disabled")
result = freeze_weekly_snapshot(
week_id=body.week_id,
force=body.force,
frozen_by="api",
)
if not result.get("ok"):
raise HTTPException(status_code=503, detail=result.get("detail", "Freeze failed"))
result["enabled"] = True
return result
@router.post("/api/analytics/rolling/label")
@limiter.limit("12/minute")
async def analytics_rolling_label(
request: Request,
body: RollingLabelRequest,
_: None = Depends(require_local_operator),
) -> dict[str, Any]:
"""Apply delayed outcome labels to a frozen week."""
if not gt_analytics_enabled():
raise HTTPException(status_code=503, detail="Strategic Risk Analytics is disabled")
week_id = str(body.week_id or "").strip()
if not week_id:
raise HTTPException(status_code=400, detail="week_id required")
if len(body.labels) == 1:
entry = body.labels[0]
result = label_region(
week_id,
entry.region,
entry.label, # type: ignore[arg-type]
notes=entry.notes,
labeled_by="api",
)
else:
result = label_regions(
week_id,
[row.model_dump() for row in body.labels],
labeled_by="api",
)
if not result.get("ok"):
raise HTTPException(status_code=404, detail=result.get("detail", "Label failed"))
result["enabled"] = True
return result