oleh13 commited on
Commit
f73086f
·
verified ·
1 Parent(s): 93e62ed

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +199 -0
app.py ADDED
@@ -0,0 +1,199 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import json
3
+ from typing import Any, Dict, List, Optional, Union
4
+
5
+ import torch
6
+ from fastapi import FastAPI, HTTPException
7
+ from pydantic import BaseModel, Field
8
+ from huggingface_hub import login
9
+ from transformers import AutoTokenizer, AutoModelForCausalLM
10
+
11
+
12
+ MODEL_ID = os.getenv("MODEL_ID", "oleh13/ord-retro-qwen25-15b-merged-fp16")
13
+
14
+ hf_token = os.getenv("HF_TOKEN")
15
+ if hf_token:
16
+ login(token=hf_token)
17
+
18
+ app = FastAPI(
19
+ title="ORD Retrosynthesis Model API",
20
+ version="1.0.0",
21
+ )
22
+
23
+
24
+ class ChatMessage(BaseModel):
25
+ role: str
26
+ content: str
27
+
28
+
29
+ class GenerateRequest(BaseModel):
30
+ prompt: Optional[str] = None
31
+ messages: Optional[List[ChatMessage]] = None
32
+
33
+ max_new_tokens: int = Field(default=1200, ge=1, le=4096)
34
+ temperature: float = Field(default=0.1, ge=0.0, le=2.0)
35
+ top_p: float = Field(default=0.9, ge=0.0, le=1.0)
36
+ repetition_penalty: float = Field(default=1.05, ge=0.5, le=2.0)
37
+ do_sample: bool = True
38
+
39
+ return_json_only: bool = True
40
+
41
+
42
+ class GenerateResponse(BaseModel):
43
+ text: str
44
+ parsed_json: Optional[Union[Dict[str, Any], List[Any]]] = None
45
+ raw_output: str
46
+ model_id: str
47
+
48
+
49
+ def extract_json_object(text: str) -> str:
50
+ text = text.strip()
51
+
52
+ first_obj = text.find("{")
53
+ last_obj = text.rfind("}")
54
+
55
+ first_arr = text.find("[")
56
+ last_arr = text.rfind("]")
57
+
58
+ obj_valid = first_obj != -1 and last_obj != -1 and last_obj > first_obj
59
+ arr_valid = first_arr != -1 and last_arr != -1 and last_arr > first_arr
60
+
61
+ if obj_valid and arr_valid:
62
+ if first_obj < first_arr:
63
+ return text[first_obj:last_obj + 1]
64
+ return text[first_arr:last_arr + 1]
65
+
66
+ if obj_valid:
67
+ return text[first_obj:last_obj + 1]
68
+
69
+ if arr_valid:
70
+ return text[first_arr:last_arr + 1]
71
+
72
+ raise ValueError("No JSON object or array found in model output.")
73
+
74
+
75
+ print(f"Loading tokenizer: {MODEL_ID}")
76
+ tokenizer = AutoTokenizer.from_pretrained(
77
+ MODEL_ID,
78
+ trust_remote_code=True,
79
+ )
80
+
81
+ if tokenizer.pad_token is None:
82
+ tokenizer.pad_token = tokenizer.eos_token
83
+
84
+
85
+ print(f"Loading model: {MODEL_ID}")
86
+
87
+ if torch.cuda.is_available():
88
+ torch_dtype = torch.bfloat16
89
+ device_map = "auto"
90
+ else:
91
+ torch_dtype = torch.float32
92
+ device_map = "cpu"
93
+
94
+ model = AutoModelForCausalLM.from_pretrained(
95
+ MODEL_ID,
96
+ torch_dtype=torch_dtype,
97
+ device_map=device_map,
98
+ trust_remote_code=True,
99
+ low_cpu_mem_usage=True,
100
+ )
101
+
102
+ model.eval()
103
+
104
+ print("Model loaded.")
105
+ print("CUDA:", torch.cuda.is_available())
106
+
107
+
108
+ @app.get("/")
109
+ def root():
110
+ return {
111
+ "status": "ok",
112
+ "model_id": MODEL_ID,
113
+ "cuda": torch.cuda.is_available(),
114
+ }
115
+
116
+
117
+ @app.get("/health")
118
+ def health():
119
+ return {
120
+ "status": "ok",
121
+ "model_id": MODEL_ID,
122
+ "cuda": torch.cuda.is_available(),
123
+ }
124
+
125
+
126
+ @app.post("/generate", response_model=GenerateResponse)
127
+ def generate(req: GenerateRequest):
128
+ if req.messages and req.prompt:
129
+ raise HTTPException(
130
+ status_code=400,
131
+ detail="Send either 'prompt' or 'messages', not both.",
132
+ )
133
+
134
+ if not req.messages and not req.prompt:
135
+ raise HTTPException(
136
+ status_code=400,
137
+ detail="Send either 'prompt' or 'messages'.",
138
+ )
139
+
140
+ if req.messages:
141
+ messages = [m.model_dump() for m in req.messages]
142
+
143
+ prompt_text = tokenizer.apply_chat_template(
144
+ messages,
145
+ tokenize=False,
146
+ add_generation_prompt=True,
147
+ )
148
+ else:
149
+ prompt_text = req.prompt
150
+
151
+ inputs = tokenizer(
152
+ [prompt_text],
153
+ return_tensors="pt",
154
+ )
155
+
156
+ if torch.cuda.is_available():
157
+ inputs = {k: v.to(model.device) for k, v in inputs.items()}
158
+
159
+ with torch.no_grad():
160
+ outputs = model.generate(
161
+ **inputs,
162
+ max_new_tokens=req.max_new_tokens,
163
+ temperature=req.temperature,
164
+ top_p=req.top_p,
165
+ repetition_penalty=req.repetition_penalty,
166
+ do_sample=req.do_sample,
167
+ eos_token_id=tokenizer.eos_token_id,
168
+ pad_token_id=tokenizer.pad_token_id,
169
+ )
170
+
171
+ raw_output = tokenizer.decode(
172
+ outputs[0][inputs["input_ids"].shape[-1]:],
173
+ skip_special_tokens=True,
174
+ ).strip()
175
+
176
+ parsed_json = None
177
+ final_text = raw_output
178
+
179
+ if req.return_json_only:
180
+ try:
181
+ json_text = extract_json_object(raw_output)
182
+ parsed_json = json.loads(json_text)
183
+ final_text = json.dumps(parsed_json, ensure_ascii=False, indent=2)
184
+ except Exception as exc:
185
+ raise HTTPException(
186
+ status_code=422,
187
+ detail={
188
+ "message": "Model did not return valid JSON.",
189
+ "error": str(exc),
190
+ "raw_output": raw_output,
191
+ },
192
+ )
193
+
194
+ return GenerateResponse(
195
+ text=final_text,
196
+ parsed_json=parsed_json,
197
+ raw_output=raw_output,
198
+ model_id=MODEL_ID,
199
+ )