borderless / app.py
spagestic's picture
Refactor Gradio node cleanup and enhance OAuth integration. Updated _cleanup_stale_gradio_nodes to handle multiple patterns for orphaned processes. Improved demo assignment in _launch_with_oauth for better hot reload compatibility with Gradio Spaces.
51b6df5
Raw
History Blame Contribute Delete
5.03 kB
# app.py
import os
import signal
import subprocess
import sys
import time
import warnings
from pathlib import Path
from typing import Any
import gradio as gr
from fastapi import Request
from fastapi.responses import HTMLResponse
from fastapi.staticfiles import StaticFiles
from gradio import Server
from gradio.blocks import Blocks
from gradio.components import LoginButton
from gradio.events import api as gr_api
warnings.filterwarnings(
"ignore",
message='Field name "json"',
category=UserWarning,
module=r"firecrawl\.v2\.types",
)
from ui import server_api
ASSETS_DIR = Path(__file__).resolve().parent / "assets"
app = Server(title="Borderless - Immigration Research Agent")
# Replaced with the OAuth Blocks instance in _launch_with_oauth (Gradio hot reload).
demo = app
def _versioned_homepage_html() -> str:
html = (ASSETS_DIR / "index.html").read_text(encoding="utf-8")
for asset in ("app.js", "globe.js", "server.css", "globe.css"):
version = int((ASSETS_DIR / asset).stat().st_mtime)
html = html.replace(f"/assets/{asset}", f"/assets/{asset}?v={version}")
return html
@app.get("/", response_class=HTMLResponse)
async def homepage() -> HTMLResponse:
return HTMLResponse(
content=_versioned_homepage_html(),
headers={"Cache-Control": "no-cache, must-revalidate"},
)
@app.get("/api/intake_choices")
def api_intake_choices() -> dict[str, Any]:
return server_api.get_intake_choices()
@app.get("/api/auth/status")
async def auth_status(request: Request) -> dict[str, Any]:
session = getattr(request, "session", None)
if session is None:
return {"logged_in": False}
oauth_info = session.get("oauth_info")
if not oauth_info:
return {"logged_in": False}
expires_at = oauth_info.get("expires_at")
if expires_at is not None and expires_at <= time.time():
session.pop("oauth_info", None)
return {"logged_in": False}
userinfo = oauth_info.get("userinfo") or {}
return {
"logged_in": True,
"username": userinfo.get("preferred_username"),
"name": userinfo.get("name"),
}
@app.api(name="get_intake_choices")
def api_get_intake_choices() -> dict:
return server_api.get_intake_choices()
@app.api(name="build_research_prompt")
def api_build_research_prompt(
current_country: server_api.DropdownValue,
residence_status: server_api.DropdownValue,
education: server_api.DropdownValue,
occupation: server_api.DropdownValue,
experience: server_api.DropdownValue,
budget: server_api.DropdownValue,
family: server_api.DropdownValue,
timeline: server_api.DropdownValue,
goals: str,
) -> dict[str, str]:
return server_api.build_research_prompt(
current_country,
residence_status,
education,
occupation,
experience,
budget,
family,
timeline,
goals,
)
@app.api(name="build_persona_prompt")
def api_build_persona_prompt(persona_id: str) -> str:
return server_api.build_persona_prompt(persona_id)
@app.api(name="chat")
def api_chat(
message: str,
history: list[dict],
globe_state: dict | None,
hf_token: gr.OAuthToken | None,
) -> dict[str, Any]:
"""Stream chat updates. Return type enables Gradio SSE output registration."""
yield from server_api.stream_chat(message, history, globe_state, hf_token)
app.mount("/assets", StaticFiles(directory=str(ASSETS_DIR)), name="assets")
def _cleanup_stale_gradio_nodes() -> None:
"""Terminate orphaned Gradio Node SSR workers from prior restarts."""
patterns = (
"gradio/templates/node/build",
"gradio/templates/node/build/server/entry.js",
)
for pattern in patterns:
try:
result = subprocess.run(
["pgrep", "-f", pattern],
capture_output=True,
text=True,
check=False,
)
except FileNotFoundError:
return
for pid_str in result.stdout.split():
if not pid_str.strip():
continue
pid = int(pid_str)
try:
os.kill(pid, signal.SIGTERM)
except ProcessLookupError:
pass
def _launch_with_oauth(**kwargs: Any):
"""Launch with a hidden LoginButton so HF OAuth routes are registered."""
with Blocks() as blocks:
LoginButton(visible=False)
for fn, api_kwargs in app._deferred_apis:
gr_api(fn=fn, **api_kwargs)
os.environ["GRADIO_SERVER_MODE_ENABLED"] = "1"
# Gradio Spaces hot reload matches __main__.demo to the launched Blocks.
main = sys.modules.get("__main__")
if main is not None:
main.demo = blocks
globals()["demo"] = blocks
return blocks.launch(_app=app, **kwargs)
app.launch = _launch_with_oauth # type: ignore[method-assign]
if __name__ == "__main__":
_cleanup_stale_gradio_nodes()
app.launch(show_error=True)