Download app.py from s-ttp/crucible: direct link, hf CLI and curl.
- Browser
- Download file 10.3 kB
-
https://huggingface.co/spaces/s-ttp/crucible/resolve/main/app.py
- Command line
-
hf download hf://spaces/s-ttp/crucible/app.py
-
curl -L -o app.py https://huggingface.co/spaces/s-ttp/crucible/resolve/main/app.py
10.3 kB
| """Crucible — AI Equity Analyst Space. | |
| A ticker in → a live 4-role LLM debate (Neutral · Bull · Bear · Portfolio Manager) over a | |
| quant ML forecast → a Buy/Hold/Sell verdict. The heavy engine runs as a subprocess and | |
| streams to the browser over SSE. | |
| """ | |
| import json | |
| import os | |
| import threading | |
| from datetime import datetime, timezone | |
| from fastapi import FastAPI, Header, HTTPException, Request | |
| from fastapi.responses import FileResponse, StreamingResponse | |
| from fastapi.staticfiles import StaticFiles | |
| import config | |
| import analyst | |
| app = FastAPI(title="Crucible") | |
| # ── Abuse guard: global per-UTC-day run cap ───────────────────────────────── | |
| # The Space is public, so anyone can trigger runs — and every run spends real | |
| # LLM + Alpha Vantage budget. Cap the number of analyses started per day (UTC). | |
| # Tune via the CRUCIBLE_DAILY_CAP env/secret; a run reads/writes a small counter | |
| # file so the limit survives within the container's lifetime. | |
| DAILY_CAP = int(os.getenv("CRUCIBLE_DAILY_CAP", "30")) | |
| _CAP_FILE = os.getenv("CRUCIBLE_CAP_FILE", "/tmp/crucible_runcount.json") | |
| _cap_lock = threading.Lock() | |
| def _reserve_run() -> tuple[bool, int, int]: | |
| """Atomically count one run against today's cap. Returns (allowed, used, cap).""" | |
| today = datetime.now(timezone.utc).strftime("%Y-%m-%d") | |
| with _cap_lock: | |
| data = {"date": today, "count": 0} | |
| try: | |
| with open(_CAP_FILE) as f: | |
| prev = json.load(f) | |
| if prev.get("date") == today: | |
| data = prev | |
| except Exception: | |
| pass | |
| if data["count"] >= DAILY_CAP: | |
| return False, data["count"], DAILY_CAP | |
| data["count"] += 1 | |
| try: | |
| with open(_CAP_FILE, "w") as f: | |
| json.dump(data, f) | |
| except Exception: | |
| pass | |
| return True, data["count"], DAILY_CAP | |
| def health(): | |
| import os | |
| return {"app": "Crucible", | |
| "engine": (config.RECOMMENDER_DIR / "predict_yearly.py").exists(), | |
| "llm": {k: bool(os.getenv(k)) for k in | |
| ("GEMINI_API_KEY", "OPENAI_API_KEY", "KIMI_API_KEY", "ANTHROPIC_API_KEY")}, | |
| "data": bool(os.getenv("ALPHAVANTAGE_API_KEY") or os.getenv("ALPHA_VANTAGE_API_KEY")), | |
| "panel_ml": (config.RECOMMENDER_DIR / "panel_ml_snapshot.parquet").exists()} | |
| def analyst_start(ticker: str, request: Request, x_crucible_client: str = Header(default=""), | |
| x_prewarm_token: str = Header(default="")): | |
| # Only the Crucible web page (or the token-holding prewarm job) may start a run. The | |
| # Space is public and every run spends LLM budget; an anonymous script posted an AAPL | |
| # analysis on 2026-09-06 with no page load and never read the result. The header is | |
| # trivial to copy, but it stops crawlers and API guessers. Who started a run is recorded | |
| # ONLY in the private server-side access log (analyst.log_access) — never in the report, | |
| # the API response, the saved report, the markdown or the report dataset. | |
| _tok = os.getenv("PREWARM_TOKEN", "") | |
| if x_crucible_client != "ui" and not (_tok and x_prewarm_token == _tok): | |
| raise HTTPException(status_code=403, detail="analyses can only be started from the Crucible web page") | |
| _via = "ui" if x_crucible_client == "ui" else "token" | |
| # Resolve FIRST (ticker or company name -> verified symbol) so a typo neither burns a | |
| # daily-cap slot nor launches an engine run that dies with a raw "failed (exit 1)". | |
| try: | |
| res = analyst.resolve_symbol(ticker) | |
| except ValueError as exc: | |
| raise HTTPException(status_code=400, detail=str(exc)) | |
| analyst._log_request(res["symbol"]) | |
| allowed, _used, cap = _reserve_run() | |
| if not allowed: | |
| raise HTTPException( | |
| status_code=429, | |
| detail=f"Daily analysis limit reached ({cap}/day). This resets at 00:00 UTC — please try again then.") | |
| try: | |
| out = {"job_id": analyst.start(res["symbol"]), "ticker": res["symbol"]} | |
| _j = analyst._jobs.get(out["job_id"]) | |
| if _j is not None and not _j.get("source"): | |
| _j["source"] = _via | |
| analyst.log_access(out["job_id"], res["symbol"], _via, request) | |
| if res.get("resolved_from"): | |
| out["resolved"] = {"from": res["resolved_from"], "name": res.get("name")} | |
| return out | |
| except ValueError as exc: | |
| raise HTTPException(status_code=400, detail=str(exc)) | |
| def track_record(min_age_days: int = 30): | |
| """Realized accuracy of past calls (direction hit-rate, mean abs error) once they age.""" | |
| return analyst.track_record(min_age_days=min_age_days) | |
| def recent(): | |
| """Cached analyses from the last 7 days — powers the Recent chips on the landing page.""" | |
| return {"recent": analyst.recent_reports()} | |
| def cached_report_md(ticker: str): | |
| """The stored audited markdown: the same fragments as the web page, byte for byte.""" | |
| from fastapi.responses import PlainTextResponse | |
| md = analyst.cached_markdown(ticker) | |
| if not md: | |
| raise HTTPException(status_code=404, detail="no published analysis for this ticker") | |
| return PlainTextResponse(md, media_type="text/markdown; charset=utf-8", | |
| headers={"Content-Disposition": f'attachment; filename="Crucible_{ticker.upper()}.md"'}) | |
| def cached_report(ticker: str): | |
| """Serve a cached full analysis instantly (0 LLM calls, no engine lock). 404 when absent | |
| or older than 7 days; includes a staleness flag when the price has drifted >7% since.""" | |
| d = analyst.cached_report(ticker) | |
| if not d: | |
| raise HTTPException(status_code=404, detail="no published analysis for this ticker") | |
| return d | |
| def dev_qa(ticker: str, x_dev_token: str = Header(default="")): | |
| """Internal QA record (validation results, rejected sentences, state snapshot). Developer-only: | |
| requires the DEV_TOKEN secret; without it configured the route does not exist.""" | |
| tok = os.getenv("DEV_TOKEN", "") | |
| if not tok: | |
| raise HTTPException(status_code=404, detail="not found") | |
| import hmac | |
| if not hmac.compare_digest(x_dev_token, tok): | |
| raise HTTPException(status_code=403, detail="forbidden") | |
| rec = analyst.qa_record(ticker) | |
| if rec is None: | |
| raise HTTPException(status_code=404, detail="no QA record for this ticker") | |
| return rec | |
| def prewarm(x_prewarm_token: str = Header(default="")): | |
| """Weekly, gated cache-warmer: only fires when organic searches haven't already filled | |
| the Recent row (>=3 fresh entries -> no-op). Token-gated so public callers can't spend | |
| LLM budget. Picks news-active large-caps not already cached, max 3.""" | |
| import os as _o | |
| tok = _o.getenv("PREWARM_TOKEN", "") | |
| if not tok or x_prewarm_token != tok: | |
| raise HTTPException(status_code=403, detail="bad or missing prewarm token") | |
| fresh = analyst.recent_reports(limit=50) | |
| if len(fresh) >= 3: | |
| return {"started": [], "reason": f"organic cache sufficient ({len(fresh)} fresh entries)"} | |
| need = 3 - len(fresh) | |
| have = {r["ticker"] for r in fresh} | |
| started = [] | |
| try: | |
| import urllib.request as _u, json as _j | |
| av = _o.getenv("ALPHAVANTAGE_API_KEY") or _o.getenv("ALPHA_VANTAGE_API_KEY", "") | |
| # What's trending IN THE NEWS: aggregate relevance-weighted ticker mentions across | |
| # the latest news feed (not "most traded", which skews to meme/penny volume). | |
| with _u.urlopen("https://www.alphavantage.co/query?function=NEWS_SENTIMENT" | |
| f"&sort=LATEST&limit=200&apikey={av}", timeout=25) as r: | |
| feed = _j.loads(r.read()).get("feed") or [] | |
| score: dict = {} | |
| for a in feed: | |
| for ts in a.get("ticker_sentiment") or []: | |
| sym = (ts.get("ticker") or "").upper() | |
| if ":" in sym or not sym.isalpha() or len(sym) > 5: # skip CRYPTO:/FOREX:/funds | |
| continue | |
| score[sym] = score.get(sym, 0.0) + float(ts.get("relevance_score") or 0) | |
| for sym, _sc in sorted(score.items(), key=lambda x: -x[1]): | |
| if len(started) >= need: | |
| break | |
| if sym in have: | |
| continue | |
| try: | |
| res = analyst.resolve_symbol(sym) # verifies a real quote exists | |
| jid = analyst.start(res["symbol"]) | |
| analyst._jobs[jid]["source"] = "prewarmed" | |
| started.append(res["symbol"]); have.add(res["symbol"]) | |
| except ValueError: | |
| continue | |
| except Exception as exc: | |
| return {"started": started, "error": str(exc)[:120]} | |
| return {"started": started, "basis": "news-trending (relevance-weighted mentions, latest feed)"} | |
| def jobs(): | |
| """Who holds the engine, who is queued, recent jobs — for diagnosing 'stuck' runs.""" | |
| return analyst.jobs_view() | |
| def usage(): | |
| """Today's run count vs the daily cap (no side effects).""" | |
| today = datetime.now(timezone.utc).strftime("%Y-%m-%d") | |
| used = 0 | |
| try: | |
| with open(_CAP_FILE) as f: | |
| d = json.load(f) | |
| if d.get("date") == today: | |
| used = d.get("count", 0) | |
| except Exception: | |
| pass | |
| return {"date": today, "used": used, "cap": DAILY_CAP} | |
| def analyst_stream(job_id: str): | |
| if not analyst.exists(job_id): | |
| raise HTTPException(status_code=404, detail="unknown job") | |
| return StreamingResponse(analyst.stream(job_id), media_type="text/event-stream") | |
| def analyst_result(job_id: str): | |
| if not analyst.exists(job_id): | |
| raise HTTPException(status_code=404, detail="unknown job") | |
| return analyst.result(job_id) | |
| def index(): | |
| return FileResponse(config.APP_DIR / "static" / "index.html", headers={"Cache-Control": "no-cache, must-revalidate"}) | |
| app.mount("/static", StaticFiles(directory=config.APP_DIR / "static"), name="static") | |