amitashukla commited on
Commit
e169c4c
·
1 Parent(s): 6b71f6c

use merged finetuned Qwen 2.5 model

Browse files
Files changed (4) hide show
  1. app.py +8 -51
  2. requirements.txt +6 -5
  3. src/chat.py +74 -71
  4. src/config.py +5 -1
app.py CHANGED
@@ -1,16 +1,5 @@
1
  """
2
  Gradio Web Interface for Harbor Treatment Navigation Chatbot
3
-
4
- Landing page offers three paths:
5
- 1. Quick Recommendations — enter a zip code, get nearby options inline
6
- 2. Talk to a Human — compact crisis callout with phone number
7
- 3. Get Personalized Advice — leads to the AI chatbot
8
-
9
- Run locally:
10
- python app.py
11
-
12
- Access in browser:
13
- http://localhost:7860
14
  """
15
 
16
  import os
@@ -240,12 +229,10 @@ ZIPCODE_RE = re.compile(r"^\d{5}$")
240
 
241
 
242
  def is_valid_zip(zipcode: str) -> bool:
243
- """Return True if zipcode is exactly 5 digits."""
244
  return bool(ZIPCODE_RE.match(zipcode.strip()))
245
 
246
 
247
  def _load_resources_once():
248
- """Load resource CSVs once and cache."""
249
  if not hasattr(_load_resources_once, "_cache"):
250
  current_dir = os.path.dirname(os.path.abspath(__file__))
