MedPlain / server.py
MedPlain
Together: low concurrency + patient retries to avoid 503s; friendlier busy-provider error
5edda8d
Raw
History Blame Contribute Delete
5.82 kB
"""
Flask server for the Clinical Simplifier Web UI.
Serves the single-page front-end and exposes a streaming `/api/simplify`
endpoint that runs the full V7 pipeline (in-process, via engine.py) for one
clinical note and pushes live stage-progress events back to the browser as
Server-Sent Events, finishing with the full structured result.
Run:
cd webapp
python server.py
# open http://127.0.0.1:5000
"""
from __future__ import annotations
import json
import os
import queue
import threading
import traceback
from pathlib import Path
from flask import Flask, Response, jsonify, request, send_from_directory
import engine
HERE = Path(__file__).resolve().parent
app = Flask(__name__, static_folder=str(HERE / "static"), static_url_path="/static")
# Only one pipeline run executes at a time (Engine holds a run-lock), but we
# guard the queue plumbing too so progress streams stay isolated per request.
_DEFAULT_ABLATIONS = int(os.environ.get("CC_NUM_ABLATIONS", "32"))
def _sse(stage: str, status: str, data=None) -> str:
return "data: " + json.dumps({"stage": stage, "status": status, "data": data}) + "\n\n"
@app.route("/")
def index():
return send_from_directory(str(HERE / "templates"), "index.html")
@app.route("/api/health")
def health():
return jsonify({
"status": "ok",
"loaded": engine.ENGINE._loaded,
"load_status": engine.ENGINE.load_status,
"default_ablations": _DEFAULT_ABLATIONS,
"simplifier_models": [
{"key": m["key"], "label": m["label"], "provider": m["provider"],
"tier": m["tier"], "default": m.get("default", False)}
for m in engine.SIMPLIFIER_MODELS
],
"novita_models": engine.NOVITA_MODELS_MENU,
"judge_panel": engine.JUDGE_PANEL,
})
@app.route("/api/dataset")
def dataset():
"""Return the grade-3 gold simplification triples for benchmark mode."""
fp = engine.DATA_DIR / "laymaker_grade3_plain.json"
try:
rows = json.loads(fp.read_text(encoding="utf-8"))
items = [{"id": r.get("ID"), "original": r.get("ORIGINAL"),
"reference": r.get("REFERENCE")} for r in rows
if r.get("ORIGINAL") and r.get("REFERENCE")]
return jsonify({"ok": True, "items": items})
except Exception as e:
return jsonify({"ok": False, "error": str(e), "items": []})
@app.route("/api/warmup", methods=["POST"])
def warmup():
"""Pre-load glossaries + LLM so the first real request is fast."""
def progress(stage, status, data):
pass
try:
engine.ENGINE.ensure_loaded(progress)
return jsonify({"ok": True, "load_status": engine.ENGINE.load_status})
except Exception as e:
traceback.print_exc()
return jsonify({"ok": False, "error": str(e)}), 500
@app.route("/api/simplify", methods=["POST"])
def simplify():
data = request.get_json(force=True, silent=True) or {}
note = (data.get("note") or "").strip()
reference = (data.get("reference") or "").strip()
fw_model = (data.get("fw_model") or "").strip() or None
novita_model = (data.get("novita_model") or "").strip() or None
run_judge = bool(data.get("judge"))
ablations = data.get("ablations")
try:
ablations = int(ablations) if ablations else None
except (TypeError, ValueError):
ablations = None
if not note:
return Response(_sse("error", "error", {"error": "Please enter a clinical note."}),
mimetype="text/event-stream")
if len(note) > 6000:
return Response(_sse("error", "error", {"error": "Note too long (max 6000 chars)."}),
mimetype="text/event-stream")
q: "queue.Queue" = queue.Queue()
holder: dict = {}
def progress(stage, status, d):
q.put({"stage": stage, "status": status, "data": d})
def worker():
try:
holder["result"] = engine.ENGINE.run(
note, progress=progress, ablations=ablations, fw_model=fw_model,
novita_model=novita_model, reference=reference, run_judge=run_judge)
except Exception as e: # noqa: BLE001
msg = str(e)
if "503" in msg or "service_unavailable" in msg.lower() or "service unavailable" in msg.lower():
holder["error"] = ("The selected provider is temporarily busy (503). "
"Please try again in a moment, lower the ContextCite "
"ablations in Settings, or switch to a Firework model "
"(more reliable than the Together ones).")
else:
holder["error"] = f"{type(e).__name__}: {e}"
traceback.print_exc()
finally:
q.put(None)
threading.Thread(target=worker, daemon=True).start()
def stream():
yield _sse("queued", "running", {"detail": "starting pipeline"})
while True:
item = q.get()
if item is None:
break
yield "data: " + json.dumps(item) + "\n\n"
if "result" in holder:
yield _sse("result", "done", holder["result"])
else:
yield _sse("error", "error", {"error": holder.get("error", "unknown error")})
return Response(stream(), mimetype="text/event-stream",
headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no",
"Connection": "keep-alive"})
if __name__ == "__main__":
port = int(os.environ.get("PORT", "5000"))
host = os.environ.get("HOST", "127.0.0.1")
print(f"\n MedPlain UI → http://{host}:{port}\n")
# threaded=True so the SSE stream + the worker thread run concurrently.
app.run(host=host, port=port, threaded=True, debug=False)