starfriend commited on
Commit
d4b7ffd
·
verified ·
1 Parent(s): 256d995

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +234 -1060
app.py CHANGED
@@ -1,670 +1,260 @@
1
- import os
2
- import re
3
- from pathlib import Path
4
- import time
5
  import threading
6
- import torch
7
- import pandas as pd
8
  import gradio as gr
9
- from difflib import SequenceMatcher
10
- import unsloth
11
  from unsloth import FastLanguageModel
12
- from peft import PeftModel
13
- from huggingface_hub import snapshot_download
14
-
15
- # --- 1. Configuration ---
16
- BASE_MODEL_ID = "unsloth/Meta-Llama-3.1-8B-Instruct"
17
- IT_ADAPTER_PATH = "./DA-IT"
18
- DPO_ADAPTER_PATH = "./DA-DPO"
19
- MAX_OPTIONS = 8
20
 
21
- DATA_PATH = os.path.join("Testing MCQA data", "Decarbonization_MCQA.csv")
22
 
23
- # --- 2. Load dataset ---
24
- try:
25
- MCQA_DF = pd.read_csv(DATA_PATH, encoding="utf-8")
26
- except UnicodeDecodeError:
27
- MCQA_DF = pd.read_csv(DATA_PATH, encoding="latin1")
 
 
 
 
 
 
 
 
 
 
 
 
28
 
29
- # Ensure only Question + A-D columns
30
- MCQA_DF = MCQA_DF[["Question", "A", "B", "C", "D"]]
31
 
32
- # --- 3. Lazy Loading for Models ---
 
 
33
  _model = None
34
  _tokenizer = None
35
-
36
- # --- Dynamic adapter configuration for agent-chat testing ---
37
- AGENT_ADAPTER_REPO_ID = "starfriend/WaterScopeAI-Adapters"
38
 
39
 
40
- def download_adapter_subfolder(subfolder: str) -> Path:
41
- """Download one adapter folder from the private Hub repository.
42
-
43
- The Space must define an HF_TOKEN secret with read access to
44
- starfriend/WaterScopeAI-Adapters. Hugging Face caches unchanged files, so
45
- repeated requests do not redownload the same adapter files.
 
 
 
 
 
 
 
46
  """
47
- hf_token = os.getenv("waterscopeai")
48
-
49
- if not hf_token:
50
- raise RuntimeError(
51
- "HF_TOKEN is not configured. Add a secret named HF_TOKEN in "
52
- "the Hugging Face Space settings, with read access to "
53
- f"{AGENT_ADAPTER_REPO_ID}."
54
- )
55
-
56
- local_repo_path = snapshot_download(
57
- repo_id=AGENT_ADAPTER_REPO_ID,
58
- repo_type="model",
59
- revision="main",
60
- token=hf_token,
61
- allow_patterns=[
62
- f"{subfolder}/*",
63
- f"{subfolder}/**",
64
- ],
65
- )
66
-
67
- adapter_path = Path(local_repo_path) / Path(subfolder)
68
- config_path = adapter_path / "adapter_config.json"
69
-
70
- if not config_path.is_file():
71
- raise FileNotFoundError(
72
- "adapter_config.json was not downloaded from "
73
- f"{AGENT_ADAPTER_REPO_ID}/{subfolder}. "
74
- f"Expected local path: {config_path}"
75
- )
76
-
77
- return adapter_path
78
-
79
- # User-facing adapter aliases -> repository subfolders
80
- AGENT_ADAPTER_PATHS = {
81
- "da_dpo": "DA-DPO",
82
- "da_it": "DA-IT",
83
- "dpo": "DPO",
84
- "bpd": "DA/BPd",
85
- "book": "DA/Book",
86
- "paperall": "DA/Paperall",
87
- "paperdc": "DA/Paperdc",
88
- "tulucore": "DA/Tulucore",
89
- "tulufair": "DA/Tulufair",
90
- "wikicore": "DA/Wikicore",
91
- "wikifair": "DA/Wikifair",
92
- }
93
-
94
- # Unique internal PEFT names prevent conflicts with the existing local
95
- # adapters named "it" and "dpo", which are used by the unchanged functions.
96
- AGENT_INTERNAL_NAMES = {
97
- alias: f"agent_test_{alias}"
98
- for alias in AGENT_ADAPTER_PATHS
99
- }
100
-
101
- _loaded_agent_adapters = set()
102
- _agent_adapter_lock = threading.Lock()
103
-
104
- def load_model_and_tokenizer():
105
  global _model, _tokenizer
106
-
107
- print("Initializing model and tokenizer for the first time...")
108
 
109
- if "_model" in globals() and _model is not None:
110
- print("Model already initialized.")
111
  return _model, _tokenizer
112
-
113
- base_model, _tokenizer = FastLanguageModel.from_pretrained(
114
- model_name=BASE_MODEL_ID,
115
- dtype=None,
116
- load_in_4bit=False,
117
- device_map={"": 0}, # Use GPU explicitly
118
- )
119
 
120
- FastLanguageModel.for_inference(base_model)
 
 
121
 
122
- print("Base model loaded.")
 
 
 
 
 
123
 
124
- #Load adapters
125
- _model = PeftModel.from_pretrained(base_model, IT_ADAPTER_PATH, adapter_name="it")
126
- print("DA-IT model loaded.")
127
- _model.load_adapter(DPO_ADAPTER_PATH, adapter_name="dpo")
128
- print("DA-DPO model loaded.")
129
 
130
- _model.eval()
131
- print("All models and adapters are ready!")
132
-
133
- # --- 4. Utility Functions ---
134
- def extract_letter(raw_answer: str) -> str:
135
- """Extract predicted option letter from model output"""
136
- # Priority 1: Look for explicit phrases like "answer is B"
137
- match = re.search(r"(?:answer|option) is\s+([A-H])", raw_answer, re.IGNORECASE)
138
- if match:
139
- return match.group(1).upper()
140
 
141
- # Priority 2: Look for formats like "B." or "B)" at the start
142
- match = re.search(r"^\s*([A-H])[\.\):]", raw_answer)
143
- if match:
144
- return match.group(1).upper()
145
 
146
- # Priority 3: Look for the first standalone letter in the text
147
- match = re.search(r"\b([A-H])\b", raw_answer)
148
- if match:
149
- return match.group(1).upper()
150
 
151
- return "N/A"
 
 
 
 
 
152
 
 
153
 
154
- def clean_repetitions(text: str) -> str:
155
- lines = [l.strip() for l in text.strip().splitlines() if l.strip()]
156
- if not lines:
157
- return ""
158
 
159
- # split into words (keep punctuation as part of word)
160
- def tokenize(line):
161
- return re.findall(r"\S+", line)
 
 
 
162
 
163
- result = tokenize(lines[0])
 
164
 
165
- for line in lines[1:]:
166
- tokens = tokenize(line)
 
 
167
 
168
- # find overlap
169
- i = 0
170
- while i < len(result) and i < len(tokens) and result[i].rstrip(".,!?") == tokens[i].rstrip(".,!?"):
171
- i += 1
172
 
