ONGRID / app.py
SeaWolf-AI's picture
fix: define _ip helper (verify 500)
3cdb5a1 verified
Raw History Blame Contribute Delete
7.47 kB
import os, json, time, collections, os.path as op, threading, urllib.request, urllib.error
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
from fastapi.staticfiles import StaticFiles
app = FastAPI()
KEY = os.environ.get("ANTHROPIC_API_KEY", "")
MODEL = os.environ.get("KNOU_MODEL", "claude-haiku-4-5-20251001")
# ================= ZTC (zero-token answer verification, in-process on CPU) =================
ZTC_REPO = os.environ.get("ZTC_REPO", "FINAL-Bench/ZTC-Judge-4B")
_ZT = {"ready": False, "err": None, "model": None, "tok": None, "cfg": None, "lin": None, "cv": None}
def _ztc_load():
try:
import numpy as np, torch
from transformers import AutoModel, AutoTokenizer
from huggingface_hub import snapshot_download
p = snapshot_download(ZTC_REPO)
cfg = json.load(open(op.join(p, "ztc_config.json"), encoding="utf-8"))
lin = dict(np.load(op.join(p, cfg["probe_file"]), allow_pickle=True))
cvp = op.join(p, "ztc_curve_probe_v2.npz")
if not op.exists(cvp):
cvp = op.join(p, cfg.get("curve_probe_file", "ztc_curve_probe.npz"))
cv = dict(np.load(cvp))
tok = AutoTokenizer.from_pretrained(p)
if tok.pad_token is None:
tok.pad_token = tok.eos_token
model = AutoModel.from_pretrained(p, dtype=torch.bfloat16, low_cpu_mem_usage=True).eval()
_ZT.update(model=model, tok=tok, cfg=cfg, lin=lin, cv=cv, ready=True)
except Exception as e:
_ZT["err"] = str(e)[:200]
threading.Thread(target=_ztc_load, daemon=True).start()
def ztc_score(question, answer):
import numpy as np, torch
cfg, tok, model, lin, cv = _ZT["cfg"], _ZT["tok"], _ZT["model"], _ZT["lin"], _ZT["cv"]
text = cfg["template"] % ((question or "").strip(), (answer or "").strip())
b = tok([text], return_tensors="pt", truncation=True, max_length=cfg["max_length"])
with torch.no_grad():
h = model(input_ids=b["input_ids"], attention_mask=b["attention_mask"]).last_hidden_state
v = h[0, int(b["attention_mask"].sum()) - 1].float().numpy().astype(np.float64)
s = float(((v - lin["mu"]) / lin["sd"]) @ lin["w"])
z = (v - cv["mu"]) / cv["sd"]; a = cv["anchors"].astype(np.float64)
d2 = np.sum(a * a, 1) + float(z @ z) - 2.0 * (a @ z)
k = np.exp(-np.maximum(d2, 0.0) / (float(cv["med"]) * float(cv["gamma"])))
c = float(k @ cv["alpha"].astype(np.float64))
t = cfg["thresholds"]
verdict = "๊ฒ€ํ†  ์š”๋ง" if s <= t["review"] else ("์ด์ƒ ์—†์Œ" if s >= t["clear"] else "ํŒ๋‹จ ๋ณด๋ฅ˜")
return {"score": round(s, 3), "verdict": verdict, "curved": round(c, 3), "repo": ZTC_REPO, "generated_tokens": 0}
# ๋น„์šฉ ์•…์šฉ ๋ฐฉ์ง€: IP๋‹น ๋ถ„๋‹น ํ˜ธ์ถœ ์ œํ•œ (์ •์ƒ ์‚ฌ์šฉ์—” ์˜ํ–ฅ ์—†์Œ)
_HITS = collections.defaultdict(list)
_LIMIT = int(os.environ.get("KNOU_RATE_PER_MIN", "20"))
def _rate_ok(ip):
now = time.time(); q = _HITS[ip]
while q and now - q[0] > 60: q.pop(0)
if len(q) >= _LIMIT: return False
q.append(now); return True
SYS = (
"๋„ˆ๋Š” ์ง€์‹๊ทธ๋ž˜ํ”„ ๊ธฐ๋ฐ˜ ์งˆ์˜์‘๋‹ต ์—”์ง„์ด๋‹ค. ์ œ๊ณต๋œ '์‚ฌ์‹ค(ํŠธ๋ฆฌํ”Œ)'๋งŒ ๊ทผ๊ฑฐ๋กœ ๋‹ตํ•œ๋‹ค. "
"ํŠธ๋ฆฌํ”Œ์€ 'A โ€” ๊ด€๊ณ„ โ†’ B' ํ˜•์‹์ด๋ฉฐ ๋‹ค์Œ์ฒ˜๋Ÿผ ์ž์—ฐ์–ด๋กœ ํ•ด์„ํ•ด ๋‹ต์„ ๊ตฌ์„ฑํ•œ๋‹ค: "
"IsA=A๋Š” B์˜ ํ•œ ์ข…๋ฅ˜, UsedFor=A๋Š” B์— ์“ฐ์ž„, AtLocation=A๋Š” B์— ์žˆ์Œ, "
"CapableOf=A๋Š” B๋ฅผ ํ•  ์ˆ˜ ์žˆ์Œ, MadeOf=A๋Š” B๋กœ ๋งŒ๋“ค์–ด์ง, HasA=A๋Š” B๋ฅผ ๊ฐ€์ง, "
"HasProperty=A๋Š” Bํ•œ ์„ฑ์งˆ, PartOf=A๋Š” B์˜ ์ผ๋ถ€, Causes=A๋Š” B๋ฅผ ์ผ์œผํ‚ด, HasSubevent=A๋ฅผ ํ•˜๋ฉด B๊ฐ€ ์ผ์–ด๋‚จ, RelatedTo=A๋Š” B์™€ ๊ด€๋ จ. "
"์งˆ๋ฌธ ๋Œ€์ƒ์— ๊ด€ํ•œ ์‚ฌ์‹ค๋“ค์„ ๋ฌถ์–ด ์ž์‹ ๊ฐ ์žˆ๊ฒŒ ์ง€์ •๋œ ์–ธ์–ด๋กœ 2~4๋ฌธ์žฅ์œผ๋กœ ๋‹ตํ•œ๋‹ค. "
"๊ด€๊ณ„ ์ด๋ฆ„(IsA ๋“ฑ)์„ ๊ทธ๋Œ€๋กœ ๋‚˜์—ดํ•˜์ง€ ๋ง๊ณ  ์ž์—ฐ์Šค๋Ÿฌ์šด ๋ฌธ์žฅ์œผ๋กœ ํ’€์–ด ์“ด๋‹ค. "
"๋ณด๋„ ๊ธฐ์‚ฌ(๊ธฐ์‚ฌ ์ œ๋ชฉ์ด ์‚ฌ์‹ค๋กœ ์ฃผ์–ด์ง)์˜ ๊ฒฝ์šฐ ์ œ๋ชฉ ๋‚ด์šฉ์„ ๊ทผ๊ฑฐ๋กœ ์‚ฌ์‹ค๋งŒ ์š”์•ฝํ•œ๋‹ค. "
"์ •๋ง๋กœ ๊ด€๋ จ ์‚ฌ์‹ค์ด ํ•˜๋‚˜๋„ ์—†์„ ๋•Œ๋งŒ ํ•ด๋‹น ์–ธ์–ด๋กœ '๊ด€๋ จ ์ •๋ณด๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค'๋ผ๊ณ  ๋‹ตํ•œ๋‹ค. ์—†๋Š” ์‚ฌ์‹ค์„ ์ง€์–ด๋‚ด์ง€ ์•Š๋Š”๋‹ค."
)
def _ip(req):
fwd = req.headers.get("x-forwarded-for")
return (fwd.split(",")[0].strip() if fwd else (req.client.host if req.client else "x")) or "x"
@app.get("/health")
def health():
return {"ok": True, "has_key": bool(KEY), "model": MODEL}
@app.post("/ask")
async def ask(req: Request):
try:
data = await req.json()
except Exception:
return JSONResponse({"answer": "์š”์ฒญ ํ˜•์‹ ์˜ค๋ฅ˜."})
q = (str(data.get("query") or "")).strip()[:600]
facts = data.get("facts") or []
facts = [str(f)[:200] for f in facts][:60]
lang = str(data.get("lang") or "ko")[:2]
langname = {"ko": "ํ•œ๊ตญ์–ด(Korean)", "en": "English", "zh": "ไธญๆ–‡(Chinese)"}.get(lang, "ํ•œ๊ตญ์–ด(Korean)")
ip = (req.client.host if req.client else "x")
fwd = req.headers.get("x-forwarded-for")
if fwd: ip = fwd.split(",")[0].strip()
if not _rate_ok(ip):
return JSONResponse({"answer": "์š”์ฒญ์ด ๋งŽ์Šต๋‹ˆ๋‹ค. ์ž ์‹œ ํ›„ ๋‹ค์‹œ ์‹œ๋„ํ•ด ์ฃผ์„ธ์š”. / Rate limit, please retry shortly."})
if not q:
return JSONResponse({"answer": "..."})
if not KEY:
return JSONResponse({"answer": "(์„œ๋ฒ„์— LLM ํ‚ค๊ฐ€ ์„ค์ •๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค. Space Secrets์— ANTHROPIC_API_KEY๋ฅผ ์ถ”๊ฐ€ํ•˜์„ธ์š”.)"})
factstr = "\n".join("- " + f for f in facts) if facts else "(๊ด€๋ จ ์‚ฌ์‹ค ์—†์Œ)"
prompt = ("Facts retrieved from the knowledge graph:\n" + factstr + "\n\nQuestion: " + q +
"\n\nAnswer ONLY from the facts above, in " + langname +
", in 2-4 sentences. If the facts are insufficient, say so briefly in " + langname + ". Do not invent facts.")
body = json.dumps({
"model": MODEL, "max_tokens": 400, "system": SYS,
"messages": [{"role": "user", "content": prompt}],
}).encode()
r = urllib.request.Request(
"https://api.anthropic.com/v1/messages", data=body,
headers={"x-api-key": KEY, "anthropic-version": "2023-06-01", "content-type": "application/json"},
)
try:
resp = urllib.request.urlopen(r, timeout=45)
d = json.load(resp)
text = "".join(b.get("text", "") for b in d.get("content", []) if b.get("type") == "text")
return JSONResponse({"answer": text or "(๋นˆ ์‘๋‹ต)"})
except urllib.error.HTTPError as e:
return JSONResponse({"answer": "(LLM ์˜ค๋ฅ˜ " + str(e.code) + ")"})
except Exception:
return JSONResponse({"answer": "(LLM ํ˜ธ์ถœ ์‹คํŒจ)"})
@app.get("/ztc_status")
def ztc_status():
return {"ready": _ZT["ready"], "err": _ZT["err"], "repo": ZTC_REPO}
@app.post("/verify")
async def verify_ep(req: Request):
if not _rate_ok("v:" + _ip(req)):
return JSONResponse({"ok": False, "error": "rate"})
try:
data = await req.json()
except Exception:
return JSONResponse({"ok": False})
q = str(data.get("query") or "")[:600]; a = str(data.get("answer") or "")[:2000]
if not q or not a:
return JSONResponse({"ok": False})
if not _ZT["ready"]:
return JSONResponse({"ok": False, "error": "ztc_loading", "detail": _ZT["err"]})
try:
return JSONResponse({"ok": True, "ztc": ztc_score(q, a)})
except Exception as e:
return JSONResponse({"ok": False, "error": str(e)[:160]})
app.mount("/", StaticFiles(directory="static", html=True), name="static")