곡κ°λ©΄ νμ κΈ° μ κ±° β A/B λ³λ ¬ λΉκ΅λ μ μ§, /api/judge μ°¨λ¨(ν€ λ³΄νΈ)
859afbb verified Download server.py from FINAL-Bench/pharma-evidence-search: direct link, hf CLI and curl.
- Browser
- Download file 27.6 kB
-
https://huggingface.co/spaces/FINAL-Bench/pharma-evidence-search/resolve/main/server.py
- Command line
-
hf download hf://spaces/FINAL-Bench/pharma-evidence-search/server.py
-
curl -L -o server.py https://huggingface.co/spaces/FINAL-Bench/pharma-evidence-search/resolve/main/server.py
27.6 kB
| """ | |
| μ μ½ R&D κ·Όκ±° κ²μ β μΉ μλΉμ€ | |
| κ²μ¦λ κ²λ§ μλΉμ€νλ€: | |
| Β· 보λ₯ κ²μ΄νΈ PubMedQA μ λ¬Έκ° λΌλ²¨ 300λ¬Έν μ¬μ λ±λ‘ νμ¦ | |
| AUROC 0.742 Β· conf>=0.90 μμ νμ λ₯ 54.0% Β· μ νλ 87.7% (κΈ°μ 75.0%) | |
| Β· μΈμ© λ¬΄κ²°μ± νμ λͺ©λ‘ λ° PMID = νκ°. μ€λ 4ν μΈ‘μ μ λΆ 0건 | |
| Β· μ¨νλ λ―Έμ€ μΈλΆ API μμ‘΄ 0. μ GPU3 μλ¨Έλ§ λͺ¨λΈλ‘λ§ λλ€ | |
| π΄ μλΉμ€νμ§ μλ κ² (μΈ‘μ μμ λ―Έλ¬νλ€) | |
| Β· μ½λ¬Ό νΈμ΅-ν΄μ **νμ μΈ΅** β λ€μ― λ² μλν΄ λ€μ― λ² λ€ μ λ ₯μ μλμΌλ‘ λͺ» μ±μ λ€ | |
| Β· κ·Όκ±°μ¬μ¬ **λ¬Έν κ²μ¦** β 격차 β25%p λ‘ λ―Έλ¬ | |
| λ κΈ°λ₯μ μ½λκ° μμΌλ **μΌμ§ μλλ€.** λ―Έλ¬ν κ²μ μ νμ λ£μ§ μλλ€. | |
| """ | |
| from __future__ import annotations | |
| import os, sys, time, traceback | |
| HERE = os.path.dirname(os.path.abspath(__file__)) | |
| sys.path.insert(0, HERE) | |
| from fastapi import FastAPI | |
| from fastapi.responses import HTMLResponse, JSONResponse | |
| from pydantic import BaseModel | |
| import json, socket, urllib.error, urllib.request | |
| from fastapi import Request | |
| import re as A_re | |
| from fastapi.responses import StreamingResponse | |
| # π΄ 릴λ μ΄ μ μ© λ°°ν¬(HF Space)μλ νμ΄νλΌμΈ μ½λκ° **μλ€** β μλ μ² import νλ©΄ | |
| # 컨ν μ΄λκ° κΈ°λμ‘°μ°¨ λͺ» νλ€. μλ₯κ° μ§μ λΌ μμΌλ©΄ λλ μμ΄ λ¬λ€. | |
| _RELAY_ONLY = bool(os.environ.get("PHARMA_UPSTREAM_ASK", "").strip()) | |
| if _RELAY_ONLY: | |
| # λλ μλ λ°°ν¬ β μμλ§ λμμΌλ‘ λλ€(κ°μ νμμ© Β· μ€μ νμ μ μλ₯κ° νλ€) | |
| THRESHOLDS = {"balanced": 0.348, "strict": 0.680, "broad": 0.124} | |
| if not _RELAY_ONLY: | |
| import trials as A_CT | |
| from ask import (ask, THRESHOLDS, search_multi as A_search_multi, PROMPT as A_PROMPT, | |
| stream_llm as A_stream_llm, logprob_confidence as A_logprob_confidence, | |
| USE_TRIALS as A_USE_TRIALS) | |
| # π΄ κ³΅κ° λ ΈμΆ μ κ²½λ‘ λͺ©λ‘μ κ΄κ³ νμ§ μλλ€. νμνλ©΄ PHARMA_DOCS=1 λ‘ μΌ λ€. | |
| app = FastAPI(title="μ μ½ R&D κ·Όκ±° κ²μ", | |
| docs_url=("/api/docs" if os.environ.get("PHARMA_DOCS") == "1" else None), | |
| redoc_url=None, openapi_url=(None if os.environ.get("PHARMA_DOCS") != "1" else "/openapi.json")) | |
| class Q(BaseModel): | |
| question: str | |
| mode: str = "balanced" | |
| web: bool = False # π΄ μΉ νμ₯ opt-in β μ§λ¬Έμ΄ μΈλΆ κ²μμ¬λ‘ λκ°λ€ # balanced 0.90 Β· strict 0.95 Β· broad 0.85 | |
| n: int = 8 | |
| # ββ μ κ·Ό ν΅μ ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # π΄ 2026-08-31 μ€μΈ‘: μ λ°(λ³Έ PC)μμ `POST /api/ask` κ° **μΈμ¦ μμ΄ 200** μ΄μλ€. | |
| # ν λ²μ νΈμΆμ΄ μ°λ¦¬ GPU μμ LLM μ 3ν λλ¦°λ€(μ¬μμ±Β·λ΅λ³Β·κ²μ¦). | |
| # λΆλΉ μ νμ `/api/llm` μλ§ κ±Έλ € μμκ³ `/api/ask` λ **무μ ν**μ΄μλ€ β | |
| # λ°λ³΅ νΈμΆνλ©΄ GPU3 λ₯Ό μ μ ν΄ **νν μμ κΉμ§ λ°μ΄λΈλ€.** | |
| # | |
| # λ μΈ΅μΌλ‘ λ§λλ€: | |
| # β μ κ·Ό ν ν° `PHARMA_ACCESS_TOKEN` β μ€μ λΌ μμΌλ©΄ ν€λλ μΏΌλ¦¬λ‘ μꡬνλ€. | |
| # λ―Έμ€μ μ΄λ©΄ μ΄μ΄ λλ(μ¬λ΄ μμ°) μλ β‘λ νμ 건λ€. | |
| # β‘ IP λΉ λΆλΉ νΈμΆ μ ν β ν ν°μ΄ μμ΄λ 건λ€. 곡μ GPU μ΄κΈ° λλ¬Έμ΄λ€. | |
| # μ€μ νλ©΄ μ΄ μλ²λ **μ€κ³κΈ°**κ° λλ€(HF Space μ©). λΉμ°λ©΄ μ€μ€λ‘ μ²λ¦¬νλ€(μ μ©). | |
| UPSTREAM_ASK = os.environ.get("PHARMA_UPSTREAM_ASK", "").strip() | |
| ACCESS_TOKEN = os.environ.get("PHARMA_ACCESS_TOKEN", "").strip() | |
| ASK_PER_MIN = int(os.environ.get("PHARMA_ASK_PER_MIN", "6")) | |
| _ask_hits: dict[str, list[float]] = {} | |
| def _client_ip(request: Request) -> str: | |
| fwd = request.headers.get("x-forwarded-for", "") | |
| if fwd: | |
| return fwd.split(",")[0].strip() | |
| return request.client.host if request.client else "?" | |
| def check_access(request: Request): | |
| """ν΅κ³Όνλ©΄ None, λ§μΌλ©΄ JSONResponse λ₯Ό λλ €μ€λ€.""" | |
| if ACCESS_TOKEN: | |
| tok = (request.headers.get("x-pharma-access", "") | |
| or request.query_params.get("k", "")).strip() | |
| if tok != ACCESS_TOKEN: | |
| return JSONResponse({"error": "μ κ·Ό ν ν°μ΄ νμν©λλ€"}, status_code=401) | |
| ip = _client_ip(request) | |
| now = time.time() | |
| hits = [t for t in _ask_hits.get(ip, []) if now - t < 60] | |
| if len(hits) >= ASK_PER_MIN: | |
| _ask_hits[ip] = hits | |
| return JSONResponse( | |
| {"error": f"λΆλΉ {ASK_PER_MIN}ν μ νμ λλ€. μ μ ν λ€μ μλνμμμ€.", | |
| "retry_after_sec": int(60 - (now - hits[0]))}, status_code=429) | |
| hits.append(now) | |
| _ask_hits[ip] = hits | |
| if len(_ask_hits) > 4096: # λ©λͺ¨λ¦¬ 무ν μ¦κ° λ°©μ§ | |
| for k in [k for k, v in _ask_hits.items() if not v or now - v[-1] > 300]: | |
| _ask_hits.pop(k, None) | |
| return None | |
| class RQ(BaseModel): | |
| question: str | |
| max_tokens: int = 900 | |
| def api_rival(q: RQ, request: Request): | |
| """κ°μ μ§λ¬Έμ λ²μ© λͺ¨λΈ(3μ¬ μ€ λ¬΄μμ)μ β λΉκ΅ λ°λͺ¨. π΄ μ§λ¬Έμ΄ μΈλΆλ‘ λκ°λ€(νλ©΄ κ³ μ§).""" | |
| blocked = check_access(request) | |
| if blocked is not None: | |
| return blocked | |
| if UPSTREAM_ASK: # 릴λ μ΄: μλ₯(μ )κ° λμ λΆλ₯Έλ€ β ν€λ μ μλ§ μλ€ | |
| try: | |
| body = json.dumps(dict(question=q.question, max_tokens=q.max_tokens)).encode() | |
| hdrs = {"Content-Type": "application/json", | |
| "x-client-ip": _client_ip(request)} | |
| ut = os.environ.get("PHARMA_UPSTREAM_TOKEN", "").strip() | |
| if ut: | |
| hdrs["x-pharma-access"] = ut | |
| rq2 = urllib.request.Request(UPSTREAM_ASK.replace("/api/ask", "/api/rival"), | |
| data=body, headers=hdrs) | |
| with urllib.request.urlopen(rq2, timeout=180) as f: | |
| return JSONResponse(json.load(f)) | |
| except Exception as e: | |
| return JSONResponse({"error": str(e)[:100]}, status_code=502) | |
| try: | |
| import rival | |
| # π΄ νλ 4,000 β μ°λ¦¬ λ΅κ³Ό κ°μ μν(곡μ 쑰건). 900 μ λΌμ΄λ² λ΅μ΄ μ€κ°μ μλ € | |
| # "μΌλΆλ¬ μ΄λΌνκ² λ³΄μλ€"λ μλΉλ₯Ό λΆλ₯Έλ€. | |
| r = rival.answer(q.question, max_tokens=max(100, min(q.max_tokens, 4096))) | |
| return JSONResponse(r or {"error": "λΌμ΄λ² μλ΅ μμ"}) | |
| except Exception as e: | |
| return JSONResponse({"error": str(e)[:100]}, status_code=500) | |
| class JQ(BaseModel): | |
| question: str | |
| ours: str | |
| rival_text: str | |
| def api_judge(q: JQ, request: Request): | |
| """μ 3μ μ¬ν β π΄ 곡κ°λ©΄μμλ **μ°¨λ¨**νλ€(νμ₯λ μΉμΈ 2026-09-05). | |
| μ΄μ : β μ²μ 보λ μ§λ¬Έμμ μΉλ₯ μ΄ ~50% λΌ μ°λ¦¬ μλΉμ€ νμ΄μ§μ μ°λ¦¬κ° μ§λ νλ©΄μ΄ λ¬λ€ | |
| β‘κ°μ λ΅μλ νμ μ΄ νμ°¨λ§λ€ κ°λ¦°λ€(μΈ‘μ λΆμμ ) | |
| β’μ΅λͺ λ°©λ¬Έμκ° μ°λ¦¬ OpenAI ν€λ₯Ό μλͺ¨νλ€(κ³Όκ±° OpenRouter ν¬λ λ§ μμ§ μ λ‘). | |
| λ΄λΆ νκ°(eval_arena)λ μ μμ μ§μ νΈμΆνλ―λ‘ μν₯ μλ€ β μ μΌν μΈλΆ μ νμ± μ νΈλ‘ μ μ§. | |
| """ | |
| return JSONResponse({"error": "곡κ°λ©΄μμλ νμ κΈ°λ₯μ μ 곡νμ§ μμ΅λλ€"}, status_code=404) | |
| blocked = check_access(request) | |
| if blocked is not None: | |
| return blocked | |
| if UPSTREAM_ASK: | |
| try: | |
| body = json.dumps(dict(question=q.question, ours=q.ours, | |
| rival_text=q.rival_text)).encode() | |
| hdrs = {"Content-Type": "application/json", | |
| "x-client-ip": _client_ip(request)} | |
| ut = os.environ.get("PHARMA_UPSTREAM_TOKEN", "").strip() | |
| if ut: | |
| hdrs["x-pharma-access"] = ut | |
| rq2 = urllib.request.Request(UPSTREAM_ASK.replace("/api/ask", "/api/judge"), | |
| data=body, headers=hdrs) | |
| with urllib.request.urlopen(rq2, timeout=240) as f: | |
| return JSONResponse(json.load(f)) | |
| except Exception as e: | |
| return JSONResponse({"error": str(e)[:100]}, status_code=502) | |
| try: | |
| import rival | |
| from ask import search as _search | |
| # μ¬νμ© κ·Όκ±° β μ€λ¦½ μΆμ²(Europe PMC)μμ μλ‘ νμν΄ λμ‘° κΈ°μ€μΌλ‘ μ€λ€ | |
| ev = "" | |
| try: | |
| # π΄ ꡬ쑰 κ²°ν¨ μ리: μ¬νμ λμ‘° μλ£κ° "μ체 κ²μ 6νΈ"λΏμ΄λΌ, λ΅μ΄ μΈμ©ν PMID κ° | |
| # κ±°κΈ° μμΌλ©΄ μ€μ‘΄ λ Όλ¬Έλ "νμΈ λΆκ°"λ‘ κ°μ λλ€(μ¬μ λ¬Έμ΄ μ€ν : "μ°Έκ³ κ·Όκ±°κ° μμ΄"). | |
| # βΉ **λ λ΅μ΄ μΈμ©ν PMID μ μ΄λ‘μ μ§μ μ‘°νν΄** λμ‘° μλ£μ ν©μΉλ€ β | |
| # μΈμ© κ²μ¦μ΄ μΆμΈ‘μ΄ μλλΌ λμ‘°κ° λλ€. | |
| import re as _re2 | |
| cited_ids = list(dict.fromkeys(_re2.findall(r"PMID:(\d{6,9})", | |
| q.ours + " " + q.rival_text)))[:8] | |
| ps = [] | |
| for pid in cited_ids: | |
| try: | |
| ps += _search(f"EXT_ID:{pid} AND SRC:MED", n=1) | |
| except Exception: | |
| pass | |
| have = {str(p["pmid"]) for p in ps} | |
| ps += [x for x in _search(q.question, n=6) if str(x["pmid"]) not in have] | |
| # π΄ μ΄λ‘ 900μ μ λ¨μ΄ HRΒ·CI μμΉλ₯Ό μλΌ μ¬νμ΄ "κ·Όκ±°μ μλ μμΉ"λ‘ κ°μ νλ€ β 1600μλ‘ | |
| ev = (chr(10) * 2).join("[PMID:%s] %s%s%s" % (p["pmid"], p["title"], chr(10), | |
| p["abstract"][:1600]) for p in ps[:12]) | |
| # π΄ A κ° κΈ°λλ [FDA:μ½λ¬Όλͺ ] λΌλ²¨ μλ¬Έμ μ¬νμ΄ λͺ» λ°μ FDA μΈμ© μ λΆκ° "νμΈ λΆκ°"μλ€ | |
| fda_names = list(dict.fromkeys(_re2.findall(r"\[FDA:([A-Za-z][A-Za-z0-9\- ]{2,30})\]", | |
| q.ours + " " + q.rival_text)))[:2] | |
| for nm in fda_names: | |
| try: | |
| import fda as _fda | |
| fb = _fda.block(nm.strip()) | |
| if fb: | |
| ev = fb + (chr(10) * 2) + ev | |
| except Exception: | |
| pass | |
| except Exception: | |
| pass | |
| r = rival.judge(q.question, q.ours, q.rival_text, evidence=ev) | |
| # π΄ μΉν¨ μ μ κΈ°λ‘ β μ΄κΈΈ λλ§ μΈκ³ μ§ λ 침묡νλ©΄ κ·Έ μΉλ₯ μ μ¬κΈ°λ€. | |
| try: | |
| if r: | |
| with open(os.path.join(os.path.dirname(os.path.abspath(__file__)), | |
| "judge_ledger.jsonl"), "a", encoding="utf-8") as fp: | |
| fp.write(json.dumps(dict(t=time.time(), q=q.question[:200], | |
| winner=r.get("winner"), | |
| reason=(r.get("reason") or "")[:200]), | |
| ensure_ascii=False) + chr(10)) | |
| except Exception: | |
| pass | |
| return JSONResponse(r or {"error": "μ¬ν μλ΅ μμ"}) | |
| except Exception as e: | |
| return JSONResponse({"error": str(e)[:100]}, status_code=500) | |
| def api_ask(q: Q, request: Request): | |
| # π΄ 2026-08-31: HF Space λ μν¬λ¦Ώ μμ΄λ λμν΄μΌ νλ€. | |
| # μμ Space λ₯Ό `/api/llm`(ν ν° νμ)μ λΆμλλ **ν λ²λ λμνμ§ λͺ»νλ€** β | |
| # μ¬λμ΄ μν¬λ¦Ώμ λ£μ΄μ€ λκΉμ§ 401 μ΄μλ€. μμ±λμ§ μμ κ²μ λ°°ν¬λ‘ λ³΄κ³ ν μ μ΄λ€. | |
| # βΉ Space λ μλ₯μ `/api/ask` λ₯Ό **κ·Έλλ‘ μ€κ³**νλ€. ν ν°μ΄ νμ μκ³ | |
| # μλ₯μ λΆλΉ μ νμ΄ κ·Έλλ‘ μ μ©λλ€(μ€νλ € λ¨μ© λ°©μ§μ λ«λ€). | |
| if UPSTREAM_ASK: | |
| try: | |
| body = json.dumps(dict(question=q.question, mode=q.mode, n=q.n, | |
| web=bool(getattr(q, "web", False)))).encode() | |
| hdrs = {"Content-Type": "application/json", | |
| "x-client-ip": _client_ip(request)} # κ³΅κ° Space: μ¬μ©μλ³ νλλ₯Ό μν΄ μ€IP μ λ¬ | |
| ut = os.environ.get("PHARMA_UPSTREAM_TOKEN", "").strip() | |
| if ut: | |
| hdrs["x-pharma-access"] = ut | |
| rq = urllib.request.Request(UPSTREAM_ASK, data=body, headers=hdrs) | |
| with urllib.request.urlopen(rq, timeout=600) as f: | |
| return JSONResponse(json.load(f)) | |
| except urllib.error.HTTPError as e: | |
| try: | |
| return JSONResponse(json.load(e), status_code=e.code) | |
| except Exception: | |
| return JSONResponse({"error": f"μλ₯ μλ΅ μ€λ₯ {e.code}"}, status_code=e.code) | |
| except Exception as e: | |
| return JSONResponse({"error": f"μλ₯μ λΏμ§ λͺ»νμ΅λλ€: {str(e)[:160]}"}, | |
| status_code=502) | |
| blocked = check_access(request) | |
| if blocked is not None: | |
| return blocked | |
| if not q.question.strip(): | |
| return JSONResponse({"error": "μ§λ¬Έμ΄ λΉμμ΅λλ€"}, status_code=400) | |
| if q.mode not in THRESHOLDS: | |
| return JSONResponse({"error": f"mode λ {sorted(THRESHOLDS)} μ€ νλ"}, status_code=400) | |
| t0 = time.time() | |
| try: | |
| r = ask(q.question, mode=q.mode, n=max(3, min(q.n, 12)), web=bool(q.web)) | |
| except RuntimeError as e: | |
| # μ€μ λλ½μ μ₯μ κ° μλλΌ **μ€μ μλ΄**λ‘ λλ €μ€λ€ | |
| return JSONResponse({"error": str(e)[:300], "setup_required": True}, status_code=503) | |
| except Exception as e: | |
| traceback.print_exc() | |
| return JSONResponse({"error": f"κ²μ μ€ν¨: {str(e)[:200]}"}, status_code=500) | |
| got = {p["pmid"] for p in r["papers"]} | |
| cited = set(r.get("cited_pmids") or []) | |
| r["hallucinated_pmids"] = sorted(cited - got) # νμ λͺ©λ‘ λ° PMID = νκ° | |
| r["elapsed_sec"] = round(time.time() - t0, 1) | |
| r["threshold"] = THRESHOLDS[q.mode] | |
| return r | |
| def health(): | |
| return dict(ok=True, backend=os.environ.get("PHARMA_BACKEND", "api"), | |
| proxy=bool(PROXY_TOKEN), rate_per_min=RATE_PER_MIN, | |
| access_token_required=bool(ACCESS_TOKEN), ask_per_min=ASK_PER_MIN, | |
| relay=bool(UPSTREAM_ASK), | |
| model=os.environ.get("PHARMA_LOCAL_MODEL", "-"), | |
| thresholds=THRESHOLDS) | |
| # ββ LLM νλ‘μ βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # π΄ μ νμνκ°: HF Space(cpu-basic)λ 9B λ₯Ό λͺ» λλ¦¬κ³ , μ μ llama-server λ 127.0.0.1 μλ§ | |
| # λ¬Άμ¬ μμ΄ Space κ° λΏμ μ μλ€. κ·Έλμ **Space λ νλ©΄λ§, μΆλ‘ μ μ°λ¦¬ GPU μμ μ°λ¦¬ λͺ¨λΈλ‘.** | |
| # μ΄κ²μ΄ μμΌλ©΄ μ νμ΄ λ¨μ λͺ¨λΈμ μ°κ² λλ€. | |
| # | |
| # π΄ μ°λ¦¬ GPU λ₯Ό μΈν°λ·μ μ¬λ κ²μ΄λ―λ‘ λ κ°μ§λ₯Ό 건λ€: | |
| # β 곡μ ν ν°(`PHARMA_PROXY_TOKEN`) β μμΌλ©΄ νλ‘μ μμ²΄κ° κΊΌμ§λ€ | |
| # β‘ λΆλΉ νΈμΆ μ ν β 곡μ GPU λΌ λ¨μ μμ μ λ°μ΄λ΄λ©΄ μ λλ€ | |
| PROXY_TOKEN = os.environ.get("PHARMA_PROXY_TOKEN", "").strip() | |
| UPSTREAM = os.environ.get("PHARMA_SERVER", "http://127.0.0.1:7910/v1/chat/completions") | |
| RATE_PER_MIN = int(os.environ.get("PHARMA_RATE_PER_MIN", "20")) | |
| _calls: list[float] = [] | |
| async def api_llm(request: Request): | |
| if not PROXY_TOKEN: | |
| return JSONResponse({"error": "νλ‘μκ° κΊΌμ Έ μμ΅λλ€ (PHARMA_PROXY_TOKEN λ―Έμ€μ )"}, | |
| status_code=503) | |
| if request.headers.get("x-pharma-token", "") != PROXY_TOKEN: | |
| return JSONResponse({"error": "μΈμ¦ μ€ν¨"}, status_code=401) | |
| now = time.time() | |
| _calls[:] = [t for t in _calls if now - t < 60] | |
| if len(_calls) >= RATE_PER_MIN: | |
| return JSONResponse({"error": f"λΆλΉ {RATE_PER_MIN}ν μ νμ λμμ΅λλ€"}, status_code=429) | |
| _calls.append(now) | |
| body = await request.body() | |
| rq = urllib.request.Request(UPSTREAM, data=body, | |
| headers={"Content-Type": "application/json"}) | |
| try: | |
| with urllib.request.urlopen(rq, timeout=600) as f: | |
| return JSONResponse(json.load(f)) | |
| except Exception as e: | |
| return JSONResponse({"error": f"μλ₯ λͺ¨λΈ νΈμΆ μ€ν¨: {str(e)[:160]}"}, status_code=502) | |
| # ββ μ€νΈλ¦¬λ° μ’ λ¨μ ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # κ²μΒ·κ²μ΄νΈλ κ·Έλλ‘ λκ³ **λ΅λ³ μμ±λ§** νλ¦°λ€. κ²μ΄νΈλ λ΅μ΄ λ€ λμ¨ λ€ νμ νλ―λ‘ | |
| # π΄ μ€νΈλ¦¬λ° μ€μλ μμ§ **보λ₯ νμ μ **μ΄λ€ β νλ©΄μμ κ·Έ μ¬μ€μ λ°λμ νμνλ€. | |
| async def api_ask_stream(q: Q, request: Request): | |
| blocked = check_access(request) | |
| if blocked is not None: | |
| return blocked | |
| if UPSTREAM_ASK: | |
| up = UPSTREAM_ASK.replace("/api/ask", "/api/ask/stream") | |
| def relay(): | |
| body = json.dumps(dict(question=q.question, mode=q.mode, n=q.n, | |
| web=bool(getattr(q, "web", False)))).encode() | |
| _h = {"Content-Type": "application/json", | |
| "x-client-ip": _client_ip(request)} | |
| _ut = os.environ.get("PHARMA_UPSTREAM_TOKEN", "").strip() | |
| if _ut: | |
| _h["x-pharma-access"] = _ut | |
| rq = urllib.request.Request(up, data=body, | |
| headers=_h) | |
| try: | |
| with urllib.request.urlopen(rq, timeout=900) as f: | |
| for chunk in f: | |
| yield chunk | |
| except Exception as e: | |
| yield ("data: " + json.dumps({"type": "error", | |
| "error": str(e)[:200]}) + chr(10) + chr(10)).encode() | |
| return StreamingResponse(relay(), media_type="text/event-stream", | |
| headers={"Cache-Control": "no-store", | |
| "X-Accel-Buffering": "no"}) | |
| def gen(): | |
| def ev(o): | |
| return "data: " + json.dumps(o, ensure_ascii=False) + chr(10) + chr(10) | |
| t0 = time.time() | |
| # π΄ μ€μ¬κ³ 2μ°λ°μ μ’ μ§λΆ: μμ§μ ask.prepare() **ν ν¨μλ‘ ν΅μΌ**νλ€. | |
| # μ€νΈλ¦¬λ°μ΄ μ체 μμ§μ κ°λ λμ β μ κ²μ΄νΈκ° μ’μ μ΄μμ λ€μ§μκ³ | |
| # β‘FDAΒ·UniProt λ± 12μ’ μμ€κ° νλ©΄ κ²½λ‘μ λΉ μ Έ μ¬νμ μμ κ·Όκ±° λΆμ‘±μΌλ‘ μ‘λ€. | |
| try: | |
| from ask import prepare as A_prepare, LEDGER_ADD as A_LEDGER | |
| import fulltext as A_FT | |
| P = A_prepare(q.question, mode=q.mode, n=max(3, min(q.n, 12)), | |
| web=bool(getattr(q, "web", False))) | |
| except Exception as e: | |
| yield ev({"type": "error", "error": "κ²μ μ€ν¨: " + str(e)[:160]}); return | |
| papers, trials, queries = P["papers"], P["trials"], P["queries"] | |
| if not papers and not trials: | |
| yield ev({"type": "done", "answered": False, "reason": "κ²μ κ²°κ³Ό μμ", | |
| "papers": [], "trials": [], "queries": queries, | |
| "elapsed_sec": round(time.time() - t0, 1)}); return | |
| yield ev({"type": "papers", "papers": papers, "trials": trials, "queries": queries, | |
| "qtype": P["qtype"], "used_fda": P["fda"], "used_s2": P["s2"]}) | |
| block = (chr(10) + chr(10)).join(A_FT.block(p) for p in papers) | |
| if P["extra"]: | |
| block = block + chr(10) + chr(10) + P["extra"] | |
| EVB = int(os.environ.get("PHARMA_EVIDENCE_CHARS", "26000")) | |
| if len(block) > EVB: | |
| block = block[:EVB] + chr(10) + "(κ·Όκ±°κ° κΈΈμ΄ μ΄νλ μλ Έμ΅λλ€)" | |
| prompt_t = A_PROMPT + (A_LEDGER if P["qtype"] == "judge" else "") | |
| prompt = prompt_t.format(q=q.question, papers=block, | |
| trials=(A_CT.block(trials) if trials else "(μμ)")) | |
| buf = [] | |
| try: | |
| for piece in A_stream_llm(prompt): | |
| buf.append(piece) | |
| yield ev({"type": "delta", "text": piece}) | |
| except Exception as e: | |
| yield ev({"type": "error", "error": "μμ± μ€ν¨: " + str(e)[:160]}); return | |
| raw = "".join(buf) | |
| # π΄ κ°νμ΄ λ μ κ·μμ νμΌμ μ¬μ λ μμ£Ό κΉ¨μ§λ€. λ¬Έμμ΄ λΆν λ‘ λ½λλ€. | |
| body = raw.split("ANSWER:", 1)[-1] | |
| ans = body.split("CONFIDENCE:", 1)[0].strip() or raw.strip() | |
| conf = A_logprob_confidence(block, q.question) | |
| thr = THRESHOLDS[q.mode] | |
| # π΄ μ€μ¬κ³ (νμ₯λ μ λ°): μ€νΈλ¦¬λ° κ²½λ‘μ **μ νμ μ½λκ° λ°λ‘ μ΄μ** μμλ€ β | |
| # ask.ask() μ λ£μ μ νλΆκΈ°Β·μ°¨λ¨κΆ μΆμκ° μ¬κΈ°μ μμ΄μ, μ νν μ΄μμ | |
| # λ€ νλ €λ³΄λΈ λ€ λ§μ§λ§μ μ κ²μ΄νΈκ° 보λ₯ λμ₯μ μ°μλ€. | |
| # κ°μ νμ κ·μΉμ μ΄μνλ€: νλ¨ν=κ²μ΄νΈ λ―Έμ μ© Β· μ¬μ€ν=λͺ λ°±λΆμΌμΉ(0.15)λ§ μ°¨λ¨. | |
| _qt = P["qtype"] | |
| if _qt == "judge": | |
| ok = True | |
| else: | |
| _blk = float(os.environ.get("PHARMA_FACT_BLOCK", "0.15")) | |
| ok = conf is not None and conf >= min(thr, _blk) | |
| # μ λ Ή μΈμ© μ¬νκ²μ¦ β μ€μ‘΄νλ©΄ κ·Όκ±° μΉκ²©, μλλ©΄ κ²½κ³ (ask μ κ°μ ν¨μ) | |
| try: | |
| from ask import rescue_citations as A_rescue | |
| ans, _ghosts, _rescued = A_rescue(ans, papers, trials) | |
| except Exception: | |
| _ghosts, _rescued = [], [] | |
| got = {str(p["pmid"]) for p in papers} | |
| cited = sorted(set(A_re.findall("PMID:([A-Za-z]*[0-9]+)", ans))) | |
| cited_nct = sorted(set(A_re.findall("NCT:?[ ]*(NCT[0-9]+)", ans))) | |
| yield ev({"type": "done", "answered": ok, "confidence": conf, "threshold": thr, | |
| "qtype": _qt, | |
| "answer": ans if ok else None, "withheld_answer": None if ok else ans, | |
| "reason": ("νλ¨ν μ§μ β κ²μ΄νΈ λμ λ Όλ¬Έλ³ μ견μΌλ‘ νλ¨νμμμ€" | |
| if _qt == "judge" else | |
| (("μ λ’°λ " + str(round(conf, 3)) + " Β· π΄κ²μ΄νΈ μ¬λ³΄μ μ€ β μ°Έκ³ λ§") | |
| if ok else ("κ·Όκ±°κ° μ§λ¬Έκ³Ό λͺ λ°±ν 무κ΄ν©λλ€ (μ λ’°λ " | |
| + str(round(conf, 3) if conf is not None else 0) + ")"))), | |
| "cited_pmids": cited, "cited_ncts": cited_nct, | |
| "trials": trials, | |
| "hallucinated_pmids": _ghosts, "rescued_pmids": _rescued, | |
| "papers": papers, | |
| "elapsed_sec": round(time.time() - t0, 1)}) | |
| return StreamingResponse(gen(), media_type="text/event-stream", | |
| headers={"Cache-Control": "no-store", "X-Accel-Buffering": "no"}) | |
| # ββ νΌμ‘ κ°μ§ ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # π΄ 2026-08-31: ννμ΄ κ°μ GPU μμ boltz 4νλ‘μΈμ€λ₯Ό λλ € μ¬μ©λ₯ 100% κ° λμ | |
| # "Say OK." ν λ§λμ **49μ΄**κ° κ±Έλ Έλ€. μ¬μ©μμκ²λ **κ·Έλ₯ λ©μΆ κ²μΌλ‘ 보μΈλ€.** | |
| # π΄ 곡μ GPU μ΄λ―λ‘ λ¨μ μμ μ 건λλ¦¬μ§ μλλ€. λμ **μνλ₯Ό μ μ§νκ² μλ¦°λ€.** | |
| def probe_llm_latency(timeout: int = 40): | |
| """μ§§μ μμ² νλλ‘ μ§κΈ μΌλ§λ λ°λ Έλμ§ μ°λ€. | |
| π΄ **νμμμκ³Ό μ¬λ§μ ꡬλΆνλ€.** 25μ΄ μμ λ΅μ΄ μλ€κ³ μλ²κ° μ£½μ κ²μ΄ μλλ€ β | |
| 곡μ GPU κ° λ°λ¦¬λ©΄ "ok" ν λ§λλ 28~49μ΄κ° κ±Έλ¦°λ€(μ€μΈ‘). | |
| π ꡬλΆμ **μμΈ μ’ λ₯**λ‘ νλ€. νμμμμ΄ λ¬λ€λ©΄ **μ°κ²°μ μ΄λ―Έ λ κ²**μ΄κ³ , | |
| μ°κ²° μμ²΄κ° μ λλ©΄ `URLError(ConnectionRefused)` κ° **μ¦μ** λλ€. | |
| (μμ μμΌμ λ°λ‘ μ΄μ΄ νμΈνλ €λ€ μ€νλ € μ€ννλ€ β λ¨μν μͺ½μ΄ μ³μλ€.) | |
| λ°ν: (μ΄, μν) Β· μν = ok | timeout | refused | |
| """ | |
| up = os.environ.get("PHARMA_SERVER", "http://127.0.0.1:7910/v1/chat/completions") | |
| body = json.dumps(dict(model="local", temperature=0.0, max_tokens=2, | |
| chat_template_kwargs={"enable_thinking": False}, | |
| messages=[{"role": "user", "content": "ok"}])).encode() | |
| t = time.time() | |
| try: | |
| rq = urllib.request.Request(up, data=body, | |
| headers={"Content-Type": "application/json"}) | |
| with urllib.request.urlopen(rq, timeout=timeout) as f: | |
| f.read(1) | |
| return round(time.time() - t, 1), "ok" | |
| except (TimeoutError, socket.timeout): | |
| return round(time.time() - t, 1), "timeout" | |
| except urllib.error.URLError as e: | |
| inner = getattr(e, "reason", None) | |
| if isinstance(inner, (TimeoutError, socket.timeout)): | |
| return round(time.time() - t, 1), "timeout" | |
| return round(time.time() - t, 1), "refused" | |
| except Exception: | |
| return round(time.time() - t, 1), "timeout" | |
| def api_load(): | |
| """μ§κΈ μλ²κ° μΌλ§λ λΆλΉλκ°. νλ©΄μ΄ κ²μ μ μ λ¬Όμ΄λ³Έλ€.""" | |
| if UPSTREAM_ASK: | |
| # π΄ μλ₯κ° λ°λ¦¬λ©΄ **λΆν μ‘°ν μ체λ λ°λ¦°λ€.** κ·Έκ²μ `unknown`/`down` μΌλ‘ λ³΄κ³ νλ©΄ | |
| # κ°μ₯ λΆλΉ λ κ°μ₯ λΆμ νν΄μ§λ€. μ‘°νκ° μ€ν¨ν κ² μμ²΄κ° **νΌμ‘μ μ νΈ**λ€. | |
| up = UPSTREAM_ASK.replace("/api/ask", "/api/load") | |
| try: | |
| with urllib.request.urlopen(urllib.request.Request(up), timeout=60) as f: | |
| return JSONResponse(json.load(f)) | |
| except urllib.error.URLError as e: | |
| inner = getattr(e, "reason", None) | |
| if isinstance(inner, (TimeoutError, socket.timeout)): | |
| return dict(state="congested", latency_sec=None, | |
| note="μλ₯ μν μ‘°νκ° 60μ΄λ₯Ό λκ²Όμ΅λλ€") | |
| return dict(state="down", latency_sec=None) | |
| except Exception: | |
| return dict(state="congested", latency_sec=None, | |
| note="μλ₯ μνλ₯Ό νμΈνμ§ λͺ»νμ΅λλ€") | |
| ms, st = probe_llm_latency() | |
| if st == "refused": | |
| return dict(state="down", latency_sec=None) | |
| if st == "timeout": | |
| return dict(state="congested", latency_sec=ms, note="μλ΅μ΄ 25μ΄λ₯Ό λκ²Όμ΅λλ€") | |
| # νκ°ν λ μ΄ μμ²μ 1μ΄ λ―Έλ§μ΄λ€. μ€μΈ‘ νΌμ‘ μ 49μ΄. | |
| state = "idle" if ms < 3 else ("busy" if ms < 15 else "congested") | |
| return dict(state=state, latency_sec=ms) | |
| def index(): | |
| with open(os.path.join(HERE, "index.html"), encoding="utf-8") as f: | |
| return HTMLResponse(f.read(), headers={"Cache-Control": "no-store"}) | |
| # π΄ λ‘μ§ νμ HTML μ μΊμ κΈμ§ β μ κ·Έλ¬λ©΄ κ³ μΉ νμ΄ λͺ λΆκ° μ λ¬λ€(κΈ°λ‘λ μ¬κ³ ) | |
| if __name__ == "__main__": | |
| import uvicorn | |
| uvicorn.run(app, host="0.0.0.0", port=int(os.environ.get("PORT", "7912")), log_level="info") | |