173
- # append only the non-overlapping part
174
- result.extend(tokens[i:])
175
 
176
- return " ".join(result)
177
-
 
 
178
 
179
- # Global variable to track cancellation
180
- cancellation_requested = False
 
 
181
 
182
- def run_mcqa_comparison(question, opt_a, opt_b, opt_c, opt_d, opt_e, opt_f, opt_g, opt_h, generate_explanation):
183
- global _model, _tokenizer, cancellation_requested
184
-
185
- # Reset cancellation flag at start
186
- cancellation_requested = False
187
 
188
- if _model is None or _tokenizer is None:
189
- gr.Info("Initializing models for the first time, this may take a few minutes...")
190
- load_model_and_tokenizer()
191
-
192
- options = [opt_a, opt_b, opt_c, opt_d, opt_e, opt_f, opt_g, opt_h]
193
- active_options = [opt for opt in options if opt and opt.strip()]
194
- if not question or len(active_options) < 2:
195
- error_msg = ("Error", "Please enter a question and at least two options.")
196
- yield error_msg[0], error_msg[1], error_msg[0], error_msg[1], error_msg[0], error_msg[1]
197
- return
198
-
199
- # Check for cancellation
200
- if cancellation_requested:
201
- gr.Info("Processing cancelled by user")
202
- return
203
-
204
- # Build the user input content
205
- option_labels = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
206
- user_content = [f"Question: {question}\n Candidate options are: "]
207
- for i, option_text in enumerate(active_options):
208
- user_content.append(f"{option_labels[i]}. {option_text},")
209
-
210
- user_content = " ".join(user_content)
211
-
212
- if generate_explanation:
213
- optional_instruction = "\nProvide the letter of the correct option first. Then give an expert-level explanation in this format: 'The answer is [LETTER]. Because [EXPLANATION].'"
214
- max_tokens = 200
215
- else:
216
- optional_instruction = "\nNo need to explain your answer. "
217
- max_tokens = 30
218
-
219
- # System message provides context
220
- messages = [
221
- {
222
- "role": "system",
223
- "content": (
224
- "You are an expert in water and wastewater treatment and engineering, "
225
- "particularly in decarbonization, carbon emissions, and sustainability in the water/wastewater sector. "
226
- "Please answer the following multiple choice question. "
227
- f"{optional_instruction}"
228
- )
229
- },
230
- {
231
- "role": "user",
232
- "content": user_content
233
- }
234
- ]
235
 
236
- # Apply the tokenizer's chat template
237
- chat_input = _tokenizer.apply_chat_template(
238
- messages,
239
- tokenize=False,
240
- add_generation_prompt=True
241
  )
242
 
243
- inputs = _tokenizer(chat_input, return_tensors="pt").to(_model.device)
244
-
245
- def get_prediction(max_tokens_for_generation):
246
- # Check for cancellation before generating
247
- if cancellation_requested:
248
- raise gr.Error("Processing cancelled by user")
249
-
250
- with torch.no_grad():
251
- outputs = _model.generate(
252
- **inputs,
253
- max_new_tokens=max_tokens_for_generation,
254
- eos_token_id=_tokenizer.eos_token_id,
255
- pad_token_id=_tokenizer.pad_token_id,
256
- temperature=0.000001,
257
- )
258
- return _tokenizer.decode(outputs[0][len(inputs["input_ids"][0]):], skip_special_tokens=True)
259
-
260
- try:
261
- yield "", "Running...", "", "", "", ""
262
-
263
- # Check for cancellation before processing base model
264
- if cancellation_requested:
265
- gr.Info("Processing cancelled by user")
266
- return
267
-
268
- with _model.disable_adapter():
269
- base_raw = clean_repetitions(get_prediction(max_tokens))
270
- base_letter = extract_letter(base_raw)
271
-
272
- # Check for cancellation before processing IT model
273
- if cancellation_requested:
274
- gr.Info("Processing cancelled by user")
275
- return
276
-
277
- yield base_letter, base_raw.strip(), "", "Running...", "", ""
278
- _model.set_adapter("it")
279
- it_raw = clean_repetitions(get_prediction(max_tokens))
280
- it_letter = extract_letter(it_raw)
281
-
282
- # Check for cancellation before processing DPO model
283
- if cancellation_requested:
284
- gr.Info("Processing cancelled by user")
285
- return
286
-
287
- yield base_letter, base_raw.strip(), it_letter, it_raw.strip(), "", "Running..."
288
- _model.set_adapter("dpo")
289
- dpo_raw = clean_repetitions(get_prediction(max_tokens))
290
- dpo_letter = extract_letter(dpo_raw)
291
-
292
- yield base_letter, base_raw.strip(), it_letter, it_raw.strip(), dpo_letter, dpo_raw.strip()
293
-
294
- except gr.Error as e:
295
- if "cancelled" in str(e).lower():
296
- gr.Info("Processing cancelled by user")
297
- else:
298
- raise e
299
-
300
-
301
- # Function to handle cancellation
302
- def cancel_processing():
303
- global cancellation_requested
304
- cancellation_requested = True
305
- return "Cancellation requested"
306
-
307
-
308
- # Safe tokenization wrapper
309
- def safe_tokenize(chat_input, _tokenizer, _model, max_new_tokens=1600):
310
- # 1. Validate input type
311
- if not isinstance(chat_input, str) or len(chat_input.strip()) == 0:
312
- raise ValueError("chat_input must be a non-empty string")
313
-
314
- # 2. Sanitize weird characters (e.g., emojis, zero-width spaces)
315
- clean_input = re.sub(r"[^\x00-\x7F]+", " ", chat_input)
316
-
317
- # 3. Tokenize with truncation to avoid position limit issues
318
- max_input_tokens = (
319
- _model.config.max_position_embeddings
320
- - max_new_tokens
321
- - 100
322
  )
323
 
324
- tokens = _tokenizer(
325
- clean_input,
326
  return_tensors="pt",
327
  truncation=True,
328
- max_length=max_input_tokens
329
- )
330
-
331
- # 4. Validate token IDs
332
- vocab_size = _model.get_input_embeddings().weight.shape[0]
333
- max_id = tokens["input_ids"].max().item()
334
- min_id = tokens["input_ids"].min().item()
335
-
336
- print(f"[DEBUG] chat_input: {repr(chat_input)}")
337
- print(f"[DEBUG] sanitized_input: {repr(clean_input)}")
338
- print(f"[DEBUG] token IDs min: {min_id}, max: {max_id}, vocab size: {vocab_size}")
339
-
340
- if max_id >= vocab_size or min_id < 0:
341
- raise ValueError(f"Token IDs out of range: min {min_id}, max {max_id}, vocab size {vocab_size}")
342
-
343
- # 5. Move tokens to model device
344
- tokens = {k: v.to(_model.device) for k, v in tokens.items() if isinstance(v, torch.Tensor)}
345
- return tokens
346
-
347
-
348
- # Chat function using DA-DPO model
349
- def chat_with_model(message, history, max_new_tokens=1600, temperature=0.000001):
350
- """
351
- Chat with the model using a messages-style prompt (system + history + user).
352
- Keeps behavior consistent with the MCQA prompt flow (uses apply_chat_template).
353
- """
354
- global _model, _tokenizer
355
-
356
- if _model is None or _tokenizer is None:
357
- gr.Info("Initializing models for the first time, this may take a few minutes...")
358
- load_model_and_tokenizer()
359
-
360
- # Use the DPO adapter (as in your original)
361
- _model.set_adapter("dpo")
362
- _model.eval()
363
-
364
- system_prompt = (
365
- '''
366
- You are an expert AI assistant in water and wastewater engineering.
367
- When answering a question:
368
- - First, write down all relevant facts or values.
369
- - Next, identify which one is correct based on those facts for factual comparisons or multiple options.
370
- - Next, provide a clear description for conceptual definition questions.
371
- - Finally, clearly state your conclusion in this format:
372
- [Main answer]. [one or two sentences explaining the reasoning]. [appropriate values, equations to support the reasoning].
373
- '''
374
  )
