Files
mvanhorn--last30days-skill/skills/last30days/scripts/lib/backends.py
T
2026-08-14 10:16:04 -07:00

701 lines
26 KiB
Python

"""Backend-chain descriptors with predicted selection (doctor, R4).
Chained sources declare their routing here ONCE — imported from the
definitions ``lib/env.py`` already owns (chain order, pin var names) — and
``resolve()`` turns side-effect-free probes into a truthful prediction of
what the next run will do.
Two resolution modes:
- ``alternative`` (X, YouTube, web search): the pipeline tries genuinely
interchangeable backends in a declared order. Resolution probes ALL
candidates first, then picks (collect-then-pick): the first fully-usable
backend wins the "will use" prediction; otherwise the best degraded
candidate resolves with a warn tier; otherwise the source is an error
carrying the highest-priority backend's prescription. Collecting before
picking prevents an installed-but-unauthenticated preferred backend from
shadowing a fully working fallback.
- ``conditional`` (Reddit): routing is per-query and outcome-dependent —
public keyless composite by default, ScrapeCreators backfill only when
results fall below the configured thinness floor (see the gating in
``lib/pipeline.py``). No probe can pick one winner, so resolution renders
honest conditional wording instead of an ``active_backend``. Reddit's
internal keyless lanes (rss/listing/arctic/shreddit) are sub-probe detail
inside the public composite, never chain entries.
``active_backend`` semantics: a PREDICTION — "the first backend the probes
say the next run will try" — rendered as "will use". It is not an
observation of what served a past run, and runtime failover can still
diverge mid-run (a present-but-expired paid key passes a presence probe).
Paid lanes (xai, xquik, serper, and every other API-key backend, including
ScrapeCreators) probe KEY PRESENCE ONLY: a dict lookup, never a network
call or credential spend. Binary-backed lanes reuse the U1 dependency
probe layer (``health.probe_dependency``) so a stale shim reads as BROKEN,
not available (#692).
This module observes and predicts only. It must never alter which backend
the pipeline actually uses; parity with the pipeline's pre-failover
selection is asserted in ``tests/test_backend_descriptors.py``.
"""
from __future__ import annotations
from dataclasses import dataclass
from shutil import which
from typing import Any, Callable, Dict, List, Optional, Tuple
from . import env, health, prescriptions
# Resolution modes.
MODE_ALTERNATIVE = "alternative" # probe-ordered chain, first-usable wins
MODE_CONDITIONAL = "conditional" # per-query routing; wording, never a winner
# Rollup tiers for a resolved chain (doctor maps these into its R1 table).
TIER_OK = "ok"
TIER_WARN = "warn"
TIER_ERROR = "error"
# Web search backend order. grounding.web_search's auto branch owns the
# runtime behavior (brave -> exa -> serper -> parallel -> keyless floor);
# there is no importable constant there, so this declaration is guarded by
# the grounding-auto parity test rather than an import.
WEB_BACKEND_ORDER: Tuple[str, ...] = ("brave", "exa", "serper", "parallel", "keyless")
# YouTube backend order (pipeline: yt-dlp first, ScrapeCreators search
# fallback when yt-dlp is absent or fails — see lib/pipeline.py).
YOUTUBE_BACKEND_ORDER: Tuple[str, ...] = ("yt-dlp", "scrapecreators")
# Chain-failure fixes embed the registry's CLI forms (KTD 7): the command a
# backend finding prescribes and the one doctor/quality-nudge render for the
# same failure mode come from one entry and cannot drift.
_SC_PRESCRIPTION = (
"set SCRAPECREATORS_API_KEY (free 10,000-call signup: "
f"{prescriptions.get('scrapecreators', 'key_missing').fix_cli})"
)
_X_COOKIES_PRESCRIPTION = (
"run setup with browser-cookie consent: "
f"{prescriptions.get('x', 'cookies_missing').fix_cli}"
)
@dataclass
class BackendFinding:
"""Side-effect-free probe outcome for one backend of a chained source.
``status`` uses the ``lib.health`` vocabulary (OK/DEGRADED/MISSING/
BROKEN/TIMEOUT/ERROR). ``prescription`` is the fix when non-OK.
``requires`` is the backend's requirement note for report rendering.
"""
name: str
status: str
detail: str = ""
prescription: str = ""
requires: str = ""
@property
def usable(self) -> bool:
"""Fully or partially usable (OK/DEGRADED) — eligible for selection."""
return self.status in (health.OK, health.DEGRADED)
@dataclass(frozen=True)
class BackendSpec:
"""One backend in a chain: name, probe, requirement note, paid flag.
``probe`` must be side-effect-free. When ``paid`` is True the probe is
key-presence only: no subprocess, no network, no credential spend.
``opt_in`` marks backends that are never auto-selected and require an
explicit pin (grok).
"""
name: str
requires: str
probe: Callable[[Dict[str, Any]], "BackendFinding"]
paid: bool = False
opt_in: bool = False
@dataclass(frozen=True)
class ChainDescriptor:
"""A chained source's declared routing: backends, mode, and pin knob."""
source: str
mode: str
backends: Tuple[BackendSpec, ...]
pin_var: Optional[str] = None # env var pin (X, Reddit)
pin_flag: Optional[str] = None # CLI flag pin (web: --web-backend)
@dataclass
class BackendResolution:
"""Resolved routing for one chained source.
``active_backend`` is the will-use PREDICTION for alternative chains
and always None for conditional mode (Reddit never gets a computed
winner — ``conditional`` carries the honest wording instead).
"""
source: str
mode: str
chain: List[str]
findings: List[BackendFinding]
active_backend: Optional[str] = None
tier: str = TIER_OK
pinned: bool = False
pin: Optional[str] = None
prescription: str = ""
conditional: str = ""
@property
def summary(self) -> str:
"""One-line rendering: will-use prediction or conditional wording."""
if self.mode == MODE_CONDITIONAL:
return self.conditional
if self.active_backend is None:
line = f"no usable backend (chain: {' -> '.join(self.chain)})"
if self.prescription:
line += f"; fix: {self.prescription}"
return line
line = f"will use: {self.active_backend}"
if self.pinned:
line += f" (pinned via {self._pin_origin()})"
return line
def _pin_origin(self) -> str:
d = DESCRIPTORS.get(self.source)
if d is None:
return "pin"
return d.pin_var or d.pin_flag or "pin"
# ---------------------------------------------------------------------------
# Probes. All side-effect-free; paid lanes are pure dict lookups.
# ---------------------------------------------------------------------------
def _key_probe(name: str, key_var: str, requires: str, note: str = "") -> Callable:
"""Key-presence probe for a paid API lane. Never touches the network."""
def probe(config: Dict[str, Any]) -> BackendFinding:
if config.get(key_var):
return BackendFinding(
name=name,
status=health.OK,
detail=f"{key_var} present",
requires=requires,
)
prescription = note or f"set {key_var} in ~/.config/last30days/.env"
return BackendFinding(
name=name,
status=health.MISSING,
detail=f"{key_var} not set",
prescription=prescription,
requires=requires,
)
return probe
def _probe_bird(config: Dict[str, Any]) -> BackendFinding:
"""Bird = vendored X GraphQL client (node script) + browser-cookie creds.
Cookie presence is checked FIRST, mirroring ``env._x_backend_available``'s
gating (``has_bird_creds and is_bird_installed()``): without cookies bird
is unconfigured regardless of node/script state, and the fix is the
cookie-consent flow — a broken node runtime must not turn an unconfigured
backend into an error carrying a node prescription.
"""
from . import bird_x
requires = "X browser cookies (AUTH_TOKEN/CT0) + node"
if not (config.get("AUTH_TOKEN") and config.get("CT0")):
return BackendFinding(
name="bird",
status=health.MISSING,
detail="X browser cookies (AUTH_TOKEN/CT0) not configured",
prescription=_X_COOKIES_PRESCRIPTION,
requires=requires,
)
if not bird_x.is_bird_installed():
# Distinguish a missing/broken node runtime from a missing script.
node = health.probe_dependency("node")
if node.status != health.OK:
return BackendFinding(
name="bird",
status=node.status,
detail=node.detail,
prescription=node.prescription,
requires=requires,
)
return BackendFinding(
name="bird",
status=health.MISSING,
detail="vendored bird-search client not found",
prescription="reinstall the skill (npx skills add . -g -y) to restore lib/vendor/bird-search",
requires=requires,
)
node = health.probe_dependency("node")
if node.status != health.OK:
# Resolvable-but-broken node (stale shim) must not read as usable.
return BackendFinding(
name="bird",
status=node.status,
detail=node.detail,
prescription=node.prescription,
requires=requires,
)
return BackendFinding(
name="bird",
status=health.OK,
detail="browser-cookie auth (AUTH_TOKEN/CT0) configured",
requires=requires,
)
def _probe_grok(config: Dict[str, Any]) -> BackendFinding:
"""grok CLI = keyless X. LOCAL-ONLY probe, like _probe_xurl.
Deliberately does NOT call ``health.probe_dependency``: that helper runs
``subprocess.run([name, "--version"])``, and the whole-doctor-path test
patches ``subprocess.run`` to raise.
Consequence to be honest about: a grok binary that resolves on PATH but
will not execute (the stale-shim class) reports OK here and fails only when
a real run shells out. ``grok_x.is_available`` does not close that gap
either -- it is also filesystem-only. ``health.probe_dependency("grok")``
is the executing probe, and it runs in doctor's CLI-health block rather
than on this no-subprocess path.
"""
from . import grok_x
requires = "grok CLI installed + signed in (no X credential)"
if which("grok") is None:
off_path = health._off_path_binary("grok")
if off_path is not None:
return BackendFinding(
name="grok",
status=health.MISSING,
requires=requires,
detail=f"grok is installed at {off_path} but that directory is not on this process's PATH",
prescription=f'add {off_path.parent} to PATH (e.g. export PATH="{off_path.parent}:$PATH")',
)
return BackendFinding(
name="grok",
status=health.MISSING,
requires=requires,
detail="grok CLI not found on PATH",
prescription=(
"install the Grok CLI: curl -fsSL https://x.ai/cli/install.sh | bash, "
"then run `grok login`"
),
)
store_status, store_detail, expires_at = grok_x.stored_auth_status()
if store_status == grok_x.AUTH_OK:
return BackendFinding(
name="grok",
status=health.OK,
requires=requires,
detail=f"{store_detail} (not live-verified until a run)",
)
if store_status == grok_x.AUTH_EXPIRED:
expiry_str = expires_at.isoformat() if expires_at else "unknown"
return BackendFinding(
name="grok",
status=health.DEGRADED,
requires=requires,
detail=(
f"Grok session expired at {expiry_str}; "
"refresh happens at run time (if revoked, run `grok login --device-auth`)"
),
prescription="grok login --device-auth",
)
if store_status == grok_x.AUTH_ERROR:
return BackendFinding(
name="grok",
status=health.ERROR,
requires=requires,
detail=store_detail,
prescription="grok login",
)
return BackendFinding(
name="grok",
status=health.MISSING,
requires=requires,
detail="grok CLI installed but not signed in",
prescription="grok login",
)
def _probe_xurl(config: Dict[str, Any]) -> BackendFinding:
"""xurl = official X API v2 CLI (OAuth2). Free lane; LOCAL-ONLY probe.
Doctor's no-network guarantee forbids the live ``xurl whoami`` check
(``xurl_x.is_available()`` — an authenticated X API call, reserved for
research time). This probe keys on local evidence instead: the binary
on PATH plus xurl's on-disk token store (~/.xurl). Stored credentials
read as OK with an explicit "not live-verified" caveat; an unreadable
token store is a typed ERROR (broken, not unconfigured).
"""
from . import xurl_x
requires = "xurl CLI installed + OAuth2 login"
if which("xurl") is None:
return BackendFinding(
name="xurl",
status=health.MISSING,
detail="xurl CLI not found on PATH",
prescription="npm install -g xurl && xurl auth oauth2 login",
requires=requires,
)
store_status, store_detail = xurl_x.stored_auth_status()
if store_status == xurl_x.AUTH_OK:
return BackendFinding(
name="xurl",
status=health.OK,
detail=(
"installed; stored OAuth2 credentials present; "
"auth not live-verified (no network)"
),
requires=requires,
)
if store_status == xurl_x.AUTH_ERROR:
return BackendFinding(
name="xurl",
status=health.ERROR,
detail=store_detail,
prescription="xurl auth oauth2 login",
requires=requires,
)
return BackendFinding(
name="xurl",
status=health.MISSING,
detail="xurl installed but not authenticated",
prescription="xurl auth oauth2 login",
requires=requires,
)
def _probe_ytdlp(config: Dict[str, Any]) -> BackendFinding:
"""yt-dlp via the U1 dependency-probe layer (missing/broken/timeout)."""
dep = health.probe_dependency("yt-dlp")
return BackendFinding(
name="yt-dlp",
status=dep.status,
detail=dep.detail,
prescription=dep.prescription,
requires="yt-dlp on the agent-subprocess PATH",
)
def _probe_web_keyless(config: Dict[str, Any]) -> BackendFinding:
"""The keyless web-search floor: works keyless, but degraded quality."""
requires = "no key; suppressed on native-search hosts"
if env.keyless_web_allowed(config):
return BackendFinding(
name="keyless",
status=health.DEGRADED,
detail="keyless search floor (no paid key; lower quality)",
requires=requires,
)
return BackendFinding(
name="keyless",
status=health.MISSING,
detail="keyless floor suppressed: host has native web search",
prescription="",
requires=requires,
)
def _probe_reddit_public(config: Dict[str, Any]) -> BackendFinding:
"""Public keyless Reddit composite; internal lanes are sub-probe detail."""
return BackendFinding(
name="public",
status=health.OK,
detail="public keyless composite (lanes: rss, listing, arctic, shreddit)",
requires="none (public endpoints)",
)
# ---------------------------------------------------------------------------
# Registry: routing declared once, from env.py's definitions where they exist.
# ---------------------------------------------------------------------------
_X_PROBES: Dict[str, Callable[[Dict[str, Any]], BackendFinding]] = {
"xai": _key_probe("xai", "XAI_API_KEY", "XAI_API_KEY (xAI/Grok live search)"),
"grok": _probe_grok,
"bird": _probe_bird,
"xurl": _probe_xurl,
"xquik": _key_probe("xquik", "XQUIK_API_KEY", "XQUIK_API_KEY (xquik.com)"),
}
_X_PAID = {"xai", "xquik"}
# Opt-in backends: never auto-selected; require explicit pin.
_X_OPT_IN = set(env.X_BACKEND_OPT_IN)
_WEB_PROBES: Dict[str, Callable[[Dict[str, Any]], BackendFinding]] = {
"brave": _key_probe("brave", "BRAVE_API_KEY", "BRAVE_API_KEY"),
"exa": _key_probe("exa", "EXA_API_KEY", "EXA_API_KEY"),
"serper": _key_probe("serper", "SERPER_API_KEY", "SERPER_API_KEY"),
"parallel": _key_probe("parallel", "PARALLEL_API_KEY", "PARALLEL_API_KEY"),
"keyless": _probe_web_keyless,
}
_WEB_KEYED = {"brave", "exa", "serper", "parallel"}
_SC_SPEC = BackendSpec(
name="scrapecreators",
requires="SCRAPECREATORS_API_KEY",
probe=_key_probe(
"scrapecreators", "SCRAPECREATORS_API_KEY", "SCRAPECREATORS_API_KEY",
note=_SC_PRESCRIPTION,
),
paid=True,
)
# X backend requirements, keyed by name.
_X_REQUIRES: Dict[str, str] = {
"xai": "XAI_API_KEY (xAI/Grok live search)",
"grok": "grok CLI installed + signed in (opt-in only; pin to enable)",
"bird": "X browser cookies (AUTH_TOKEN/CT0) + node",
"xurl": "xurl CLI installed + OAuth2 login",
"xquik": "XQUIK_API_KEY (xquik.com)",
}
DESCRIPTORS: Dict[str, ChainDescriptor] = {
# X: chain order and pin var imported from env.py (single source of truth).
# Backends include the auto chain (X_BACKEND_ORDER) plus opt-in entries
# (X_BACKEND_OPT_IN) for doctor visibility. Opt-in backends like grok
# appear in findings but are never auto-selected; pin to enable.
"x": ChainDescriptor(
source="x",
mode=MODE_ALTERNATIVE,
backends=tuple(
BackendSpec(
name=name,
requires=_X_REQUIRES[name],
probe=_X_PROBES[name],
paid=name in _X_PAID,
opt_in=name in _X_OPT_IN,
)
for name in env.X_BACKEND_ORDER + env.X_BACKEND_OPT_IN
),
pin_var=env.X_BACKEND_PIN_VAR,
),
"youtube": ChainDescriptor(
source="youtube",
mode=MODE_ALTERNATIVE,
backends=(
BackendSpec(
name="yt-dlp",
requires="yt-dlp on the agent-subprocess PATH",
probe=_probe_ytdlp,
),
_SC_SPEC,
),
pin_var=None, # no YouTube pin knob exists
),
"web": ChainDescriptor(
source="web",
mode=MODE_ALTERNATIVE,
backends=tuple(
BackendSpec(
name=name,
requires=(f"{name.upper()}_API_KEY" if name in _WEB_KEYED
else "no key; suppressed on native-search hosts"),
probe=_WEB_PROBES[name],
paid=name in _WEB_KEYED,
)
for name in WEB_BACKEND_ORDER
),
pin_var=None, # pinned per-run via --web-backend, not an env var
pin_flag="--web-backend",
),
"reddit": ChainDescriptor(
source="reddit",
mode=MODE_CONDITIONAL,
backends=(
BackendSpec(
name="public",
requires="none (public endpoints)",
probe=_probe_reddit_public,
),
_SC_SPEC,
),
pin_var=env.REDDIT_BACKEND_PIN_VAR,
),
}
def get_descriptor(source: str) -> ChainDescriptor:
"""Return the declared routing descriptor for ``source`` (KeyError if none)."""
return DESCRIPTORS[source]
# ---------------------------------------------------------------------------
# Resolution
# ---------------------------------------------------------------------------
def resolve(
source: str,
config: Dict[str, Any],
pin: Optional[str] = None,
) -> BackendResolution:
"""Resolve a chained source's routing into a truthful prediction.
``pin`` is an explicit per-run pin (the ``--web-backend`` flag); it
takes precedence over the descriptor's env pin var. ``"auto"``/None
mean unpinned. Probing is side-effect-free and collect-then-pick.
Time budget: backends are probed sequentially, so a chain's budget is
ADDITIVE across its backends — each binary-backed probe is bounded by
``health.PROBE_TIMEOUT`` and paid/key lanes are dict lookups that cost
nothing, giving a worst case of roughly (binary probes in the chain) x
``health.PROBE_TIMEOUT``. Deliberately no intra-chain concurrency:
probes are memoized per process and the worst case only occurs when
multiple binaries are simultaneously hung.
"""
descriptor = get_descriptor(source)
findings = [
_run_probe(spec, config) for spec in descriptor.backends
]
if descriptor.mode == MODE_CONDITIONAL:
return _resolve_conditional(descriptor, config, findings)
return _resolve_alternative(descriptor, config, findings, pin)
def _run_probe(spec: BackendSpec, config: Dict[str, Any]) -> BackendFinding:
"""Run one probe, isolating failures so one bad probe can't blank a chain."""
try:
finding = spec.probe(config)
except Exception as exc: # a probe bug must not take the report down
finding = BackendFinding(
name=spec.name,
status=health.ERROR,
detail=f"probe failed: {type(exc).__name__}: {exc}",
requires=spec.requires,
)
if not finding.requires:
finding.requires = spec.requires
return finding
def _resolve_alternative(
descriptor: ChainDescriptor,
config: Dict[str, Any],
findings: List[BackendFinding],
pin: Optional[str],
) -> BackendResolution:
names = [spec.name for spec in descriptor.backends]
by_name = {f.name: f for f in findings}
# Track which backends are opt-in (never auto-selected).
opt_in_names = {spec.name for spec in descriptor.backends if spec.opt_in}
res = BackendResolution(
source=descriptor.source,
mode=MODE_ALTERNATIVE,
chain=list(names),
findings=findings,
)
pin_name: Optional[str] = None
if pin and pin not in ("auto", "none") and pin in by_name:
pin_name = pin
elif descriptor.pin_var:
raw = (config.get(descriptor.pin_var) or "").lower()
if raw in by_name:
pin_name = raw
if pin_name:
# A pin forces a single backend (no failover) — mirror
# env.x_backend_chain's pin semantics exactly.
res.pinned = True
res.pin = pin_name
finding = by_name[pin_name]
if finding.status == health.OK:
res.active_backend = pin_name
res.tier = TIER_OK
elif finding.status == health.DEGRADED:
res.active_backend = pin_name
res.tier = TIER_WARN
else:
res.tier = TIER_ERROR
res.prescription = finding.prescription or (
f"unpin {descriptor.pin_var or descriptor.pin_flag} or fix {pin_name}"
)
return res
# Collect-then-pick: first fully-usable wins; else best degraded; else
# error carrying the highest-priority backend's prescription.
# Opt-in backends are NEVER auto-selected; skip them entirely.
auto_findings = [f for f in findings if f.name not in opt_in_names]
for finding in auto_findings:
if finding.status == health.OK:
res.active_backend = finding.name
res.tier = TIER_OK
return res
for finding in auto_findings:
if finding.status == health.DEGRADED:
res.active_backend = finding.name
res.tier = TIER_WARN
return res
res.tier = TIER_ERROR
# Prescription comes from the first auto-chain backend, not opt-in.
res.prescription = auto_findings[0].prescription if auto_findings else ""
return res
def _reddit_sc_min_items(config: Dict[str, Any]) -> int:
"""The thinness floor, parsed exactly as the pipeline parses it
(lib/pipeline.py reddit fetch: int(... or 0), malformed -> 0)."""
try:
return int(config.get(env.REDDIT_SC_MIN_ITEMS_VAR) or 0)
except (TypeError, ValueError):
return 0
def _resolve_conditional(
descriptor: ChainDescriptor,
config: Dict[str, Any],
findings: List[BackendFinding],
) -> BackendResolution:
"""Reddit: render the real per-query semantics, never a computed winner."""
res = BackendResolution(
source=descriptor.source,
mode=MODE_CONDITIONAL,
chain=[spec.name for spec in descriptor.backends],
findings=findings,
active_backend=None, # conditional mode never picks a winner
tier=TIER_OK, # the public keyless composite is always reachable
)
has_key = bool(config.get("SCRAPECREATORS_API_KEY"))
raw_pin = (config.get(descriptor.pin_var) or "").lower() if descriptor.pin_var else ""
pinned_sc = has_key and raw_pin == "scrapecreators"
floor = _reddit_sc_min_items(config)
if pinned_sc:
res.pinned = True
res.pin = "scrapecreators"
res.conditional = (
f"ScrapeCreators primary (pinned via {descriptor.pin_var}); "
"public keyless composite fallback"
)
return res
if has_key:
if floor > 0:
backfill = (
f"ScrapeCreators backfill when results fall below the "
f"{floor}-item floor"
)
else:
backfill = "ScrapeCreators backfill when the free path returns nothing"
res.conditional = f"public keyless composite (default); {backfill}"
return res
res.conditional = "public keyless composite (default); no ScrapeCreators key for backfill"
if raw_pin == "scrapecreators":
# The pipeline ignores the pin without a key; say so honestly.
res.conditional += (
f" ({descriptor.pin_var} pin ignored: SCRAPECREATORS_API_KEY not set)"
)
return res