rolmaxx commited on
Commit
2eb3a25
·
verified ·
1 Parent(s): c2fd1dd

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +161 -42
app.py CHANGED
@@ -1,11 +1,83 @@
1
  import spaces # MUST be imported before torch
2
 
 
 
 
 
 
 
3
  import torch
4
  import gradio as gr
 
 
5
  from transformers import AutoTokenizer, AutoModelForCausalLM
6
  from peft import PeftModel
7
 
8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9
  # ============================================================
10
  # Configuration
11
  # ============================================================
@@ -24,71 +96,44 @@ appropriate professional medical care."""
24
  # Tokenizer & Model – loaded on CPU (no GPU yet)
25
  # ============================================================
26
 
27
- print("Loading tokenizer...")
28
  tokenizer = AutoTokenizer.from_pretrained(BASE_MODEL)
29
  if tokenizer.pad_token is None:
30
  tokenizer.pad_token = tokenizer.eos_token
31
 
32
- print("Loading base model on CPU...")
33
  model = AutoModelForCausalLM.from_pretrained(
34
  BASE_MODEL,
35
  torch_dtype=torch.float16,
36
- device_map="cpu", # force CPU – GPU will be used later inside the decorated function
37
  )
38
- print("Base model loaded on CPU.")
39
 
40
- print("Loading MediGuide QLoRA adapter on CPU...")
41
  model = PeftModel.from_pretrained(
42
  model,
43
  ADAPTER_MODEL,
44
- torch_device="cpu", # load adapter on CPU
45
  )
46
- model = model.merge_and_unload() # merge for faster inference
47
  model.eval()
48
- print("MediGuide loaded successfully (on CPU).")
49
 
50
 
51
  # ============================================================
52
- # Generation – GPU is requested via @spaces.GPU
53
  # ============================================================
54
 
55
  @spaces.GPU(duration=60)
56
- def generate_response(message, history):
57
- """
58
- Generate a response from the model.
59
- history: list of [user_msg, assistant_msg] tuples from Gradio.
60
- """
61
- # 1. Sanitise inputs
62
- if not isinstance(message, str):
63
- message = str(message) if message is not None else ""
64
-
65
- messages = [{"role": "system", "content": SYSTEM_PROMPT}]
66
-
67
- for turn in history:
68
- if not isinstance(turn, (list, tuple)) or len(turn) < 2:
69
- continue
70
- user_msg, assistant_msg = turn[0], turn[1]
71
- if user_msg is not None and str(user_msg).strip():
72
- messages.append({"role": "user", "content": str(user_msg)})
73
- if assistant_msg is not None and str(assistant_msg).strip():
74
- messages.append({"role": "assistant", "content": str(assistant_msg)})
75
-
76
- if message.strip():
77
- messages.append({"role": "user", "content": message.strip()})
78
- else:
79
- return "Please enter a valid question."
80
-
81
- # 2. Move model to GPU (now available thanks to @spaces.GPU)
82
  model.to("cuda")
83
 
84
- # 3. Apply chat template
85
  prompt = tokenizer.apply_chat_template(
86
  messages,
87
  tokenize=False,
88
  add_generation_prompt=True,
89
  )
90
 
91
- # 4. Tokenize
92
  inputs = tokenizer(
93
  prompt,
94
  return_tensors="pt",
@@ -97,7 +142,6 @@ def generate_response(message, history):
97
  )
98
  inputs = {k: v.to("cuda") for k, v in inputs.items()}
99
 
100
- # 5. Generate
101
  with torch.no_grad():
102
  outputs = model.generate(
103
  **inputs,
@@ -109,17 +153,60 @@ def generate_response(message, history):
109
  pad_token_id=tokenizer.eos_token_id,
110
  )
111
 
112
- # 6. Decode only new tokens
113
  generated_tokens = outputs[0][inputs["input_ids"].shape[-1]:]
114
  response = tokenizer.decode(generated_tokens, skip_special_tokens=True)
115
 
116
- # 7. Move model back to CPU to free GPU memory for other requests
117
  model.to("cpu")
118
- torch.cuda.empty_cache() # optional, helps release memory
119
 
120
  return response.strip()
121
 
122
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
123
  # ============================================================
124
  # Gradio UI
125
  # ============================================================
@@ -160,8 +247,40 @@ with gr.Blocks(title="MediGuide") as demo:
160
  clear_btn.click(lambda: [], outputs=chatbot)
161
 
162
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
163
  # ============================================================
164
  # Launch
165
  # ============================================================
166
 
167
- demo.queue().launch()
 
 
 
1
  import spaces # MUST be imported before torch
2
 
3
+ import time
4
+ import json
5
+ import logging
6
+ import uuid
7
+ from datetime import datetime, timezone
8
+
9
  import torch
10
  import gradio as gr
11
+ from fastapi import FastAPI
12
+ from pydantic import BaseModel
13
  from transformers import AutoTokenizer, AutoModelForCausalLM
14
  from peft import PeftModel
15
 
16
 
17
+ # ============================================================
18
+ # Logging setup (monitoring)
19
+ # ============================================================
20
+
21
+ logging.basicConfig(
22
+ level=logging.INFO,
23
+ format="%(asctime)s | %(levelname)s | %(message)s",
24
+ )
25
+ logger = logging.getLogger("mediguide")
26
+
27
+ REQUEST_LOG_FILE = "request_log.jsonl" # note: ephemeral storage on free Spaces, resets on restart
28
+
29
+
30
+ def log_request(request_id, message, response, latency_s, flagged, error=None):
31
+ entry = {
32
+ "request_id": request_id,
33
+ "timestamp": datetime.now(timezone.utc).isoformat(),
34
+ "message_len": len(message),
35
+ "response_len": len(response) if response else 0,
36
+ "latency_s": round(latency_s, 3),
37
+ "flagged": flagged,
38
+ "error": error,
39
+ }
40
+ logger.info(json.dumps(entry))
41
+ try:
42
+ with open(REQUEST_LOG_FILE, "a") as f:
43
+ f.write(json.dumps(entry) + "\n")
44
+ except Exception as e:
45
+ logger.warning(f"Could not write to request log file: {e}")
46
+
47
+
48
+ # ============================================================
49
+ # Guardrails
50
+ # ============================================================
51
+
52
+ EMERGENCY_KEYWORDS = [
53
+ "suicide", "kill myself", "want to die", "end my life",
54
+ "chest pain", "can't breathe", "cannot breathe", "heart attack",
55
+ "overdose", "severe bleeding", "unconscious", "stroke symptoms",
56
+ ]
57
+
58
+ DISCLAIMER = (
59
+ "\n\n⚠️ This is not a substitute for professional medical advice. "
60
+ "If this is urgent, please contact a doctor or emergency services."
61
+ )
62
+
63
+ EMERGENCY_RESPONSE = (
64
+ "This sounds like it could be a medical emergency. Please contact "
65
+ "your local emergency services or go to the nearest emergency room "
66
+ "immediately. I'm not able to provide emergency medical care."
67
+ )
68
+
69
+
70
+ def check_emergency(message: str) -> bool:
71
+ msg_lower = message.lower()
72
+ return any(kw in msg_lower for kw in EMERGENCY_KEYWORDS)
73
+
74
+
75
+ def apply_output_guardrails(response: str) -> str:
76
+ if "not a substitute" not in response.lower() and "professional medical" not in response.lower():
77
+ response = response.strip() + DISCLAIMER
78
+ return response
79
+
80
+
81
  # ============================================================
82
  # Configuration
83
  # ============================================================
 
96
  # Tokenizer & Model – loaded on CPU (no GPU yet)
97
  # ============================================================
98
 
99
+ logger.info("Loading tokenizer...")
100
  tokenizer = AutoTokenizer.from_pretrained(BASE_MODEL)
101
  if tokenizer.pad_token is None:
102
  tokenizer.pad_token = tokenizer.eos_token
103
 
104
+ logger.info("Loading base model on CPU...")
105
  model = AutoModelForCausalLM.from_pretrained(
106
  BASE_MODEL,
107
  torch_dtype=torch.float16,
108
+ device_map="cpu",
109
  )
110
+ logger.info("Base model loaded on CPU.")
111
 
112
+ logger.info("Loading MediGuide QLoRA adapter on CPU...")
113
  model = PeftModel.from_pretrained(
114
  model,
115
  ADAPTER_MODEL,
116
+ torch_device="cpu",
117
  )
118
+ model = model.merge_and_unload()
119
  model.eval()
120
+ logger.info("MediGuide loaded successfully (on CPU).")
121
 
122
 
123
  # ============================================================
124
+ # Core generation (unchanged logic, guardrails + logging wrapped around it)
125
  # ============================================================
126
 
127
  @spaces.GPU(duration=60)
128
+ def _run_model(messages):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
129
  model.to("cuda")
130
 
 
131
  prompt = tokenizer.apply_chat_template(
132
  messages,
133
  tokenize=False,
134
  add_generation_prompt=True,
135
  )
136
 
 
137
  inputs = tokenizer(
138
  prompt,
139
  return_tensors="pt",
 
142
  )
143
  inputs = {k: v.to("cuda") for k, v in inputs.items()}
144
 
 
145
  with torch.no_grad():
146
  outputs = model.generate(
147
  **inputs,
 
153
  pad_token_id=tokenizer.eos_token_id,
154
  )
155
 
 
156
  generated_tokens = outputs[0][inputs["input_ids"].shape[-1]:]
157
  response = tokenizer.decode(generated_tokens, skip_special_tokens=True)
158
 
 
159
  model.to("cpu")
160
+ torch.cuda.empty_cache()
161
 
162
  return response.strip()
163
 
164
 
165
+ def generate_response(message, history):
166
+ """
167
+ history: list of [user_msg, assistant_msg] tuples from Gradio.
168
+ Wraps model call with guardrails + logging.
169
+ """
170
+ request_id = str(uuid.uuid4())[:8]
171
+ start = time.time()
172
+
173
+ if not isinstance(message, str):
174
+ message = str(message) if message is not None else ""
175
+ message = message.strip()
176
+
177
+ if not message:
178
+ return "Please enter a valid question."
179
+
180
+ # Input guardrail: emergency detection short-circuits the model entirely
181
+ if check_emergency(message):
182
+ latency = time.time() - start
183
+ log_request(request_id, message, EMERGENCY_RESPONSE, latency, flagged="emergency_keyword")
184
+ return EMERGENCY_RESPONSE
185
+
186
+ messages = [{"role": "system", "content": SYSTEM_PROMPT}]
187
+ for turn in history:
188
+ if not isinstance(turn, (list, tuple)) or len(turn) < 2:
189
+ continue
190
+ user_msg, assistant_msg = turn[0], turn[1]
191
+ if user_msg is not None and str(user_msg).strip():
192
+ messages.append({"role": "user", "content": str(user_msg)})
193
+ if assistant_msg is not None and str(assistant_msg).strip():
194
+ messages.append({"role": "assistant", "content": str(assistant_msg)})
195
+ messages.append({"role": "user", "content": message})
196
+
197
+ try:
198
+ response = _run_model(messages)
199
+ response = apply_output_guardrails(response)
200
+ latency = time.time() - start
201
+ log_request(request_id, message, response, latency, flagged=None)
202
+ return response
203
+ except Exception as e:
204
+ latency = time.time() - start
205
+ log_request(request_id, message, "", latency, flagged=None, error=str(e))
206
+ logger.error(f"[{request_id}] generation failed: {e}")
207
+ return "Sorry, something went wrong while generating a response. Please try again."
208
+
209
+
210
  # ============================================================
211
  # Gradio UI
212
  # ============================================================
 
247
  clear_btn.click(lambda: [], outputs=chatbot)
248
 
249
 
250
+ # ============================================================
251
+ # FastAPI wrapper — gives you a real REST endpoint alongside the UI
252
+ # ============================================================
253
+
254
+ app = FastAPI(title="MediGuide API")
255
+
256
+
257
+ class ChatRequest(BaseModel):
258
+ message: str
259
+ history: list = []
260
+
261
+
262
+ class ChatResponse(BaseModel):
263
+ response: str
264
+
265
+
266
+ @app.get("/health")
267
+ def health():
268
+ return {"status": "ok"}
269
+
270
+
271
+ @app.post("/chat", response_model=ChatResponse)
272
+ def chat_endpoint(req: ChatRequest):
273
+ response = generate_response(req.message, req.history)
274
+ return ChatResponse(response=response)
275
+
276
+
277
+ app = gr.mount_gradio_app(app, demo.queue(), path="/")
278
+
279
+
280
  # ============================================================
281
  # Launch
282
  # ============================================================
283
 
284
+ if __name__ == "__main__":
285
+ import uvicorn
286
+ uvicorn.run(app, host="0.0.0.0", port=7860)