375
 
376
- # Build messages list (same format used elsewhere)
377
- messages = [{"role": "system", "content": system_prompt}]
378
-
379
- if history:
380
- for user_msg, bot_msg in history:
381
- # skip empty items robustly
382
- if user_msg is not None:
383
- messages.append({"role": "user", "content": user_msg})
384
- if bot_msg is not None:
385
- messages.append({"role": "assistant", "content": bot_msg})
386
-
387
- # Current user message
388
- messages.append({"role": "user", "content": message})
389
-
390
- # Apply the tokenizer/chat template to get the final chat input string
391
- chat_input = _tokenizer.apply_chat_template(
392
- messages,
393
- tokenize=False,
394
- add_generation_prompt=True
395
- )
396
-
397
- # ---- Use safe tokenization ----
398
- try:
399
- inputs = safe_tokenize(chat_input, _tokenizer, _model, max_new_tokens=max_new_tokens)
400
- except Exception as e:
401
- print(f"[ERROR] Failed to tokenize input: {e}")
402
- return "Sorry, your input contains unsupported characters. Please rephrase. If error persists, try to clear chat and start the chat again. "
403
-
404
- # Tokenize (batch of 1) and move tensors to same device as model
405
- #inputs = _tokenizer(chat_input, return_tensors="pt").to(_model.device)
406
-
407
- with torch.no_grad():
408
- outputs = _model.generate(
409
- **inputs,
410
- max_new_tokens=max_new_tokens,
411
- eos_token_id=_tokenizer.eos_token_id,
412
- pad_token_id=_tokenizer.pad_token_id,
413
- do_sample=False,
414
- repetition_penalty=1.15,
415
- no_repeat_ngram_size=4,
416
- )
417
-
418
- # Decode the newly generated tokens only
419
- generated_ids = outputs[0][len(inputs["input_ids"][0]):]
420
- response = _tokenizer.decode(generated_ids, skip_special_tokens=True).strip()
421
- return response
422
 
 
423
 
424
 
425
- # Agent chat function supporting the base model, selectable adapters,
426
- # and a caller-supplied system prompt.
427
- def agent_chat_with_model(
 
428
  message,
429
  chat_history=None,
430
- adapter_name="da_it",
431
  system_prompt="",
432
- max_new_tokens=1600,
433
- return_diagnostics=False,
434
  ):
435
  """
436
- General question answering and document chat using either the unadapted
437
- base model or a selectable LoRA adapter.
438
-
439
- The base model is loaded only once. Each requested adapter is downloaded
440
- and loaded on its first use, then reused while the Space remains running.
441
-
442
- API inputs:
443
- message: str
444
- chat_history: list, optional
445
- adapter_name: str
446
- One of: base, da_dpo, da_it, dpo, bpd, book, paperall, paperdc,
447
- tulucore, tulufair, wikicore, or wikifair.
448
- system_prompt: str, optional
449
- A true system message supplied by the caller. If empty, the
450
- default WaterScope-AI system prompt is used.
451
- max_new_tokens: int, optional
452
- return_diagnostics: bool, optional
453
- If True, prepend model and tokenization diagnostics to the API
454
- response so they are visible in the local client output.
455
-
456
- API output:
457
- str: assistant response, optionally preceded by diagnostics
458
  """
459
- global _model, _tokenizer, _loaded_agent_adapters
460
 
461
  if _model is None or _tokenizer is None:
462
- load_model_and_tokenizer()
463
 
464
  if not isinstance(message, str) or not message.strip():
465
  return "Please provide a non-empty message."
466
 
467
- requested_alias = str(adapter_name or "da_it").strip().lower()
468
- available_choices = ["base", *AGENT_ADAPTER_PATHS.keys()]
469
-
470
- if requested_alias not in available_choices:
471
- available = ", ".join(available_choices)
472
- return (
473
- f"Unknown adapter '{requested_alias}'. "
474
- f"Available options: {available}."
475
- )
476
 
477
  try:
478
  max_new_tokens = int(max_new_tokens)
479
  except (TypeError, ValueError):
480
- max_new_tokens = 1600
481
-
482
- max_new_tokens = max(32, min(max_new_tokens, 4096))
483
-
484
- default_system_prompt = """
485
- You are WaterScope-AI, an expert assistant in water, wastewater,
486
- environmental engineering, and sustainability.
487
-
488
- Answer the user's actual question directly and clearly. You can:
489
- - explain concepts;
490
- - answer technical questions;
491
- - analyze or summarize user-provided text;
492
- - compare methods, results, or alternatives;
493
- - discuss research papers and their implications.
494
-
495
- When the user provides a document or passage:
496
- - base document-specific claims only on the supplied content;
497
- - preserve important quantitative results;
498
- - do not invent values, citations, standards, organizations, or findings;
499
- - do not continue or reproduce the source text;
500
- - do not generate repetitive words, phrases, numbers, or sequences;
501
- - summarize rather than completing patterns found in the document;
502
- - clearly distinguish information stated in the document from your interpretation.
503
-
504
- For general questions, use your domain knowledge and explain uncertainty when needed.
505
- Use concise paragraphs unless the user requests a specific structure or level of detail.
506
- """.strip()
507
 
508
- active_system_prompt = (
509
- str(system_prompt).strip()
510
- if system_prompt is not None and str(system_prompt).strip()
511
- else default_system_prompt
512
- )
513
 
514
- messages = [{"role": "system", "content": active_system_prompt}]
515
-
516
- # Accept either Gradio paired history or role/content dictionaries.
517
- if chat_history:
518
- for item in chat_history:
519
- if isinstance(item, dict):
520
- role = item.get("role")
521
- content = item.get("content")
522
- if role in {"user", "assistant"} and content:
523
- messages.append({"role": role, "content": str(content)})
524
-
525
- elif isinstance(item, (list, tuple)) and len(item) >= 2:
526
- user_msg, assistant_msg = item[0], item[1]
527
- if user_msg:
528
- messages.append({"role": "user", "content": str(user_msg)})
529
- if assistant_msg:
530
- messages.append({
531
- "role": "assistant",
532
- "content": str(assistant_msg),
533
- })
534
-
535
- messages.append({"role": "user", "content": message.strip()})
536
-
537
- chat_input = _tokenizer.apply_chat_template(
538
  messages,
539
  tokenize=False,
540
  add_generation_prompt=True,
541
  )
