Download app.py from oleh13/LoRa: direct link, hf CLI and curl.
- Browser
- Download file 4.97 kB
-
https://huggingface.co/spaces/oleh13/LoRa/resolve/main/app.py
- Command line
-
hf download hf://spaces/oleh13/LoRa/app.py
-
curl -L -o app.py https://huggingface.co/spaces/oleh13/LoRa/resolve/main/app.py
4.97 kB
| import os | |
| import json | |
| from typing import Any, Dict, List, Optional, Union | |
| import torch | |
| from fastapi import FastAPI, HTTPException | |
| from pydantic import BaseModel, Field | |
| from huggingface_hub import login | |
| from transformers import AutoTokenizer, AutoModelForCausalLM | |
| MODEL_ID = os.getenv("MODEL_ID", "oleh13/ord-retro-qwen25-15b-merged-fp16") | |
| hf_token = os.getenv("HF_TOKEN") | |
| if hf_token: | |
| login(token=hf_token) | |
| app = FastAPI( | |
| title="ORD Retrosynthesis Model API", | |
| version="1.0.0", | |
| ) | |
| class ChatMessage(BaseModel): | |
| role: str | |
| content: str | |
| class GenerateRequest(BaseModel): | |
| prompt: Optional[str] = None | |
| messages: Optional[List[ChatMessage]] = None | |
| max_new_tokens: int = Field(default=1200, ge=1, le=4096) | |
| temperature: float = Field(default=0.1, ge=0.0, le=2.0) | |
| top_p: float = Field(default=0.9, ge=0.0, le=1.0) | |
| repetition_penalty: float = Field(default=1.05, ge=0.5, le=2.0) | |
| do_sample: bool = True | |
| return_json_only: bool = True | |
| class GenerateResponse(BaseModel): | |
| text: str | |
| parsed_json: Optional[Union[Dict[str, Any], List[Any]]] = None | |
| raw_output: str | |
| model_id: str | |
| def extract_json_object(text: str) -> str: | |
| text = text.strip() | |
| first_obj = text.find("{") | |
| last_obj = text.rfind("}") | |
| first_arr = text.find("[") | |
| last_arr = text.rfind("]") | |
| obj_valid = first_obj != -1 and last_obj != -1 and last_obj > first_obj | |
| arr_valid = first_arr != -1 and last_arr != -1 and last_arr > first_arr | |
| if obj_valid and arr_valid: | |
| if first_obj < first_arr: | |
| return text[first_obj:last_obj + 1] | |
| return text[first_arr:last_arr + 1] | |
| if obj_valid: | |
| return text[first_obj:last_obj + 1] | |
| if arr_valid: | |
| return text[first_arr:last_arr + 1] | |
| raise ValueError("No JSON object or array found in model output.") | |
| print(f"Loading tokenizer: {MODEL_ID}") | |
| tokenizer = AutoTokenizer.from_pretrained( | |
| MODEL_ID, | |
| trust_remote_code=True, | |
| ) | |
| if tokenizer.pad_token is None: | |
| tokenizer.pad_token = tokenizer.eos_token | |
| print(f"Loading model: {MODEL_ID}") | |
| if torch.cuda.is_available(): | |
| torch_dtype = torch.bfloat16 | |
| device_map = "auto" | |
| else: | |
| torch_dtype = torch.float32 | |
| device_map = "cpu" | |
| model = AutoModelForCausalLM.from_pretrained( | |
| MODEL_ID, | |
| torch_dtype=torch_dtype, | |
| device_map=device_map, | |
| trust_remote_code=True, | |
| low_cpu_mem_usage=True, | |
| ) | |
| model.eval() | |
| print("Model loaded.") | |
| print("CUDA:", torch.cuda.is_available()) | |
| def root(): | |
| return { | |
| "status": "ok", | |
| "model_id": MODEL_ID, | |
| "cuda": torch.cuda.is_available(), | |
| } | |
| def health(): | |
| return { | |
| "status": "ok", | |
| "model_id": MODEL_ID, | |
| "cuda": torch.cuda.is_available(), | |
| } | |
| def generate(req: GenerateRequest): | |
| if req.messages and req.prompt: | |
| raise HTTPException( | |
| status_code=400, | |
| detail="Send either 'prompt' or 'messages', not both.", | |
| ) | |
| if not req.messages and not req.prompt: | |
| raise HTTPException( | |
| status_code=400, | |
| detail="Send either 'prompt' or 'messages'.", | |
| ) | |
| if req.messages: | |
| messages = [m.model_dump() for m in req.messages] | |
| prompt_text = tokenizer.apply_chat_template( | |
| messages, | |
| tokenize=False, | |
| add_generation_prompt=True, | |
| ) | |
| else: | |
| prompt_text = req.prompt | |
| inputs = tokenizer( | |
| [prompt_text], | |
| return_tensors="pt", | |
| ) | |
| if torch.cuda.is_available(): | |
| inputs = {k: v.to(model.device) for k, v in inputs.items()} | |
| with torch.no_grad(): | |
| outputs = model.generate( | |
| **inputs, | |
| max_new_tokens=req.max_new_tokens, | |
| temperature=req.temperature, | |
| top_p=req.top_p, | |
| repetition_penalty=req.repetition_penalty, | |
| do_sample=req.do_sample, | |
| eos_token_id=tokenizer.eos_token_id, | |
| pad_token_id=tokenizer.pad_token_id, | |
| ) | |
| raw_output = tokenizer.decode( | |
| outputs[0][inputs["input_ids"].shape[-1]:], | |
| skip_special_tokens=True, | |
| ).strip() | |
| parsed_json = None | |
| final_text = raw_output | |
| if req.return_json_only: | |
| try: | |
| json_text = extract_json_object(raw_output) | |
| parsed_json = json.loads(json_text) | |
| final_text = json.dumps(parsed_json, ensure_ascii=False, indent=2) | |
| except Exception as exc: | |
| raise HTTPException( | |
| status_code=422, | |
| detail={ | |
| "message": "Model did not return valid JSON.", | |
| "error": str(exc), | |
| "raw_output": raw_output, | |
| }, | |
| ) | |
| return GenerateResponse( | |
| text=final_text, | |
| parsed_json=parsed_json, | |
| raw_output=raw_output, | |
| model_id=MODEL_ID, | |
| ) |