""" app.py — Single Entry Point: FastAPI API + Gradio UI ===================================================== Mounts Gradio onto the FastAPI app at /gradio. One uvicorn process serves both the OpenAI-compatible API (/v1/...) and the full Gradio UI (/gradio). Usage: python app.py # Or via run.sh """ import os import sys # SR-61b: Mitigate OOM on RTX 2060 (12GB) os.environ["PYTORCH_CUDA_ALLOC_CONF"] = "expandable_segments:True,max_split_size_mb:256" # ── Disable PX debug/telemetry output for clean serving ── os.environ.setdefault("DEBUG_ROUTING", "0") os.environ.setdefault("DEBUG_PX", "0") os.environ.setdefault("SUBJECTIVE_TELEMETRY", "0") # Plan 7.1: Hard-Crash auf unbehandelte Exceptions — sys/threading/faulthandler- # Hooks installieren. ENV PX_HARD_CRASH=0 macht install() zum no-op. import crash_handler crash_handler.install() import gradio as gr from server import app as fastapi_app, manager from config import MODEL_REGISTRY, SERVER_CONFIG from benchmark_engine import BenchmarkEngine from gradio_tabs.chat_tab import build_chat_tab from gradio_tabs.cognitive_tests_tab import build_cognitive_tests_tab from gradio_tabs.pzombie_eval_tab import build_pzombie_eval_tab from gradio_tabs.telemetry_tab import build_telemetry_tab from gradio_tabs._styles import get_css # ── Create shared benchmark engine ── engine = BenchmarkEngine(manager) # ── Build Gradio Blocks ── # Plan ui-styling Task #181: CSS-Variablen aus _styles.py werden in # mount_gradio_app(css=...) injiziert (Gradio 6 API: css ist launch-time, # nicht Blocks-constructor — siehe UserWarning sonst). with gr.Blocks(title="PX Cognitive Architecture Explorer") as demo: # Resolve protocol for UI display protocol = "https" if SERVER_CONFIG.get("ssl_cert") and os.path.exists(os.path.join(os.path.dirname(os.path.abspath(__file__)), SERVER_CONFIG.get("ssl_cert"))) else "http" gr.Markdown(f""" # 🧠 PX Cognitive Architecture Explorer **Phenomenological eXtension** — Model-agnostic PX patch system with cognitive evaluation. Models: **PX-patched** (gemma3-270m-px, minicpm5-1b-px) | **Baseline** (gemma3-270m-base, gemma3-270m-it, minicpm5-1b-base) API: `{protocol}://localhost:{SERVER_CONFIG['port']}/v1/` (OpenAI-compatible) | UI: `/gradio` """) with gr.Tabs(): with gr.Tab("💬 Chat"): session_id_state, chatbot, session_dropdown, session_id_display = build_chat_tab(manager) with gr.Tab("🧪 Cognitive Tests"): build_cognitive_tests_tab(manager, engine) with gr.Tab("🔬 P-Zombie Evaluation"): build_pzombie_eval_tab(manager, engine) with gr.Tab("📊 Telemetry"): build_telemetry_tab(manager) # ── Initialization ── def init_app(session_id): from gradio_tabs.chat_tab import on_load return on_load(session_id) demo.load( fn=init_app, inputs=[session_id_state], outputs=[session_id_state, chatbot, session_dropdown, session_id_display] ) gr.Markdown(""" --- **PX Explorer v1.1** | [OpenAI API Documentation](/v1/docs) | [System Status](/) *Built with SciMind4 Rigor Protocol. Anti-Zombie Grade: B+* """) # HF-Space-Mode: KEIN mount_gradio_app! HF's gradio-SDK ruft die # `app`-Variable ggf. via uvicorn auf, was Port 7860 vorab belegt # → demo.launch(7860) crasht mit "Cannot find empty port". # Plan: in HF-Mode nur `demo` exportieren, Mount nur in local-mode. if os.environ.get("SPACES_RUN_MODE") or os.environ.get("SPACE_ID"): app = None # HF-SDK: nichts zu mounten else: # ── Mount Gradio onto FastAPI at /gradio ── # Plan ui-styling: CSS aus _styles.py an mount_gradio_app weiterreichen. app = gr.mount_gradio_app(fastapi_app, demo, path="/gradio", css=get_css()) if __name__ == "__main__": import uvicorn from config import SERVER_CONFIG # HF-Space-Detection: HF startet das gradio-App via gr.launch(). # Auf HF-Space haben wir: # - /v1/ API: NICHT verfügbar (kein uvicorn-Server) # - /gradio UI: verfügbar via demo.launch() # Das ist der Trade-off — HF's gradio-SDK kann nur ein einzelnes # gr.Blocks mounten, nicht ein FastAPI+Gradio-Composite. # Plan: portiere /v1/ API auf Gradio-API-Endpoints in einem # separaten Schritt (siehe TODO app.py:port-v1-to-gradio). if os.environ.get("SPACES_RUN_MODE") or os.environ.get("SPACE_ID"): print(f"[PX Explorer] HF-Space detected — launching gr.Blocks via demo.launch() (no /v1/ API on HF)") # Plan 2026-07-09: ssr_mode=False ist KRITISCH auf HF-Space. # Default (True) startet einen Node-SSR-Proxy auf 7860, der im # HF-Container scheitert (kein Node installiert). Gradio fällt # auf 7861 zurück, aber SvelteKit-SSR-Loader will trotzdem # /info JSON von 127.0.0.1:7861 fetchen — was der BROWSER nicht # kann (localhost → user-machine). Resultat: data:null im # Page-Render → "Could not get API info" → Login-Panel. # ssr_mode=False → client-side-render → /info ist relative URL # → HF-Proxy leitet korrekt zu Gradio-Server auf 7861 weiter. demo.launch( server_name="0.0.0.0", server_port=7860, show_error=True, ssr_mode=False, ) else: # SSL Configuration ssl_cert = SERVER_CONFIG.get("ssl_cert") ssl_key = SERVER_CONFIG.get("ssl_key") # Resolve relative paths if ssl_cert and not os.path.isabs(ssl_cert): ssl_cert = os.path.join(os.path.dirname(os.path.abspath(__file__)), ssl_cert) if ssl_key and not os.path.isabs(ssl_key): ssl_key = os.path.join(os.path.dirname(os.path.abspath(__file__)), ssl_key) use_ssl = ssl_cert and os.path.exists(ssl_cert) and ssl_key and os.path.exists(ssl_key) protocol = "https" if use_ssl else "http" print(f"[PX Explorer] Starting on {SERVER_CONFIG['host']}:{SERVER_CONFIG['port']} (SSL: {use_ssl})") print(f"[PX Explorer] API: {protocol}://localhost:{SERVER_CONFIG['port']}/v1/") print(f"[PX Explorer] UI: {protocol}://localhost:{SERVER_CONFIG['port']}/gradio") run_kwargs = { "app": app, "host": SERVER_CONFIG["host"], "port": SERVER_CONFIG["port"], "log_level": "info", } if use_ssl: run_kwargs["ssl_certfile"] = ssl_cert run_kwargs["ssl_keyfile"] = ssl_key # Plan 7.1: uvicorn.Server statt uvicorn.run() damit wir nach # Loop-Creation den asyncio-Hook attachen können (uvicorn.run() # versteckt den Loop hinter einer Fire-and-forget-Wrapper-Funktion). config = uvicorn.Config(**run_kwargs) server = uvicorn.Server(config) _orig_startup = server.startup async def _patched_startup(sockets=None): await _orig_startup(sockets=sockets) # uvicorn ≥0.30 speichert den Loop erst während serve() als # ``server.main_loop``. Hier läuft unser Patched-Startup im # selben Loop, also können wir den aktuellen direkt greifen. import asyncio as _asyncio crash_handler.install_asyncio(_asyncio.get_running_loop()) server.startup = _patched_startup server.run()