542
 
543
- try:
544
- inputs = safe_tokenize(
545
- chat_input,
546
- _tokenizer,
547
- _model,
548
- max_new_tokens=max_new_tokens,
549
- )
550
- except Exception as exc:
551
- print(f"[ERROR] Agent-chat tokenization failed: {exc}")
552
- return (
553
- "Sorry, I could not process this input. "
554
- "Please shorten the message or clear some chat history and try again."
555
- )
556
 
557
  diagnostics = {
558
- "requested_mode": requested_alias,
559
  "configured_base_model": BASE_MODEL_ID,
560
  "model_class": type(_model).__name__,
561
- "base_model_class": type(getattr(_model, "base_model", None)).__name__,
562
- "active_adapter_before_request": str(
563
- getattr(_model, "active_adapter", None)
564
- ),
565
- "loaded_peft_adapters": sorted(
566
- list(getattr(_model, "peft_config", {}).keys())
 
 
 
567
  ),
568
  "input_tokens": int(inputs["input_ids"].shape[1]),
569
- "max_new_tokens": int(max_new_tokens),
570
  "max_position_embeddings": int(
571
  getattr(_model.config, "max_position_embeddings", -1)
572
  ),
573
- "adapter_disabled_for_generation": requested_alias == "base",
574
- "merge_and_unload_used": False,
 
 
575
  }
576
 
577
- print("\n[AGENT CHAT DIAGNOSTICS]")
578
  for key, value in diagnostics.items():
579
  print(f"{key}: {value}")
580
- print("[END DIAGNOSTICS]\n")
581
-
582
- # Keep activation and generation under one lock so concurrent requests
583
- # cannot switch adapters while another response is being generated.
584
- with _agent_adapter_lock:
585
- try:
586
- if requested_alias == "base":
587
- print(
588
- "Agent chat is using the original base-model weights "
589
- "with all PEFT adapters disabled for this generation."
590
- )
591
 
592
- with _model.disable_adapter():
593
- print(
594
- "Entered _model.disable_adapter() context; "
595
- "LoRA adapters are disabled."
596
- )
597
- _model.eval()
598
- with torch.no_grad():
599
- outputs = _model.generate(
600
- **inputs,
601
- max_new_tokens=max_new_tokens,
602
- do_sample=False,
603
- repetition_penalty=1.15,
604
- no_repeat_ngram_size=4,
605
- eos_token_id=_tokenizer.eos_token_id,
606
- pad_token_id=_tokenizer.pad_token_id,
607
- use_cache=True,
608
- )
609
-
610
- else:
611
- subfolder = AGENT_ADAPTER_PATHS[requested_alias]
612
- internal_name = AGENT_INTERNAL_NAMES[requested_alias]
613
-
614
- if internal_name not in _loaded_agent_adapters:
615
- print(
616
- f"Downloading agent adapter '{requested_alias}' from "
617
- f"{AGENT_ADAPTER_REPO_ID}/{subfolder}..."
618
- )
619
-
620
- local_adapter_dir = download_adapter_subfolder(subfolder)
621
-
622
- print(f"Loading adapter from local path: {local_adapter_dir}")
623
- _model.load_adapter(
624
- str(local_adapter_dir),
625
- adapter_name=internal_name,
626
- )
627
- _loaded_agent_adapters.add(internal_name)
628
- print(
629
- f"Agent adapter '{requested_alias}' loaded successfully."
630
- )
631
-
632
- _model.set_adapter(internal_name)
633
- _model.eval()
634
- print(f"Agent chat is using adapter: {requested_alias}")
635
-
636
- with torch.no_grad():
637
- outputs = _model.generate(
638
- **inputs,
639
- max_new_tokens=max_new_tokens,
640
- do_sample=False,
641
- repetition_penalty=1.15,
642
- no_repeat_ngram_size=4,
643
- eos_token_id=_tokenizer.eos_token_id,
644
- pad_token_id=_tokenizer.pad_token_id,
645
- use_cache=True,
646
- )
647
-
648
- except Exception as exc:
649
- print(
650
- f"[ERROR] Failed to use model option "
651
- f"'{requested_alias}': {exc}"
652
- )
653
 
654
- if requested_alias == "base":
655
- return (
656
- "Could not run the base model with adapters disabled. "
657
- f"Error: {exc}"
658
- )
659
-
660
- subfolder = AGENT_ADAPTER_PATHS[requested_alias]
661
- return (
662
- f"Could not load or use adapter '{requested_alias}'. "
663
- f"Repository subfolder: '{subfolder}'. Error: {exc}"
664
  )
665
 
666
  input_length = inputs["input_ids"].shape[1]
667
- generated_ids = outputs[0][input_length:]
 
