# app/ui_proxy.py from __future__ import annotations import json, os, time from typing import Tuple, Generator import gradio as gr from gradio_client import Client def _pretty(x) -> str: try: return json.dumps(x, indent=2, ensure_ascii=False) except Exception: return str(x) def _normalize_space_id(raw: str) -> str: """Accept 'owner/repo' or a full URL; return 'owner/repo' only.""" s = (raw or "").strip() for pref in ("https://huggingface.co/spaces/", "http://huggingface.co/spaces/"): if s.startswith(pref): s = s[len(pref):].strip("/ ") return s def _client() -> Client: space_id = _normalize_space_id(os.getenv("PROXY_BACKEND_SPACE")) if not space_id or "/" not in space_id: raise RuntimeError("Backend is not configured.") token = (os.getenv("HF_API_TOKEN") or os.getenv("HF_TOKEN") or "").strip() if not token: raise RuntimeError("Access to backend is not available.") return Client(space_id, hf_token=token) def _pick_api_name(cli: Client) -> str: manual = (os.getenv("PROXY_API_NAME") or "").strip() if manual: return manual if manual.startswith("/") else f"/{manual}" try: info = cli.view_api() if not info: return "/predict" eps = info.get("endpoints") or [] names = [e.get("name") for e in eps if e and e.get("name")] if "/predict" in names: return "/predict" if names: return names[0] except Exception: pass return "/predict" def handle_query_proxy(q: str) -> Generator[Tuple[str, str, str, str, str], None, None]: """Yields: routed_json, plan_json, answer_json, human_md, perf_json""" loading = "### ⏳ Fetching…\nContacting the analysis backend." try: # Minimal, generic status yield "{}", "{}", "{}", loading, _pretty({"status": "connecting"}) cli = _client() api_name = _pick_api_name(cli) yield "{}", "{}", "{}", loading, _pretty({"status": "running"}) job = cli.submit(q, api_name=api_name) # Tiny delay for nicer UX time.sleep(0.3) if not job.done(): yield "{}", "{}", "{}", loading, _pretty({"status": "running"}) routed_json, plan_json, answer_json, human_text, perf_json = job.result() yield routed_json, plan_json, answer_json, human_text, perf_json except Exception: friendly = ( "### ❌ Something went wrong\n" "Please try again in a moment." ) # No exception details, no env, no tokens. yield "{}", "{}", "{}", friendly, _pretty({"status": "error"}) def build_proxy() -> gr.Blocks: with gr.Blocks(title="Crypto Copilot (Public)") as demo: gr.Markdown("## Crypto Copilot — Public UI") with gr.Row(): query = gr.Textbox( label="Your question", placeholder="Examples: 'What is BTC price now?', 'Compare BTC vs ETH 30d drawdown'", lines=2, ) run_btn = gr.Button("Run", variant="primary") # Summary first (shows loading message immediately) with gr.Tab("Summary"): human_out = gr.Markdown() with gr.Tab("How it was decided"): routed_out = gr.Code(language="json", label="Router output (intent/assets/windows)") with gr.Tab("Plan"): plan_out = gr.Code(language="json", label="Planner output") with gr.Tab("Answer JSON"): answer_out = gr.Code(language="json", label="Structured answer (schema)") with gr.Tab("Perf & Cost"): perf_out = gr.Code(language="json", label="Latency, backend, cache stats") run_btn.click( fn=handle_query_proxy, inputs=[query], outputs=[routed_out, plan_out, answer_out, human_out, perf_out], show_progress=True, ) gr.Examples( examples=[ "What is BTC price now?", "Compare BTC vs ETH YTD and 30d drawdown", "Show 7d return and volatility for SOL", "Portfolio: BTC 60% ETH 40%", "What moved ETH today?", ], inputs=[query], outputs=[routed_out, plan_out, answer_out, human_out, perf_out], fn=handle_query_proxy, cache_examples=False, label="Try these example questions", run_on_click=True, ) return demo