Spaces:
Paused
Paused
Download server.py from WilliamShakespear/LearnAi: direct link, hf CLI and curl.
- Browser
- Download file 7.8 kB
-
https://huggingface.co/spaces/WilliamShakespear/LearnAi/resolve/main/server.py
- Command line
-
hf download hf://spaces/WilliamShakespear/LearnAi/server.py
-
curl -L -o server.py https://huggingface.co/spaces/WilliamShakespear/LearnAi/resolve/main/server.py
7.8 kB
| #!/usr/bin/env python3 | |
| """ | |
| Laya HTTP server — bọc model Laya (decision model) thành REST API cho backend LearnAI. | |
| Hỗ trợ 2 backend: | |
| --backend mlx : `pip install laya-mlx` (Apple Silicon, không cần PyTorch) | |
| --backend torch : `pip install laya` (PyTorch, Linux/Windows/WSL; hỗ trợ laya-multilingual) | |
| Chạy: | |
| python server.py --backend torch --model convaiinnovations/laya-multilingual \ | |
| --host 0.0.0.0 --port 8791 --token <TOKEN> | |
| Hợp đồng API (xem README.md trong thư mục này và backend/src/services/layaService.ts): | |
| POST /predict { state, questions } -> { model, answers, timing_ms } | |
| GET /health -> { status, model, backend } | |
| LƯU Ý: API Python của Laya có thể thay đổi giữa các phiên bản. Script này gọi qua lớp | |
| mỏng `_predict()` và tự dò vài tên hàm phổ biến (predict / system_one). Nếu phiên bản | |
| bạn cài khác đi, chỉ cần sửa `_predict()`. | |
| """ | |
| from __future__ import annotations | |
| import argparse | |
| import os | |
| import threading | |
| import time | |
| from typing import Any, Dict | |
| # Model card Laya khuyến nghị: transformers dò TensorFlow/JAX lúc import có thể gây deadlock | |
| # khi nạp model (abseil runtime của TF). Đặt trước khi import transformers/laya. | |
| os.environ.setdefault("USE_TF", "0") | |
| os.environ.setdefault("USE_JAX", "0") | |
| os.environ.setdefault("TOKENIZERS_PARALLELISM", "false") | |
| from fastapi import FastAPI, Header, HTTPException, Request | |
| parser = argparse.ArgumentParser() | |
| parser.add_argument("--backend", choices=["mlx", "torch"], default="torch") | |
| parser.add_argument("--model", default=os.environ.get("LAYA_MODEL", "convaiinnovations/laya-multilingual")) | |
| parser.add_argument("--host", default=os.environ.get("LAYA_HOST", "0.0.0.0")) | |
| parser.add_argument( | |
| "--port", | |
| type=int, | |
| # HF Spaces: app phải nghe cổng 7860 (hoặc khai báo app_port trong README của Space). | |
| default=int(os.environ.get("LAYA_PORT") or os.environ.get("PORT") or "8791"), | |
| ) | |
| parser.add_argument("--token", default=os.environ.get("LAYA_SERVER_TOKEN", "")) | |
| parser.add_argument("--max-len", type=int, default=0, help="0 = dùng mặc định của checkpoint") | |
| args = parser.parse_args() | |
| app = FastAPI(title="Laya server cho LearnAI") | |
| _agent = None | |
| _load_lock = threading.Lock() | |
| def _load_agent() -> Any: | |
| """Nạp checkpoint (chỉ nạp một lần, an toàn khi gọi từ nhiều luồng).""" | |
| global _agent | |
| if _agent is None: | |
| with _load_lock: | |
| if _agent is None: | |
| started = time.perf_counter() | |
| if args.backend == "mlx": | |
| import laya_mlx as laya # type: ignore | |
| else: | |
| import laya # type: ignore | |
| _agent = laya.load(args.model) | |
| if args.max_len and hasattr(_agent, "cfg"): | |
| _agent.cfg["max_len"] = args.max_len | |
| print( | |
| f"[laya-server] đã nạp {args.model} (backend={args.backend}) " | |
| f"trong {time.perf_counter() - started:.1f}s" | |
| ) | |
| return _agent | |
| def _warmup_on_startup() -> None: | |
| """ | |
| Mở cổng TRƯỚC, nạp model ở luồng nền. Bắt buộc với HF Spaces/Docker: | |
| health check phải trả 200 sớm, nếu không Space bị coi là "khởi động lỗi". | |
| Đặt LAYA_WARMUP=0 nếu muốn nạp thật sự lười (chỉ khi có request đầu tiên). | |
| """ | |
| if os.environ.get("LAYA_WARMUP", "1") == "0": | |
| return | |
| threading.Thread(target=_load_agent, name="laya-warmup", daemon=True).start() | |
| def _predict(state: Dict[str, Any], questions: Dict[str, Any]) -> Dict[str, Any]: | |
| """ | |
| Lớp mỏng bọc API Python của Laya. Sửa ở ĐÂY nếu phiên bản Laya của bạn đổi tên hàm. | |
| Theo model card (https://huggingface.co/convaiinnovations/laya-multilingual): | |
| agent = laya.load("convaiinnovations/laya-multilingual") | |
| result = agent.predict(state, questions) | |
| ⇒ ưu tiên gọi PHƯƠNG THỨC `agent.predict`, sau đó mới tới các biến thể cũ. | |
| """ | |
| agent = _load_agent() | |
| if hasattr(agent, "predict"): | |
| return agent.predict(state, questions) | |
| if args.backend == "mlx": | |
| import laya_mlx as laya # type: ignore | |
| else: | |
| import laya # type: ignore | |
| if hasattr(laya, "predict"): | |
| return laya.predict(agent, state, questions) | |
| if hasattr(laya, "system_one"): | |
| return laya.system_one(agent, state, questions) | |
| raise RuntimeError("Không tìm thấy hàm predict/system_one trong gói laya đã cài.") | |
| def _extract_score(answer: Any) -> float: | |
| if isinstance(answer, dict): | |
| for key in ("score", "value", "level"): | |
| if key in answer and answer[key] is not None: | |
| return float(answer[key]) | |
| if "probability" in answer and answer["probability"] is not None: | |
| return float(answer["probability"]) | |
| return float(answer) | |
| def _extract_probability(answer: Any) -> float: | |
| if isinstance(answer, dict): | |
| for key in ("probability", "prob", "p"): | |
| if key in answer and answer[key] is not None: | |
| return float(answer[key]) | |
| if "score" in answer and answer["score"] is not None: | |
| score = float(answer["score"]) | |
| return score if 0.0 <= score <= 1.0 else 0.5 | |
| return float(answer) | |
| def health() -> Dict[str, Any]: | |
| return {"status": "ok", "backend": args.backend, "model": args.model, "loaded": _agent is not None} | |
| def warmup() -> Dict[str, Any]: | |
| """Nạp model ngay (đồng bộ) — tiện gọi sau khi Space vừa "thức" để tránh timeout.""" | |
| started = time.perf_counter() | |
| _load_agent() | |
| return { | |
| "status": "ok", | |
| "model": args.model, | |
| "loaded": True, | |
| "loaded_in_ms": round((time.perf_counter() - started) * 1000, 1), | |
| } | |
| async def predict(request: Request, authorization: str | None = Header(default=None)) -> Dict[str, Any]: | |
| if args.token: | |
| expected = f"Bearer {args.token}" | |
| if authorization != expected: | |
| raise HTTPException(status_code=401, detail="Token không hợp lệ.") | |
| payload = await request.json() | |
| state = payload.get("state") | |
| questions = payload.get("questions") | |
| if not state or not questions: | |
| raise HTTPException(status_code=422, detail="Thiếu 'state' hoặc 'questions'.") | |
| started = time.perf_counter() | |
| try: | |
| raw = _predict(state, questions) | |
| except Exception as exc: # noqa: BLE001 — trả lỗi rõ ràng cho backend | |
| raise HTTPException(status_code=500, detail=f"Laya lỗi khi suy luận: {exc}") from exc | |
| raw_answers = raw.get("answers", raw) if isinstance(raw, dict) else {} | |
| answers: Dict[str, Any] = {} | |
| for name, answer in raw_answers.items(): | |
| if isinstance(answer, dict) and any(k in answer for k in ("probability", "prob", "p")): | |
| answers[name] = { | |
| "probability": _extract_probability(answer), | |
| "answer": bool(answer.get("answer", answer.get("label", False))), | |
| } | |
| else: | |
| answers[name] = {"score": _extract_score(answer)} | |
| return { | |
| "model": payload.get("model") or args.model, | |
| "answers": answers, | |
| "timing_ms": round((time.perf_counter() - started) * 1000, 1), | |
| } | |
| if __name__ == "__main__": | |
| import uvicorn | |
| # Không nạp model ở đây để cổng được mở ngay (xem _warmup_on_startup). | |
| print(f"[laya-server] {args.model} (backend={args.backend}) — nghe {args.host}:{args.port}") | |
| uvicorn.run(app, host=args.host, port=args.port) | |