668
  response = _tokenizer.decode(
669
  generated_ids,
670
  skip_special_tokens=True,
@@ -672,8 +262,10 @@ Use concise paragraphs unless the user requests a specific structure or level of
672
 
673
  if bool(return_diagnostics):
674
  diagnostic_text = "\n".join(
675
- f"{key}: {value}" for key, value in diagnostics.items()
 
676
  )
 
677
  return (
678
  "=== DIAGNOSTICS ===\n"
679
  f"{diagnostic_text}\n"
@@ -684,497 +276,79 @@ Use concise paragraphs unless the user requests a specific structure or level of
684
  return response
685
 
686
 
687
- # Custom CSS for website-like appearance with lighter blue header
688
- custom_css = """
689
- .gradio-container {
690
- max-width: 1200px !important;
691
- margin: 0 auto !important;
692
- font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif !important;
693
- }
694
- .header {
695
- text-align: center;
696
- padding: 20px;
697
- background: linear-gradient(135deg, #6eb1ff 0%, #88d3fe 100%);
698
- color: white;
699
- border-radius: 8px;
700
- margin-bottom: 20px;
701
- }
702
- .header h1 {
703
- margin: 0;
704
- font-size: 2.5em;
705
- font-weight: 600;
706
- }
707
- .header p {
708
- margin: 10px 0 0;
709
- font-size: 1.2em;
710
- opacity: 0.9;
711
- }
712
- .section {
713
- background: white;
714
- padding: 20px;
715
- border-radius: 8px;
716
- box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
717
- margin-bottom: 20px;
718
- }
719
- .nav-bar {
720
- margin-bottom: 20px;
721
- display: flex;
722
- justify-content: center;
723
- gap: 10px;
724
- }
725
- .footer {
726
- text-align: center;
727
- padding: 15px;
728
- margin-top: 30px;
729
- color: #666;
730
- font-size: 0.9em;
731
- border-top: 1px solid #eee;
732
- }
733
- .dataframe-container {
734
- margin-top: 20px;
735
- }
736
- .model-output {
737
- background: #f8f9fa;
738
- padding: 15px;
739
- border-radius: 8px;
740
- border-left: 4px solid #6eb1ff;
741
- }
742
- .model-output h4 {
743
- margin-top: 0;
744
- color: #6eb1ff;
745
- }
746
- .option-controls {
747
- margin-top: 15px;
748
- display: flex;
749
- gap: 10px;
750
- }
751
- .cancel-btn {
752
- background: #f39c12 !important;
753
- color: white !important;
754
- }
755
- .cancel-btn:hover {
756
- background: #e67e22 !important;
757
- }
758
- .status-message {
759
- padding: 10px;
760
- border-radius: 4px;
761
- margin: 10px 0;
762
- }
763
- .status-info {
764
- background-color: #e3f2fd;
765
- border-left: 4px solid #2196f3;
766
- }
767
- .status-warning {
768
- background-color: #fff3e0;
769
- border-left: 4px solid #ff9800;
770
- }
771
- .status-error {
772
- background-color: #ffebee;
773
- border-left: 4px solid #f44336;
774
- }
775
- .status-success {
776
- background-color: #e8f5e9;
777
- border-left: 4px solid #4caf50;
778
- }
779
- /* Chat specific styles */
780
- .chat-container {
781
- display: flex;
782
- flex-direction: column;
783
- height: 500px;
784
- }
785
- .chat-messages {
786
- flex: 1;
787
- overflow-y: auto;
788
- padding: 15px;
789
- background: var(--light);
790
- border-radius: 6px;
791
- margin-bottom: 15px;
792
- display: flex;
793
- flex-direction: column;
794
- gap: 15px;
795
- }
796
- .message {
797
- display: flex;
798
- max-width: 80%;
799
- }
800
- .user-message {
801
- align-self: flex-end;
802
- }
803
- .bot-message {
804
- align-self: flex-start;
805
- }
806
- .message-content {
807
- padding: 12px 16px;
808
- border-radius: 18px;
809
- line-height: 1.4;
810
- }
811
- .user-message .message-content {
812
- background: var(--accent);
813
- color: white;
814
- border-bottom-right-radius: 4px;
815
- }
816
- .bot-message .message-content {
817
- background: var(--light-gray);
818
- color: var(--dark);
819
- border-bottom-left-radius: 4px;
820
- }
821
- .chat-input-container {
822
- display: flex;
823
- gap: 10px;
824
- }
825
- .chat-input-container textarea {
826
- flex: 1;
827
- padding: 12px;
828
- border: 1px solid var(--border);
829
- border-radius: 6px;
830
- resize: vertical;
831
- font-family: inherit;
832
- font-size: 14px;
833
- }
834
  """
 
835
 
836
- # --- 5. Gradio UI ---
837
- with gr.Blocks(theme=gr.themes.Glass(primary_hue="blue"),
838
- title="WaterScope-AI",
839
- fill_width=True,
840
- css=custom_css) as demo:
841
-
842
- # Custom Header with lighter blue
843
- with gr.Column(elem_classes="header"):
844
- gr.Markdown("WaterScope-AI")
845
- gr.Markdown("Domain-Specific Small Language Model for Water Sustainability (Demo)")
846
-
847
- # Navigation Bar
848
- with gr.Row(elem_classes="nav-bar"):
849
- gr.Button("Home", variant="secondary", size="sm")
850
- gr.Button("About", variant="secondary", size="sm")
851
- gr.Button("Documentation", variant="secondary", size="sm")
852
- gr.Button("Contact", variant="secondary", size="sm")
853
-
854
- # Create tabs for different functionalities
855
- with gr.Tabs():
856
- # MCQA Demo Tab
857
- with gr.TabItem("MCQA Demo"):
858
- # Status message area
859
- status_message = gr.HTML("", elem_classes="status-message")
860
-
861
- # Main content in a styled section
862
- with gr.Column(elem_classes="section"):
863
- # State for tracking number of visible options
864
- num_options_state = gr.State(4)
865
-
866
- # Top row with input and output panels
867
- with gr.Row():
868
- # Left panel with inputs
869
- with gr.Column(scale=1):
870
- with gr.Group():
871
- question_box = gr.Textbox(label="Question", lines=2, interactive=True)
872
-
873
- gr.Markdown("#### Options")
874
-
875
- # Create option boxes using a list (like in the working version)
876
- option_boxes = []
877
- for i in range(MAX_OPTIONS):
878
- option_boxes.append(gr.Textbox(
879
- label=f"Option {chr(ord('A') + i)}",
880
- visible=(i < 4),
881
- interactive=True
882
- ))
883
-
884
- with gr.Row():
885
- add_option_btn = gr.Button("Add Option")
886
- clear_btn = gr.Button("Clear")
887
- explanation_checkbox = gr.Checkbox(label="Generate Explanation", value=False)
888
-
889
- with gr.Row():
890
- run_btn = gr.Button("Run Comparison", variant="primary")
891
- cancel_btn = gr.Button("Cancel", variant="stop", visible=False, elem_classes="cancel-btn")
892
-
893
- # Right panel with outputs
894
- with gr.Column(scale=1):
895
- gr.Markdown("### Model Outputs")
896
- with gr.Row():
897
- with gr.Column(elem_classes="model-output"):
898
- gr.Markdown("#### Base Model")
899
- base_letter_box = gr.Textbox(label="Predicted Letter", interactive=False)
900
- base_raw_box = gr.Textbox(label="Raw Answer", lines=3, interactive=False)
901
- with gr.Column(elem_classes="model-output"):
902
- gr.Markdown("#### DA-IT Model")
903
- it_letter_box = gr.Textbox(label="Predicted Letter", interactive=False)
904
- it_raw_box = gr.Textbox(label="Raw Answer", lines=3, interactive=False)
905
- with gr.Column(elem_classes="model-output"):
906
- gr.Markdown("#### DA-DPO Model")
907
- dpo_letter_box = gr.Textbox(label="Predicted Letter", interactive=False)
908
- dpo_raw_box = gr.Textbox(label="Raw Answer", lines=3, interactive=False)
909
-
910
- # Table section with custom styling
911
- with gr.Column(elem_classes="section dataframe-container"):
912
- gr.Markdown("### Browse 777 MCQAs (click a row to autofill)")
913
- mcqa_table = gr.Dataframe(
914
- value=MCQA_DF.values.tolist(),
915
- headers=["Question", "A", "B", "C", "D"],
916
- datatype=["str"]*5,
917
- interactive=False,
918
- wrap=True,
919
- max_height=400
920
- )
921
-
922
- # Chat Tab
923
- with gr.TabItem("Chat (for testing)"):
924
- with gr.Column(elem_classes="section"):
925
- gr.Markdown("### Chat with DA-DPO Model")
926
-
927
- # Add status indicators for chat
928
- with gr.Row():
929
- with gr.Column(scale=1):
930
- gr.Markdown("**System Status:**")
931
- system_status = gr.Textbox(value="Ready", interactive=False)
932
- with gr.Column(scale=1):
933
- gr.Markdown("**API Status:**")
934
- api_status = gr.Textbox(value="Ready", interactive=False)
935
-
936
- # Chat interface
937
- chatbot = gr.Chatbot(
938
- label="Conversation",
939
- elem_classes="chat-messages",
940
- height=400
941
- )
942
-
943
- with gr.Row():
944
- msg = gr.Textbox(
945
- label="Your Message",
946
- placeholder="Type your message here...",
947
- lines=2,
948
- scale=4
949
- )
950
- send_btn = gr.Button("Send", variant="primary", scale=1)
951
-
952
- clear_chat = gr.Button("Clear Conversation")
953
-
954
- # Hidden API-only endpoint for base-model and dynamic-adapter testing.
955
- # Call with api_name="/agent_chat".
956
- agent_api_message = gr.Textbox(visible=False)
957
- agent_api_history = gr.JSON(value=[], visible=False)
958
- agent_api_adapter = gr.Dropdown(
959
- choices=["base", *AGENT_ADAPTER_PATHS.keys()],
960
- value="da_it",
961
  visible=False,
962
  )
963
- agent_api_system_prompt = gr.Textbox(
964
- value="",
 
965
  visible=False,
966
  )
967
- agent_api_max_tokens = gr.Number(value=1600, precision=0, visible=False)
968
- agent_api_return_diagnostics = gr.Checkbox(
969
- value=False,
970
  visible=False,
971
  )
972
- agent_api_output = gr.Textbox(visible=False)
973
- agent_api_trigger = gr.Button("Agent Chat API", visible=False)
974
-
975
- agent_api_trigger.click(
976
- fn=agent_chat_with_model,
977
- inputs=[
978
- agent_api_message,
979
- agent_api_history,
980
- agent_api_adapter,
981
- agent_api_system_prompt,
982
- agent_api_max_tokens,
983
- agent_api_return_diagnostics,
984
- ],
985
- outputs=agent_api_output,
986
- api_name="agent_chat",
987
  )
988
-
989
- # Footer
990
- with gr.Column(elem_classes="footer"):
991
- gr.Markdown("© 2025 WaterScope-AI | Built with Gradio")
992
-
993
- # Function to add more options
994
- def add_option(current_count):
995
- if current_count < MAX_OPTIONS:
996
- current_count += 1
997
- updates = [gr.update(visible=i < current_count) for i in range(MAX_OPTIONS)]
998
- return current_count, *updates
999
-
1000
- # Function to clear all inputs and outputs (from working version)
1001
- def clear_all():
1002
- """Clears all input and output fields."""
1003
- option_visibility_updates = [gr.update(visible=(i < 4), value="") for i in range(MAX_OPTIONS)]
1004
- return (
1005
- 4, # Reset state
1006
- "", # Clear question
1007
- *[""]*MAX_OPTIONS, # Clear option values
1008
- False, # Uncheck the explanation checkbox
1009
- "", "", "", "", "", "", # Clear all 6 output boxes
1010
- *option_visibility_updates # Apply visibility and value updates
1011
- )
1012
-
1013
- # Fixed function to load row data
1014
- def load_row(evt: gr.SelectData):
1015
- """Load a selected row from the dataframe into the input fields"""
1016
- if evt.index[0] >= len(MCQA_DF):
1017
- return ["", ""] + [""] * MAX_OPTIONS
1018
-
1019
- row = MCQA_DF.iloc[evt.index[0]]
1020
- # Return question and first 4 options (A-D), and empty for the rest
1021
- return_values = [
1022
- row["Question"] if pd.notna(row["Question"]) else "",
1023
- row["A"] if pd.notna(row["A"]) else "",
1024
- row["B"] if pd.notna(row["B"]) else "",
1025
- row["C"] if pd.notna(row["C"]) else "",
1026
- row["D"] if pd.notna(row["D"]) else ""
1027
- ]
1028
- # Add empty values for any additional options
1029
- return_values += [""] * (MAX_OPTIONS - 4)
1030
- return return_values
1031
-
1032
- # Function to toggle cancel button visibility
1033
- def toggle_cancel_button():
1034
- return gr.update(visible=True)
1035
-
1036
- # Function to hide cancel button
1037
- def hide_cancel_button():
1038
- return gr.update(visible=False)
1039
-
1040
- # Function to update status message
1041
- def update_status(message, type="info"):
1042
- if type == "info":
1043
- cls = "status-info"
1044
- elif type == "warning":
1045
- cls = "status-warning"
1046
- elif type == "error":
1047
- cls = "status-error"
1048
- elif type == "success":
1049
- cls = "status-success"
1050
- else:
1051
- cls = "status-info"
1052
-
1053
- return f'<div class="status-message {cls}">{message}</div>'
1054
-
1055
- # Connect the table selection event
1056
- mcqa_table.select(
1057
- fn=load_row,
1058
- inputs=None,
1059
- outputs=[question_box, *option_boxes]
1060
  )
1061
-
1062
- # Connect the add option button
1063
- add_option_btn.click(
1064
- fn=add_option,
1065
- inputs=[num_options_state],
1066
- outputs=[num_options_state, *option_boxes]
1067
  )
1068
-
1069
- # Define outputs to clear (from working version)
1070
- outputs_to_clear = [
1071
- num_options_state, question_box, *option_boxes, explanation_checkbox,
1072
- base_letter_box, base_raw_box, it_letter_box, it_raw_box, dpo_letter_box, dpo_raw_box,
1073
- *option_boxes
1074
- ]
1075
-
1076
- # Connect the clear button (from working version)
1077
- clear_btn.click(
1078
- fn=clear_all,
1079
- inputs=None,
1080
- outputs=outputs_to_clear,
1081
- queue=False
1082
- ).then(
1083
- fn=lambda: update_status("Form cleared", "info"),
1084
- inputs=None,
1085
- outputs=[status_message],
1086
- queue=False
1087
  )
1088
 
1089
- # Connect the run button
1090
- run_event = run_btn.click(
1091
- fn=lambda: update_status("Initializing processing...", "info"),
1092
- inputs=None,
1093
- outputs=[status_message],
1094
- queue=False
1095
- ).then(
1096
- fn=toggle_cancel_button,
1097
- inputs=None,
1098
- outputs=[cancel_btn],
1099
- queue=False
1100
- ).then(
1101
- fn=run_mcqa_comparison,
1102
- inputs=[question_box, *option_boxes, explanation_checkbox],
1103
- outputs=[base_letter_box, base_raw_box, it_letter_box, it_raw_box, dpo_letter_box, dpo_raw_box]
1104
- ).then(
1105
- fn=lambda: update_status("Processing completed successfully", "success"),
1106
- inputs=None,
1107
- outputs=[status_message],
1108
- queue=False
1109
- ).then(
1110
- fn=hide_cancel_button,
1111
- inputs=None,
1112
- outputs=[cancel_btn],
1113
- queue=False
1114
  )
1115
 
1116
- # Connect the cancel button
1117
- cancel_btn.click(
1118
- fn=cancel_processing,
1119
- inputs=None,
1120
- outputs=None,
1121
- queue=False
1122
- ).then(
1123
- fn=lambda: update_status("Processing cancelled by user", "warning"),
1124
- inputs=None,
1125
- outputs=[status_message],
1126
- queue=False
1127
- ).then(
1128
- fn=hide_cancel_button,
1129
- inputs=None,
1130
- outputs=[cancel_btn],
1131
- queue=False
1132
- )
1133
 
1134
- # Chat functionality
1135
- def respond(message, chat_history):
1136
- if not message.strip():
1137
- return "", chat_history
1138
-
1139
- # Update status
1140
- system_status.value = "Processing..."
1141
- api_status.value = "Sending request to model"
1142
-
1143
- try:
1144
- # Get response from the model
1145
- bot_message = chat_with_model(message, chat_history)
1146
- chat_history.append((message, bot_message))
1147
-
1148
- # Update status
1149
- system_status.value = "Ready"
1150
- api_status.value = "Response received"
1151
-
1152
- return "", chat_history
1153
- except Exception as e:
1154
- # Update status with error
1155
- system_status.value = "Error"
1156
- api_status.value = f"Error: {str(e)}"
1157
-
1158
- # Add error message to chat
1159
- error_msg = f"Sorry, I encountered an error: {str(e)}"
1160
- chat_history.append((message, error_msg))
1161
- return "", chat_history
1162
-
1163
- # Connect the chat send button
1164
- msg.submit(respond, [msg, chatbot], [msg, chatbot])
1165
- send_btn.click(respond, [msg, chatbot], [msg, chatbot])
1166
-
1167
- # Connect the clear chat button
1168
- def clear_chat_func():
1169
- system_status.value = "Ready"
1170
- api_status.value = "Ready"
1171
- return None
1172
-
1173
- clear_chat.click(clear_chat_func, None, chatbot, queue=False)
1174
-
1175
- # Pre-load the model when the app starts
1176
- print("Pre-loading models...")
1177
- load_model_and_tokenizer()
1178
- print("Models loaded successfully!")
1179
-
1180
- demo.queue().launch(debug=True)
 
 
 
 
 
1
  import threading
2
+ import time
3
+
4
  import gradio as gr
5
+ import torch
 
6
  from unsloth import FastLanguageModel
 
 
 
 
 
 
 
 
7
 
 
8
 
9
+ # ============================================================
10
+ # Configuration
11
+ # ============================================================
12
+ BASE_MODEL_ID = "unsloth/Meta-Llama-3.1-8B-Instruct"
13
+ LOAD_IN_4BIT = False
14
+
15
+ DEFAULT_SYSTEM_PROMPT = """
16
+ You are a scientific summarization assistant.
17
+
18
+ Summarize only the supplied document.
19
+ Do not continue the source text.
20
+ Do not invent facts, values, findings, or conclusions.
21
+ Do not reproduce numerical sequences or repetitive patterns.
22
+ Write one coherent summary of approximately 150–200 words.
23
+ When the summary is complete, stop immediately.
24
+ Begin with: "This study..."
25
+ """.strip()
26
 
 
 
27
 
28
+ # ============================================================
29
+ # Global model state
30
+ # ============================================================
31
  _model = None
32
  _tokenizer = None
33
+ _model_lock = threading.Lock()
 
 
34
 
35
 
36
+ # ============================================================
37
+ # Load a pristine base model only
38
+ # ============================================================
39
+ def load_base_model():
40
+ """
41
+ Load only the original Unsloth-hosted Meta Llama 3.1 8B Instruct model.
42
+
43
+ This diagnostic app:
44
+ - does not import PEFT;
45
+ - does not load any LoRA adapter;
46
+ - does not call PeftModel.from_pretrained();
47
+ - does not call load_adapter();
48
+ - does not call merge_and_unload().
49
  """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
50
  global _model, _tokenizer
 
 
51
 
52
+ if _model is not None and _tokenizer is not None:
 
53
  return _model, _tokenizer
 
 
 
 
 
 
 
54
 
55
+ with _model_lock:
56
+ if _model is not None and _tokenizer is not None:
57
+ return _model, _tokenizer
58
 
59
+ print("=" * 70)
60
+ print("LOADING PRISTINE BASE MODEL ONLY")
61
+ print(f"Model ID: {BASE_MODEL_ID}")
62
+ print(f"4-bit loading: {LOAD_IN_4BIT}")
63
+ print("No PEFT model and no LoRA adapters will be loaded.")
64
+ print("=" * 70)
65
 
66
+ started = time.time()
 
 
 
 
67
 
68
+ _model, _tokenizer = FastLanguageModel.from_pretrained(
69
+ model_name=BASE_MODEL_ID,
70
+ dtype=None,
71
+ load_in_4bit=LOAD_IN_4BIT,
72
+ device_map={"": 0},
73
+ )
 
 
 
 
74
 
75
+ FastLanguageModel.for_inference(_model)
76
+ _model.eval()
 
 
77
 
78
+ if _tokenizer.pad_token_id is None:
79
+ _tokenizer.pad_token = _tokenizer.eos_token
 
 
80
 
81
+ print(f"Loaded in {time.time() - started:.1f} seconds.")
82
+ print(f"Model class: {type(_model).__name__}")
83
+ print(f"Tokenizer class: {type(_tokenizer).__name__}")
84
+ print(f"PEFT configuration present: {hasattr(_model, 'peft_config')}")
85
+ print("PRISTINE BASE MODEL READY")
86
+ print("=" * 70)
87
 
88
+ return _model, _tokenizer
89
 
 
 
 
 
90
 
91
+ # ============================================================
92
+ # Message handling
93
+ # ============================================================
94
+ def normalize_history(chat_history):
95
+ """Convert common Gradio history formats into chat-template messages."""
96
+ messages = []
97
 
98
+ if not chat_history:
99
+ return messages
100
 
101
+ for item in chat_history:
102
+ if isinstance(item, dict):
103
+ role = item.get("role")
104
+ content = item.get("content")
105
 
106
+ if role in {"user", "assistant"} and content:
107
+ messages.append(
108
+ {"role": role, "content": str(content)}
109
+ )
110
 
111
+ elif isinstance(item, (list, tuple)) and len(item) >= 2:
112
+ user_message, assistant_message = item[0], item[1]
113
 
114
+ if user_message:
115
+ messages.append(
116
+ {"role": "user", "content": str(user_message)}
117
+ )
118
 
119
+ if assistant_message:
120
+ messages.append(
121
+ {"role": "assistant", "content": str(assistant_message)}
122
+ )
123
 
124
+ return messages
 
 
 
 
125
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
126
 
127
+ def tokenize_safely(chat_text, max_new_tokens):
128
+ """Tokenize without exceeding the model context window."""
129
+ model_limit = int(
130
+ getattr(_model.config, "max_position_embeddings", 131072)
 
131
  )
132
 
133
+ # Leave a small safety margin for special tokens.
134
+ max_input_tokens = max(
135
+ 1,
136
+ model_limit - int(max_new_tokens) - 32,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
137
  )
138
 
139
+ encoded = _tokenizer(
140
+ chat_text,
141
  return_tensors="pt",
142
  truncation=True,
143
+ max_length=max_input_tokens,
144
+ add_special_tokens=False,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
145
  )
146
 
147
+ encoded = {
148
+ key: value.to(_model.device)
149
+ for key, value in encoded.items()
150
+ if isinstance(value, torch.Tensor)
151
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
152
 
153
+ return encoded
154
 
155
 
156
+ # ============================================================
157
+ # Base-only API
158
+ # ============================================================
159
+ def base_chat(
160
  message,
161
  chat_history=None,
 
162
  system_prompt="",
163
+ max_new_tokens=300,
164
+ return_diagnostics=True,
165
  ):
166
  """
167
+ Generate directly from the pristine base model.
168
+
169
+ There is no adapter parameter because this app intentionally contains
170
+ no PEFT or LoRA code.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
171
  """
172
+ global _model, _tokenizer
173
 
174
  if _model is None or _tokenizer is None:
175
+ load_base_model()
176
 
177
  if not isinstance(message, str) or not message.strip():
178
  return "Please provide a non-empty message."
179
 
180
+ active_system_prompt = (
181
+ str(system_prompt).strip()
182
+ if system_prompt is not None and str(system_prompt).strip()
183
+ else DEFAULT_SYSTEM_PROMPT
184
+ )
 
 
 
 
185
 
186
  try:
187
  max_new_tokens = int(max_new_tokens)
188
  except (TypeError, ValueError):
189
+ max_new_tokens = 300
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
190
 
191
+ max_new_tokens = max(16, min(max_new_tokens, 4096))
 
 
 
 
192
 
193
+ messages = [
194
+ {"role": "system", "content": active_system_prompt},
195
+ *normalize_history(chat_history),
196
+ {"role": "user", "content": message.strip()},
197
+ ]
198
+
199
+ chat_text = _tokenizer.apply_chat_template(
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
200
  messages,
201
  tokenize=False,
202
  add_generation_prompt=True,
203
  )
204
 
205
+ inputs = tokenize_safely(
206
+ chat_text=chat_text,
207
+ max_new_tokens=max_new_tokens,
208
+ )
 
 
 
 
 
 
 
 
 
209
 
210
  diagnostics = {
211
+ "test_mode": "pristine_base_only",
212
  "configured_base_model": BASE_MODEL_ID,
213
  "model_class": type(_model).__name__,
214
+ "tokenizer_class": type(_tokenizer).__name__,
215
+ "peft_config_present": hasattr(_model, "peft_config"),
216
+ "adapter_methods_present": any(
217
+ hasattr(_model, method_name)
218
+ for method_name in (
219
+ "load_adapter",
220
+ "set_adapter",
221
+ "disable_adapter",
222
+ )
223
  ),
224
  "input_tokens": int(inputs["input_ids"].shape[1]),
225
+ "max_new_tokens": max_new_tokens,
226
  "max_position_embeddings": int(
227
  getattr(_model.config, "max_position_embeddings", -1)
228
  ),
229
+ "load_in_4bit": LOAD_IN_4BIT,
230
+ "do_sample": False,
231
+ "repetition_penalty": 1.15,
232
+ "no_repeat_ngram_size": 4,
233
  }
234
 
235
+ print("\n[BASE-ONLY DIAGNOSTICS]")
236
  for key, value in diagnostics.items():
237
  print(f"{key}: {value}")
238
+ print("[END BASE-ONLY DIAGNOSTICS]\n")
 
 
 
 
 
 
 
 
 
 
239
 
240
+ with _model_lock:
241
+ _model.eval()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
242
 
243
+ with torch.inference_mode():
244
+ outputs = _model.generate(
245
+ **inputs,
246
+ max_new_tokens=max_new_tokens,
247
+ do_sample=False,
248
+ repetition_penalty=1.15,
249
+ no_repeat_ngram_size=4,
250
+ eos_token_id=_tokenizer.eos_token_id,
251
+ pad_token_id=_tokenizer.pad_token_id,
252
+ use_cache=True,
253
  )
254
 
255
  input_length = inputs["input_ids"].shape[1]
256
+ generated_ids = outputs[0, input_length:]
257
+
258
  response = _tokenizer.decode(
259
  generated_ids,
260
  skip_special_tokens=True,
 
262
 
263
  if bool(return_diagnostics):
264
  diagnostic_text = "\n".join(
265
+ f"{key}: {value}"
266
+ for key, value in diagnostics.items()
267
  )
268
+
269
  return (
270
  "=== DIAGNOSTICS ===\n"
271
  f"{diagnostic_text}\n"
 
276
  return response
277
 
278
 
279
+ # ============================================================
280
+ # Minimal Gradio Space
281
+ # ============================================================
282
+ with gr.Blocks(title="WaterScope-AI Base Model Diagnostic") as demo:
283
+ gr.Markdown(
284
+ """
285
+ # WaterScope-AI: Pristine Base-Model Diagnostic
286
+
287
+ This temporary Space loads only
288
+ `unsloth/Meta-Llama-3.1-8B-Instruct`.
289
+
290
+ No PEFT model or LoRA adapter is loaded.
291
+ Use the hidden `/base_chat` API endpoint from your local Python code.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
292
  """
293
+ )
294
 
295
+ status = gr.Textbox(
296
+ value="Base-only diagnostic Space is ready.",
297
+ label="Status",
298
+ interactive=False,
299
+ )
300
+
301
+ # Hidden API components
302
+ api_message = gr.Textbox(
303
+ label="message",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
304
  visible=False,
305
  )
306
+ api_history = gr.JSON(
307
+ value=[],
308
+ label="chat_history",
309
  visible=False,
310
  )
311
+ api_system_prompt = gr.Textbox(
312
+ value="",
313
+ label="system_prompt",
314
  visible=False,
315
  )
316
+ api_max_tokens = gr.Number(
317
+ value=300,
318
+ precision=0,
319
+ label="max_new_tokens",
320
+ visible=False,
 
 
 
 
 
 
 
 
 
 
321
  )
322
+ api_return_diagnostics = gr.Checkbox(
323
+ value=True,
324
+ label="return_diagnostics",
325
+ visible=False,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
326
  )
327
+ api_output = gr.Textbox(
328
+ label="response",
329
+ visible=False,
 
 
 
330
  )
331
+ api_trigger = gr.Button(
332
+ "Base Chat API",
333
+ visible=False,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
334
  )
335
 
336
+ api_trigger.click(
337
+ fn=base_chat,
338
+ inputs=[
339
+ api_message,
340
+ api_history,
341
+ api_system_prompt,
342
+ api_max_tokens,
343
+ api_return_diagnostics,
344
+ ],
345
+ outputs=api_output,
346
+ api_name="base_chat",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
347
  )
348
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
349
 
350
+ print("Pre-loading the pristine base model...")
351
+ load_base_model()
352
+ print("Base-only diagnostic Space is ready.")
353
+
354
+ demo.queue().launch(debug=True)