251
  paths = [
@@ -257,23 +244,14 @@ def _load_resources_once():
257
 
258
 
259
  def get_recommendations(zipcode: str) -> list[dict]:
260
- """
261
- Return a list of treatment recommendations for the given zip code.
262
-
263
- Uses the same filter/score logic as the chatbot, but with a minimal
264
- profile containing only the zipcode.
265
- """
266
  profile = create_empty_profile()
267
  profile["logistics"]["zipcode"] = zipcode.strip()
268
-
269
  resources = _load_resources_once()
270
  filtered = filter_resources(resources, profile)
271
- top = score_resources(filtered, profile)
272
- return top
273
 
274
 
275
  def format_recommendations(zipcode: str, results: list[dict]) -> str:
276
- """Render recommendations as an HTML snippet for display."""
277
  if not results:
278
  return (
279
  f"<div class='harbor-results'>"
@@ -287,12 +265,10 @@ def format_recommendations(zipcode: str, results: list[dict]) -> str:
287
  items_html = ""
288
  for r in results:
289
  name = r.get("name", "Unknown Facility")
290
- # Build address from parts
291
  addr_parts = [r.get("address", ""), r.get("city", ""),
292
  r.get("state", ""), r.get("zip", "")]
293
  address = ", ".join(p.strip() for p in addr_parts if p.strip())
294
  phone = r.get("phone", "").strip()
295
- # Type from primary_focus
296
  focus = r.get("primary_focus", "").strip()
297
  type_label = ", ".join(
298
  v.strip().replace("_", " ").title() for v in focus.split("|")
@@ -324,25 +300,13 @@ def format_recommendations(zipcode: str, results: list[dict]) -> str:
324
  # ── App ───────────────────────────────────────────────────────────────────────
325
 
326
  def create_chatbot():
327
- """Creates the Harbor interface with a landing page and chatbot."""
328
- _load_resources_once() # pre-load CSVs so first zip lookup is fast
329
  chatbot = Chatbot()
330
 
331
  def chat(message, history):
332
- """
333
- Generate a response for the current message.
334
-
335
- Args:
336
- message (str): The current message from the user
337
- history (list): List of previous [user, assistant] message pairs
338
-
339
- Returns:
340
- str: The assistant's response
341
- """
342
  return chatbot.get_response(message)
343
 
344
  def handle_zip_submit(zipcode: str):
345
- """Validate zip and return inline results HTML."""
346
  zipcode = zipcode.strip()
347
  if not is_valid_zip(zipcode):
348
  return gr.update(
@@ -350,15 +314,12 @@ def create_chatbot():
350
  visible=True,
351
  )
352
  results = get_recommendations(zipcode)
353
-
354
- # Log recommendations to console
355
  if results:
356
  print(f"[Harbor] Zip lookup ({zipcode}) — {len(results)} recommendation(s):")
357
  for i, r in enumerate(results, 1):
358
  print(f" {i}. {r.get('name', 'Unknown')} — {r.get('city', '')}, {r.get('state', '')} {r.get('zip', '')}")
359
  else:
360
  print(f"[Harbor] Zip lookup ({zipcode}) — no results found.")
361
-
362
  return gr.update(value=format_recommendations(zipcode, results), visible=True)
363
 
364
  def show_chat():
@@ -369,12 +330,10 @@ def create_chatbot():
369
 
370
  with gr.Blocks(title="Harbor", theme=THEME, css=CSS) as demo:
371
 
372
- # ── Landing Page ──────────────────────────────────────────────
373
  with gr.Column(visible=True) as landing_page:
374
  with gr.Column(elem_classes="harbor-wrap"):
375
  gr.HTML(HEADER_MD)
376
 
377
- # Card 1 — Quick Recommendations (featured)
378
  with gr.Group(elem_classes="harbor-card harbor-card-featured"):
379
  gr.HTML("<div class='harbor-card-title'>📍 Find Options Near You</div>")
380
  gr.HTML(
@@ -395,14 +354,10 @@ def create_chatbot():
395
  scale=1,
396
  elem_classes="harbor-zip-btn",
397
  )
398
- # Results rendered outside the card so the loading spinner
399
- # does not overlay the input card above.
400
  results_html = gr.HTML(visible=False, elem_id="zip-results")
401
 
402
- # Card 2 — Crisis callout (compact)
403
  gr.HTML(CRISIS_CALLOUT_HTML)
404
 
405
- # Card 3 — Chatbot
406
  with gr.Group(elem_classes="harbor-card"):
407
  gr.HTML(CHATBOT_CARD_MD)
408
  start_chat_btn = gr.Button(
@@ -414,7 +369,6 @@ def create_chatbot():
414
 
415
  gr.HTML(FOOTER_MD)
416
 
417
- # ── Chat Page ─────────────────────────────────────────────────
418
  with gr.Column(visible=False) as chat_page:
419
  with gr.Column(elem_classes="chat-header"):
420
  back_btn = gr.Button(
@@ -438,7 +392,6 @@ def create_chatbot():
438
  ],
439
  )
440
 
441
- # ── Events ────────────────────────────────────────────────────
442
  zip_btn.click(handle_zip_submit, inputs=zip_input, outputs=results_html)
443
  zip_input.submit(handle_zip_submit, inputs=zip_input, outputs=results_html)
444
  start_chat_btn.click(show_chat, outputs=[landing_page, chat_page])
@@ -448,5 +401,9 @@ def create_chatbot():
448
 
449
 
450
  if __name__ == "__main__":
451
- demo = create_chatbot()
452
- demo.launch()
 
 
 
 
 
1
  """
2
  Gradio Web Interface for Harbor Treatment Navigation Chatbot
 
 
 
 
 
 
 
 
 
 
 
3
  """
4
 
5
  import os
 
229
 
230
 
231
  def is_valid_zip(zipcode: str) -> bool:
 
232
  return bool(ZIPCODE_RE.match(zipcode.strip()))
233
 
234
 
235
  def _load_resources_once():
 
236
  if not hasattr(_load_resources_once, "_cache"):
237
  current_dir = os.path.dirname(os.path.abspath(__file__))
238
  paths = [
 
244
 
245
 
246
  def get_recommendations(zipcode: str) -> list[dict]:
 
 
 
 
 
 
247
  profile = create_empty_profile()
248
  profile["logistics"]["zipcode"] = zipcode.strip()
 
249
  resources = _load_resources_once()
250
  filtered = filter_resources(resources, profile)
251
+ return score_resources(filtered, profile)
 
252
 
253
 
254
  def format_recommendations(zipcode: str, results: list[dict]) -> str:
 
255
  if not results:
256
  return (
257
  f"<div class='harbor-results'>"
 
265
  items_html = ""
266
  for r in results:
267
  name = r.get("name", "Unknown Facility")
 
268
  addr_parts = [r.get("address", ""), r.get("city", ""),
269
  r.get("state", ""), r.get("zip", "")]
270
  address = ", ".join(p.strip() for p in addr_parts if p.strip())
271
  phone = r.get("phone", "").strip()
 
272
  focus = r.get("primary_focus", "").strip()
273
  type_label = ", ".join(
274
  v.strip().replace("_", " ").title() for v in focus.split("|")
 
300
  # ── App ───────────────────────────────────────────────────────────────────────
301
 
302
  def create_chatbot():
303
+ _load_resources_once()
 
304
  chatbot = Chatbot()
305
 
306
  def chat(message, history):
 
 
 
 
 
 
 
 
 
 
307
  return chatbot.get_response(message)
308
 
309
  def handle_zip_submit(zipcode: str):
 
310
  zipcode = zipcode.strip()
311
  if not is_valid_zip(zipcode):
312
  return gr.update(
 
314
  visible=True,
315
  )
316
  results = get_recommendations(zipcode)
 
 
317
  if results:
318
  print(f"[Harbor] Zip lookup ({zipcode}) — {len(results)} recommendation(s):")
319
  for i, r in enumerate(results, 1):
320
  print(f" {i}. {r.get('name', 'Unknown')} — {r.get('city', '')}, {r.get('state', '')} {r.get('zip', '')}")
321
  else:
322
  print(f"[Harbor] Zip lookup ({zipcode}) — no results found.")
 
323
  return gr.update(value=format_recommendations(zipcode, results), visible=True)
324
 
325
  def show_chat():
 
330
 
331
  with gr.Blocks(title="Harbor", theme=THEME, css=CSS) as demo:
332
 
 
333
  with gr.Column(visible=True) as landing_page:
334
  with gr.Column(elem_classes="harbor-wrap"):
335
  gr.HTML(HEADER_MD)
336
 
 
337
  with gr.Group(elem_classes="harbor-card harbor-card-featured"):
338
  gr.HTML("<div class='harbor-card-title'>📍 Find Options Near You</div>")
339
  gr.HTML(
 
354
  scale=1,
355
  elem_classes="harbor-zip-btn",
356
  )
 
 
357
  results_html = gr.HTML(visible=False, elem_id="zip-results")
358
 
 
359
  gr.HTML(CRISIS_CALLOUT_HTML)
360
 
 
361
  with gr.Group(elem_classes="harbor-card"):
362
  gr.HTML(CHATBOT_CARD_MD)
363
  start_chat_btn = gr.Button(
 
369
 
370
  gr.HTML(FOOTER_MD)
371
 
 
372
  with gr.Column(visible=False) as chat_page:
373
  with gr.Column(elem_classes="chat-header"):
374
  back_btn = gr.Button(
 
392
  ],
393
  )
394
 
 
395
  zip_btn.click(handle_zip_submit, inputs=zip_input, outputs=results_html)
396
  zip_input.submit(handle_zip_submit, inputs=zip_input, outputs=results_html)
397
  start_chat_btn.click(show_chat, outputs=[landing_page, chat_page])
 
401
 
402
 
403
  if __name__ == "__main__":
404
+ try:
405
+ demo = create_chatbot()
406
+ demo.launch()
407
+ except Exception as e:
408
+ import traceback
409
+ traceback.print_exc()
requirements.txt CHANGED
@@ -1,5 +1,6 @@
1
- gradio==5.23.3
2
- huggingface_hub
3
- python-dotenv
4
- pandas
5
- requests
 
 
1
+ gradio>=4.0.0
2
+ transformers>=5.0.0
3
+ torch>=2.0.0
4
+ accelerate>=0.26.0
5
+ huggingface_hub>=0.20.0
6
+ python-dotenv
src/chat.py CHANGED
@@ -1,121 +1,124 @@
1
- from huggingface_hub import InferenceClient
2
- from src.config import BASE_MODEL, MY_MODEL, HF_TOKEN
3
  import os
4
- from src.utils.profile import load_schema, create_empty_profile, extract_profile_updates, merge_profile, profile_to_summary
 
 
 
 
 
 
5
  from src.utils.resources import load_resources, filter_resources, score_resources, format_recommendations
6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7
  class Chatbot:
8
 
9
  def __init__(self):
10
- """
11
- Initialize the chatbot with a HF model ID
12
- """
13
- model_id = MY_MODEL if MY_MODEL else BASE_MODEL # define MY_MODEL in config.py if you create a new model in the HuggingFace Hub
14
- self.client = InferenceClient(model=model_id, token=HF_TOKEN)
15
- # Initialize user profile
16
  current_dir = os.path.dirname(os.path.abspath(__file__))
17
  data_dir = os.path.join(current_dir, '..', 'data')
18
  self.profile_schema = load_schema(os.path.join(data_dir, 'user_profile_schema.json'))
19
  self.user_profile = create_empty_profile()
20
- # Load treatment resources once
21
  knowledge_dir = os.path.join(data_dir, '..', 'references', 'knowledge')
22
- resources_paths = [
23
  os.path.join(knowledge_dir, 'ma_resources.csv'),
24
  os.path.join(knowledge_dir, 'boston_resources.csv'),
25
- ]
26
- self.resources = load_resources(resources_paths)
27
 
28
- def update_profile(self, user_input):
29
- """
30
- Scan user input for profile-relevant information and merge it
31
- into the running user profile.
32
-
33
- Args:
34
- user_input (str): The user's message text.
35
- """
36
  updates = extract_profile_updates(self.profile_schema, user_input)
37
  merge_profile(self.user_profile, updates)
38
 
39
- def format_prompt(self, user_input):
40
- """
41
- Format the user's input into a list of chat messages with system context.
42
- Updates the user profile with any new information detected.
43
-
44
- This method:
45
- 1. Loads system prompt from system_prompt.md
46
- 2. Updates user profile from schema-based keyword matching
47
- 3. Injects profile summary into the system prompt so the model knows what's been gathered
48
- 4. Returns a list of message dicts for the chat completion API
49
-
50
- Args:
51
- user_input (str): The user's question
52
-
53
- Returns:
54
- list[dict]: A list of message dicts with 'role' and 'content' keys
55
- """
56
- # Get the directory where this file is located
57
  current_dir = os.path.dirname(os.path.abspath(__file__))
58
-
59
- # Load system prompt
60
  system_prompt_path = os.path.join(current_dir, '../data/system_prompt.md')
61
  with open(system_prompt_path, 'r', encoding='utf-8') as f:
62
  system_prompt = f.read().strip()
63
 
64
- # Update user profile from this message
65
  self.update_profile(user_input)
66
-
67
- # Build profile summary for the prompt
68
  profile_summary = profile_to_summary(self.user_profile)
69
 
70
- # Build system message with profile context
71
  system_content = system_prompt
72
  if profile_summary:
73
- system_content = system_content + "\n\n" + profile_summary
74
 
75
- # Return structured messages for chat completion API
76
- messages = [
77
  {"role": "system", "content": system_content},
78
  {"role": "user", "content": user_input},
79
  ]
80
 
81
- return messages
82
-
83
- def get_response(self, user_input):
84
- """
85
- Generate a response to the user's question, with resource recommendations
86
- appended when the user profile contains enough information to match.
87
-
88
- Args:
89
- user_input (str): The user's question
90
-
91
- Returns:
92
- str: The chatbot's response, optionally followed by top 3 resources
93
- """
94
- # 1. Format messages (also updates profile)
95
  messages = self.format_prompt(user_input)
96
 
97
- # 2. Generate LLM response via chat completion API
98
- result = self.client.chat_completion(
99
- messages=messages,
100
- max_tokens=512,
 
 
 
 
101
  temperature=0.7,
 
 
102
  )
103
- response = result.choices[0].message.content.strip()
104
 
105
- # 3. Filter resources by profile, score, and append top 3
106
  filtered = filter_resources(self.resources, self.user_profile)
107
  top_resources = score_resources(filtered, self.user_profile)
108
  recommendations = format_recommendations(top_resources)
109
 
110
- # Log recommendations to console
111
  if top_resources:
112
- print(f"[Harbor] Chat recommendations ({len(top_resources)}) for profile:")
113
  for i, r in enumerate(top_resources, 1):
114
  print(f" {i}. {r.get('name', 'Unknown')} — {r.get('city', '')}, {r.get('state', '')} {r.get('zip', '')}")
115
  else:
116
  print("[Harbor] No recommendations matched current profile.")
117
 
118
  if recommendations:
119
- response = response + "\n\n" + recommendations
120
 
121
  return response
 
 
 
1
  import os
2
+ import torch
3
+ from transformers import AutoModelForCausalLM, AutoTokenizer, pipeline
4
+ from src.config import BASE_MODEL, MY_MODEL, LOCAL_MODEL, HF_TOKEN
5
+ from src.utils.profile import (
6
+ load_schema, create_empty_profile,
7
+ extract_profile_updates, merge_profile, profile_to_summary,
8
+ )
9
  from src.utils.resources import load_resources, filter_resources, score_resources, format_recommendations
10
 
11
+
12
+ def _load_pipeline(model_id: str):
13
+ """Load a text-generation pipeline, using the best available device."""
14
+ print(f"[Harbor] Loading model: {model_id}")
15
+ tokenizer = AutoTokenizer.from_pretrained(model_id, token=HF_TOKEN)
16
+
17
+ if torch.cuda.is_available():
18
+ dtype = torch.bfloat16
19
+ device_map = "auto"
20
+ device = None
21
+ device_label = "CUDA"
22
+ elif torch.backends.mps.is_available():
23
+ # bitsandbytes does not support MPS; float16 on 18 GB can OOM.
24
+ # Fall back to CPU with float32.
25
+ dtype = torch.float32
26
+ device_map = None
27
+ device = -1
28
+ device_label = "CPU"
29
+ else:
30
+ dtype = torch.float32
31
+ device_map = None
32
+ device = -1
33
+ device_label = "CPU"
34
+
35
+ model = AutoModelForCausalLM.from_pretrained(
36
+ model_id,
37
+ dtype=dtype,
38
+ device_map=device_map,
39
+ token=HF_TOKEN,
40
+ )
41
+
42
+ pipe = pipeline(
43
+ "text-generation",
44
+ model=model,
45
+ tokenizer=tokenizer,
46
+ device=device,
47
+ )
48
+ print(f"[Harbor] Model ready on {device_label}: {model_id}")
49
+ return pipe, tokenizer
50
+
51
+
52
  class Chatbot:
53
 
54
  def __init__(self):
55
+ # LOCAL_MODEL is used for local development to avoid OOM on 18 GB machines.
56
+ # On HF Spaces (CUDA), MY_MODEL (the merged finetuned model) is used.
57
+ model_id = LOCAL_MODEL or MY_MODEL or BASE_MODEL
58
+ self.pipe, self.tokenizer = _load_pipeline(model_id)
59
+
 
60
  current_dir = os.path.dirname(os.path.abspath(__file__))
61
  data_dir = os.path.join(current_dir, '..', 'data')
62
  self.profile_schema = load_schema(os.path.join(data_dir, 'user_profile_schema.json'))
63
  self.user_profile = create_empty_profile()
64
+
65
  knowledge_dir = os.path.join(data_dir, '..', 'references', 'knowledge')
66
+ self.resources = load_resources([
67
  os.path.join(knowledge_dir, 'ma_resources.csv'),
68
  os.path.join(knowledge_dir, 'boston_resources.csv'),
69
+ ])
 
70
 
71
+ def update_profile(self, user_input: str):
 
 
 
 
 
 
 
72
  updates = extract_profile_updates(self.profile_schema, user_input)
73
  merge_profile(self.user_profile, updates)
74
 
75
+ def format_prompt(self, user_input: str) -> list[dict]:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
76
  current_dir = os.path.dirname(os.path.abspath(__file__))
 
 
77
  system_prompt_path = os.path.join(current_dir, '../data/system_prompt.md')
78
  with open(system_prompt_path, 'r', encoding='utf-8') as f:
79
  system_prompt = f.read().strip()
80
 
 
81
  self.update_profile(user_input)
 
 
82
  profile_summary = profile_to_summary(self.user_profile)
83
 
 
84
  system_content = system_prompt
85
  if profile_summary:
86
+ system_content += "\n\n" + profile_summary
87
 
88
+ return [
 
89
  {"role": "system", "content": system_content},
90
  {"role": "user", "content": user_input},
91
  ]
92
 
93
+ def get_response(self, user_input: str) -> str:
 
 
 
 
 
 
 
 
 
 
 
 
 
94
  messages = self.format_prompt(user_input)
95
 
96
+ prompt = self.tokenizer.apply_chat_template(
97
+ messages,
98
+ tokenize=False,
99
+ add_generation_prompt=True,
100
+ )
101
+ output = self.pipe(
102
+ prompt,
103
+ max_new_tokens=512,
104
  temperature=0.7,
105
+ do_sample=True,
106
+ return_full_text=False,
107
  )
108
+ response = output[0]["generated_text"].strip()
109
 
 
110
  filtered = filter_resources(self.resources, self.user_profile)
111
  top_resources = score_resources(filtered, self.user_profile)
112
  recommendations = format_recommendations(top_resources)
113
 
 
114
  if top_resources:
115
+ print(f"[Harbor] {len(top_resources)} recommendation(s) for current profile:")
116
  for i, r in enumerate(top_resources, 1):
117
  print(f" {i}. {r.get('name', 'Unknown')} — {r.get('city', '')}, {r.get('state', '')} {r.get('zip', '')}")
118
  else:
119
  print("[Harbor] No recommendations matched current profile.")
120
 
121
  if recommendations:
122
+ response += "\n\n" + recommendations
123
 
124
  return response
src/config.py CHANGED
@@ -12,6 +12,10 @@ BASE_MODEL = "Qwen/Qwen2.5-7B-Instruct"
12
  # BASE_MODEL = "HuggingFaceH4/zephyr-7b-beta" # ungated
13
 
14
  # If you finetune the model or change it in any way, save it to huggingface hub, then set MY_MODEL to your model ID. The model ID is in the format "your-username/your-model-name".
15
- MY_MODEL = "" #"amitashukla/harbor-qwn25-lora"
 
 
 
 
16
 
17
  HF_TOKEN = os.getenv("HF_TOKEN")
 
12
  # BASE_MODEL = "HuggingFaceH4/zephyr-7b-beta" # ungated
13
 
14
  # If you finetune the model or change it in any way, save it to huggingface hub, then set MY_MODEL to your model ID. The model ID is in the format "your-username/your-model-name".
15
+ MY_MODEL = "amitashukla/harbor-qwn25-merged"
16
+
17
+ # Used locally to avoid OOM on 18 GB unified memory.
18
+ # Set to None (or remove) when deploying to HF Spaces.
19
+ LOCAL_MODEL = None #"Qwen/Qwen2.5-1.5B-Instruct"
20
 
21
  HF_TOKEN = os.getenv("HF_TOKEN")