SeaWolf-AI's picture
곡개면 νŒμ •κΈ° 제거 β€” A/B 병렬 λΉ„κ΅λŠ” μœ μ§€, /api/judge 차단(ν‚€ 보호)
859afbb verified
Raw History Blame Contribute Delete
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
@app.post("/api/rival")
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
@app.post("/api/judge")
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)
@app.post("/api/ask")
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
@app.get("/api/health")
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] = []
@app.post("/api/llm")
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)
# ── 슀트리밍 쒅단점 ──────────────────────────────────────────────────────────
# κ²€μƒ‰Β·κ²Œμ΄νŠΈλŠ” κ·ΈλŒ€λ‘œ 두고 **λ‹΅λ³€ μƒμ„±λ§Œ** ν˜λ¦°λ‹€. κ²Œμ΄νŠΈλŠ” 닡이 λ‹€ λ‚˜μ˜¨ λ’€ νŒμ •ν•˜λ―€λ‘œ
# πŸ”΄ 슀트리밍 μ€‘μ—λŠ” 아직 **보λ₯˜ νŒμ • μ „**이닀 β€” ν™”λ©΄μ—μ„œ κ·Έ 사싀을 λ°˜λ“œμ‹œ ν‘œμ‹œν•œλ‹€.
@app.post("/api/ask/stream")
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"
@app.get("/api/load")
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)
@app.get("/", response_class=HTMLResponse)
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")