Spaces:
Sleeping
Sleeping
Jae Eun Kim commited on
Commit ·
28af803
1
Parent(s): 5e88b4f
simple version
Browse files- .gitignore +3 -0
- app.py +389 -47
- requirements.txt +8 -0
.gitignore
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
venv
|
| 2 |
+
keys.txt
|
| 3 |
+
app_backup.py
|
app.py
CHANGED
|
@@ -1,63 +1,406 @@
|
|
| 1 |
import gradio as gr
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
""
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
|
| 19 |
-
|
|
|
|
|
|
|
|
|
|
| 20 |
|
| 21 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 22 |
|
| 23 |
-
|
|
|
|
| 24 |
|
| 25 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 26 |
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
|
| 39 |
-
|
| 40 |
-
|
| 41 |
|
| 42 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 43 |
"""
|
| 44 |
-
|
|
|
|
|
|
|
|
|
|
| 45 |
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 46 |
chatbot = gr.ChatInterface(
|
| 47 |
respond,
|
| 48 |
-
type="messages",
|
| 49 |
additional_inputs=[
|
| 50 |
-
gr.Textbox(value="You are a
|
| 51 |
-
gr.Slider(minimum=1, maximum=2048, value=512, step=1, label="Max new tokens"),
|
| 52 |
-
gr.Slider(minimum=0.1, maximum=4.0, value=0.7, step=0.1, label="Temperature"),
|
| 53 |
-
gr.Slider(
|
| 54 |
-
minimum=0.1,
|
| 55 |
-
maximum=1.0,
|
| 56 |
-
value=0.95,
|
| 57 |
-
step=0.05,
|
| 58 |
-
label="Top-p (nucleus sampling)",
|
| 59 |
-
),
|
| 60 |
],
|
|
|
|
|
|
|
| 61 |
)
|
| 62 |
|
| 63 |
with gr.Blocks() as demo:
|
|
@@ -65,6 +408,5 @@ with gr.Blocks() as demo:
|
|
| 65 |
gr.LoginButton()
|
| 66 |
chatbot.render()
|
| 67 |
|
| 68 |
-
|
| 69 |
if __name__ == "__main__":
|
| 70 |
-
demo.launch()
|
|
|
|
| 1 |
import gradio as gr
|
| 2 |
+
import pandas as pd
|
| 3 |
+
import numpy as np
|
| 4 |
+
import re
|
| 5 |
+
from transformers import pipeline, AutoTokenizer
|
| 6 |
+
from sentence_transformers import SentenceTransformer
|
| 7 |
+
import torch
|
| 8 |
+
import os
|
| 9 |
+
import spaces
|
| 10 |
+
|
| 11 |
+
# --- Global/Cached Variables ---
|
| 12 |
+
try:
|
| 13 |
+
# --- Load Data and Embeddings ---
|
| 14 |
+
sheet_id = "1hMsYgDQj3ymqwxUXA7R-ITITnw3HzeVZBxaXAjiJwAE"
|
| 15 |
+
sheet_gids = {
|
| 16 |
+
"Starting Point": "0",
|
| 17 |
+
"Immediate Help": "1278392561",
|
| 18 |
+
"Counselling": "713986636",
|
| 19 |
+
"Child/Youth Counselling": "1265113400",
|
| 20 |
+
"Parenting": "299805447",
|
| 21 |
+
"Safe Housing": "1571281149",
|
| 22 |
+
"Victim Rights Info": "1952909822",
|
| 23 |
+
"Legal Rep": "958128700",
|
| 24 |
+
"Legal Info": "1989315755",
|
| 25 |
+
"Grief": "2127423570"
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
all_dfs = []
|
| 29 |
+
for sheet_name, gid in sheet_gids.items():
|
| 30 |
+
url = f"https://docs.google.com/spreadsheets/d/{sheet_id}/export?format=csv&gid={gid}"
|
| 31 |
+
try:
|
| 32 |
+
df = pd.read_csv(url)
|
| 33 |
+
df['Source_Sheet'] = sheet_name
|
| 34 |
+
all_dfs.append(df)
|
| 35 |
+
except Exception as e:
|
| 36 |
+
print(f"Error reading {sheet_name}: {e}")
|
| 37 |
+
|
| 38 |
+
if all_dfs:
|
| 39 |
+
combined_df = pd.concat(all_dfs, ignore_index=True)
|
| 40 |
+
combined_df['Combined Description'] = combined_df['Relevant crime/incident'].astype(str) + '; ' + combined_df['Description'].astype(str)
|
| 41 |
+
print(f"DF COMBINED! {combined_df}")
|
| 42 |
+
else:
|
| 43 |
+
combined_df = pd.DataFrame()
|
| 44 |
+
print("WARNING: Dataframe is empty.")
|
| 45 |
|
| 46 |
+
# --- Load Embedding Model ---
|
| 47 |
+
print("Loading Embedding Model...")
|
| 48 |
+
embedding_model = SentenceTransformer('all-MiniLM-L6-v2')
|
| 49 |
+
print("Embedding Model LOADED!")
|
| 50 |
|
| 51 |
+
if not combined_df.empty:
|
| 52 |
+
text_to_embed_description = combined_df['Combined Description'].fillna('').astype(str).tolist()
|
| 53 |
+
embeddings_description = embedding_model.encode(text_to_embed_description)
|
| 54 |
+
combined_df['embeddings_description'] = list(embeddings_description)
|
| 55 |
+
print(f"DF UPDATED! {combined_df}")
|
| 56 |
|
| 57 |
+
else:
|
| 58 |
+
print("WARNING: Skipping embedding generation due to empty DataFrame.")
|
| 59 |
|
| 60 |
+
HF_AUTH_TOKEN = os.environ.get("HF_TOKEN")
|
| 61 |
+
|
| 62 |
+
print("Loading Llama Model...")
|
| 63 |
+
model_name = "meta-llama/Llama-2-7b-chat-hf"
|
| 64 |
+
print(f"llama model: {model_name}")
|
| 65 |
|
| 66 |
+
tokenizer = AutoTokenizer.from_pretrained(model_name, use_auth_token=HF_AUTH_TOKEN)
|
| 67 |
+
|
| 68 |
+
llm = pipeline(
|
| 69 |
+
"text-generation",
|
| 70 |
+
model=model_name,
|
| 71 |
+
tokenizer=tokenizer,
|
| 72 |
+
device_map="auto",
|
| 73 |
+
torch_dtype=torch.float16,
|
| 74 |
+
)
|
| 75 |
+
|
| 76 |
+
print("LLAMA loaded!!")
|
| 77 |
|
| 78 |
+
except Exception as e:
|
| 79 |
+
print(f"FATAL ERROR during model or data loading: {e}")
|
| 80 |
|
| 81 |
|
| 82 |
+
# --- Constants ---
|
| 83 |
+
DESC_THRESHOLD = 0.3 # this is for initializing the conversation. If the start of the conversation doesn't meet this, then the chatbot's keep asking for more questions.
|
| 84 |
+
FINAL_THRESHOLD = 0.4 # this is for filtering out the most relevant information.
|
| 85 |
+
N_DESC = 20
|
| 86 |
+
|
| 87 |
+
# --- Global Chat Context State ---
|
| 88 |
+
# These will be updated within chatbot_loop and persist across calls
|
| 89 |
+
LAST_KNOWN_INTENT = None
|
| 90 |
+
LAST_KNOWN_CITY = None
|
| 91 |
+
desc_results_df = None
|
| 92 |
+
|
| 93 |
+
SYSTEM_PROMPT = """
|
| 94 |
+
YOU ARE A TRAUMA-INFORMED, COMMUNITY-CONNECTED SUPPORT AGENT DESIGNED TO ASSIST INDIVIDUALS EXPERIENCING GENDER-BASED VIOLENCE IN BRITISH COLUMBIA, CANADA. YOUR PRIMARY OBJECTIVE IS TO GUIDE USERS THROUGH THREE CRITICAL TASKS: 1. COLLECT CONTEXT, 2. ENSURE SAFETY, 3. PROVIDE COMMUNITY RESOURCES. THIS AGENT MUST EXHIBIT COMPASSION, STRUCTURE, AND DISCRETION THROUGHOUT.
|
| 95 |
"""
|
| 96 |
+
|
| 97 |
+
SYSTEM_PROMPT_RAG = SYSTEM_PROMPT + """
|
| 98 |
+
\n
|
| 99 |
+
**RAG INSTRUCTIONS**: Use the following service information, delimited by triple backticks (```), to formulate your response. **Do not mention the RAG process or the triple backticks in your final answer**. If the service information is not relevant or not available, gently state that you couldn't find a direct resource and fall back to a general safety/support message.
|
| 100 |
"""
|
| 101 |
+
|
| 102 |
+
CITY_KEYWORDS = {
|
| 103 |
+
"new west": "New Westminster",
|
| 104 |
+
"new westminster": "New Westminster",
|
| 105 |
+
"surrey": "Surrey",
|
| 106 |
+
"vancouver": "Vancouver",
|
| 107 |
+
"downtown vancouver": "Vancouver",
|
| 108 |
+
"richmond": "Richmond",
|
| 109 |
+
"north van": "North Vancouver",
|
| 110 |
+
"north vancouver": "North Vancouver",
|
| 111 |
+
"burnaby": "Burnaby",
|
| 112 |
+
"west van": "West Vancouver",
|
| 113 |
+
"west vancouver": "West Vancouver",
|
| 114 |
+
"langley": "Langley",
|
| 115 |
+
"coquitlam": "Tri-Cities (Port Moody, Coquitlam, Port Coquitlam)",
|
| 116 |
+
"port moody": "Tri-Cities (Port Moody, Coquitlam, Port Coquitlam)",
|
| 117 |
+
"port coquitlam": "Tri-Cities (Port Moody, Coquitlam, Port Coquitlam)",
|
| 118 |
+
}
|
| 119 |
+
|
| 120 |
+
VALID_CITY_CATEGORIES = [
|
| 121 |
+
"New Westminster",
|
| 122 |
+
"Surrey",
|
| 123 |
+
"Vancouver",
|
| 124 |
+
"Richmond",
|
| 125 |
+
"North Vancouver",
|
| 126 |
+
"Burnaby",
|
| 127 |
+
"West Vancouver",
|
| 128 |
+
"Langley",
|
| 129 |
+
"Delta",
|
| 130 |
+
"White Rock",
|
| 131 |
+
"Tri-Cities (Port Moody, Coquitlam, Port Coquitlam)",
|
| 132 |
+
"Other cities in BC, Canada",
|
| 133 |
+
]
|
| 134 |
+
|
| 135 |
+
|
| 136 |
+
# --- Core RAG Functions ---
|
| 137 |
+
def retrieve_with_pandas_description(query, top_k=N_DESC):
|
| 138 |
+
print(f"I'm at retrieve_with_pandas_desc with {query}")
|
| 139 |
+
|
| 140 |
+
if combined_df.empty:
|
| 141 |
+
return pd.DataFrame()
|
| 142 |
+
query_embedding = embedding_model.encode([query])[0]
|
| 143 |
+
combined_df['similarity_desc'] = combined_df['embeddings_description'].apply(lambda x: np.dot(query_embedding, x) /
|
| 144 |
+
(np.linalg.norm(query_embedding) * np.linalg.norm(x)))
|
| 145 |
+
results = combined_df.sort_values(by="similarity_desc", ascending=False).head(top_k).copy()
|
| 146 |
+
return results
|
| 147 |
+
|
| 148 |
+
def is_query_only_cities(query):
|
| 149 |
+
# Normalize the query by removing common delimiters and whitespace
|
| 150 |
+
normalized_query = re.sub(r'[,\s]+', ' ', query.lower()).strip()
|
| 151 |
+
|
| 152 |
+
if not normalized_query:
|
| 153 |
+
return False
|
| 154 |
+
|
| 155 |
+
# Check if the normalized query is an exact match for one of the CITY_KEYWORDS keys
|
| 156 |
+
if normalized_query in CITY_KEYWORDS:
|
| 157 |
+
return True
|
| 158 |
+
|
| 159 |
+
# Check if the query is composed entirely of city keywords separated by spaces
|
| 160 |
+
# This handles "surrey burnaby" or "new west"
|
| 161 |
+
|
| 162 |
+
# Check for multi-word city keywords first (e.g., "new westminster")
|
| 163 |
+
for keyword in sorted(CITY_KEYWORDS.keys(), key=len, reverse=True):
|
| 164 |
+
if keyword in normalized_query:
|
| 165 |
+
# Remove the detected keyword from the string
|
| 166 |
+
normalized_query = normalized_query.replace(keyword, '').strip()
|
| 167 |
+
|
| 168 |
+
# After removing all city keywords, if the string is empty or contains only delimiters,
|
| 169 |
+
# it means the original query was only city names.
|
| 170 |
+
if not normalized_query:
|
| 171 |
+
return True
|
| 172 |
+
|
| 173 |
+
# Fallback check (less rigorous, but helps)
|
| 174 |
+
# Check if the remaining non-city parts contain any content words (excluding "and", "or", etc.)
|
| 175 |
+
non_city_words = re.sub(r'\b(and|or|in)\b', '', normalized_query).strip()
|
| 176 |
+
return not non_city_words
|
| 177 |
+
|
| 178 |
+
def remove_substrings_from_string(main_string, substrings_list):
|
| 179 |
+
"""
|
| 180 |
+
Removes city names and optional preceding prepositions (like 'in', 'at', 'for')
|
| 181 |
+
from the main string, case-insensitively, to isolate the intent.
|
| 182 |
+
"""
|
| 183 |
+
cleaned_string = main_string
|
| 184 |
+
|
| 185 |
+
# 1. Define the prepositions we want to optionally remove
|
| 186 |
+
prepositions = r'(?:\s*(?:in|at|for)\s+)?' # Matches optional ' in ', ' at ', ' for '
|
| 187 |
+
|
| 188 |
+
# Use a set of canonical cities for efficiency
|
| 189 |
+
canonical_cities = set(substrings_list)
|
| 190 |
+
|
| 191 |
+
for canonical in canonical_cities:
|
| 192 |
+
# Find all keywords associated with this canonical city (e.g., 'new west', 'new westminster')
|
| 193 |
+
keywords = [key for key, city in CITY_KEYWORDS.items() if city == canonical]
|
| 194 |
+
|
| 195 |
+
# Sort keywords by length in descending order to match multi-word names first
|
| 196 |
+
keywords.sort(key=len, reverse=True)
|
| 197 |
+
|
| 198 |
+
for keyword in keywords:
|
| 199 |
+
# Construct a robust regex pattern: [Prepositions]? [City Keyword]
|
| 200 |
+
# The '(\s*|$)'' at the end handles cases where the city is at the end of the sentence
|
| 201 |
+
pattern = rf'{prepositions}\b{re.escape(keyword)}\b(\s*|$)'
|
| 202 |
+
|
| 203 |
+
# Use sub to replace the matched pattern (including the optional preposition) with a single space
|
| 204 |
+
# flags=re.IGNORECASE ensures case-insensitive matching
|
| 205 |
+
cleaned_string = re.sub(pattern, ' ', cleaned_string, flags=re.IGNORECASE)
|
| 206 |
+
|
| 207 |
+
# 2. Clean up resulting extra spaces, commas, and strip leading/trailing whitespace
|
| 208 |
+
cleaned_string = re.sub(r'[\s,]+', ' ', cleaned_string).strip()
|
| 209 |
+
|
| 210 |
+
return cleaned_string
|
| 211 |
+
|
| 212 |
+
def get_df_filtered_by_desc(query):
|
| 213 |
+
print(f"[DEBUG] get_df_filtered_by_desc with query: '{query}'")
|
| 214 |
+
|
| 215 |
+
return retrieve_with_pandas_description(query, top_k=N_DESC)
|
| 216 |
+
|
| 217 |
+
def detect_city_from_query(query):
|
| 218 |
+
print(f"detect_city_from_query with {query}")
|
| 219 |
+
text = query.lower()
|
| 220 |
+
detected = []
|
| 221 |
+
for keyword, canonical in CITY_KEYWORDS.items():
|
| 222 |
+
if re.search(rf"\b{re.escape(keyword.lower())}\b", text):
|
| 223 |
+
detected.append(canonical)
|
| 224 |
+
print(f"found city: {detected}")
|
| 225 |
+
return detected
|
| 226 |
+
|
| 227 |
+
def get_df_filtered_by_general_city(city_context, desc_results_df):
|
| 228 |
+
print(f"[STEP] I'm at get_df_filtered_by_general_city with query: {city_context}.")
|
| 229 |
+
print(f"[DEBUG] As of now desc_df is = {desc_results_df}")
|
| 230 |
+
|
| 231 |
+
pattern = r'\b(?:' + '|'.join(re.escape(c) for c in city_context) + r')\b'
|
| 232 |
+
general_city_filtered_df = desc_results_df[desc_results_df["City"].str.contains(pattern, case=False, na=False, regex=True)]
|
| 233 |
+
print(f"[DEBUG] FILTERED by GENERAL city! General_city_filtered_df is = {general_city_filtered_df}")
|
| 234 |
+
|
| 235 |
+
return city_context, general_city_filtered_df
|
| 236 |
+
|
| 237 |
+
def get_df_filtered_by_service_city(city_context, general_city_filtered_df):
|
| 238 |
+
print(f"[STEP] I'm at get_df_filtered_by_SERVICE_city with: '{city_context}'")
|
| 239 |
+
|
| 240 |
+
pattern = r'\b(?:' + '|'.join(re.escape(c) for c in city_context) + r')\b'
|
| 241 |
+
main_city_filtered_df = general_city_filtered_df[general_city_filtered_df["Main Service City"].str.contains(pattern, case=False, na=False, regex=True)]
|
| 242 |
+
print(f"[DEBUG] FILTERED by MAIN city! Now, main_city_filtered_df is = {main_city_filtered_df} THIS WILL FEED THE FINAL RESULTS!!")
|
| 243 |
+
|
| 244 |
+
return main_city_filtered_df if not main_city_filtered_df.empty else general_city_filtered_df
|
| 245 |
+
|
| 246 |
+
@spaces.GPU(duration=250)
|
| 247 |
+
def llm_generate_response(prompt):
|
| 248 |
+
prompt_template = f"<s>[INST] {prompt} [/INST]"
|
| 249 |
+
try:
|
| 250 |
+
response = llm(
|
| 251 |
+
prompt_template,
|
| 252 |
+
max_new_tokens=512,
|
| 253 |
+
do_sample=True,
|
| 254 |
+
temperature=0.7,
|
| 255 |
+
top_p=0.95,
|
| 256 |
+
num_return_sequences=1,
|
| 257 |
+
eos_token_id=tokenizer.eos_token_id,
|
| 258 |
+
)
|
| 259 |
+
generated_text = response[0]['generated_text']
|
| 260 |
+
response_text = generated_text.split("[/INST]")[-1].strip()
|
| 261 |
+
return response_text
|
| 262 |
+
except Exception as e:
|
| 263 |
+
print(f"LLM generation error: {e}")
|
| 264 |
+
return "I encountered an error generating a response. Please try again."
|
| 265 |
+
|
| 266 |
+
|
| 267 |
+
def generate_resources(FINAL_FILTERED):
|
| 268 |
+
print(f"[STEP] I'm at generate_resources with df: {FINAL_FILTERED}")
|
| 269 |
+
|
| 270 |
+
if FINAL_FILTERED.empty or FINAL_FILTERED['similarity_desc'].max() < FINAL_THRESHOLD:
|
| 271 |
+
return "No highly relevant services were found. However, VictimLinkBC will be a good start for you."
|
| 272 |
+
|
| 273 |
+
context_list = []
|
| 274 |
+
print(f"[STEP] I'm at generate_resources with df. Creating context_list!")
|
| 275 |
+
for _, row in FINAL_FILTERED.iterrows():
|
| 276 |
+
|
| 277 |
+
phone = row['Phone #']
|
| 278 |
+
email = row['Email']
|
| 279 |
+
website = row['Website']
|
| 280 |
+
|
| 281 |
+
# Use pd.isna() or np.isnan() to check for missing values (NaN)
|
| 282 |
+
# If the value is missing, use 'N/A', otherwise use the value.
|
| 283 |
+
phone_val = 'N/A' if pd.isna(phone) else phone
|
| 284 |
+
email_val = 'N/A' if pd.isna(email) else email
|
| 285 |
+
website_val = 'N/A' if pd.isna(website) else website
|
| 286 |
+
|
| 287 |
+
context_entry = (
|
| 288 |
+
f"Organization Name: {row['Title']}\n"
|
| 289 |
+
f"{row['Description']}\n"
|
| 290 |
+
f"📞: {phone_val}, 📧: {email_val}, 🌐: {website_val}\n"
|
| 291 |
+
"---"
|
| 292 |
+
)
|
| 293 |
+
context_list.append(context_entry)
|
| 294 |
+
print(f"[STEP] I'm at generate_resources with CONTEXT_LIST []: {context_entry}")
|
| 295 |
+
|
| 296 |
+
return "\n\n".join(context_list)
|
| 297 |
+
|
| 298 |
+
|
| 299 |
+
def chatbot_loop(query, history):
|
| 300 |
+
global LAST_KNOWN_INTENT, LAST_KNOWN_CITY
|
| 301 |
+
global desc_results_df
|
| 302 |
+
|
| 303 |
+
final_query=query
|
| 304 |
+
city_context = detect_city_from_query(query)
|
| 305 |
+
is_first_interaction = not history
|
| 306 |
+
|
| 307 |
+
if is_query_only_cities(query):
|
| 308 |
+
# This block executes if city_context IS in CITY_KEYWORDS
|
| 309 |
+
print(f"Skipping similarity check because query is a city name itself ('{city_context}') ")
|
| 310 |
+
|
| 311 |
+
if LAST_KNOWN_INTENT and city_context:
|
| 312 |
+
LAST_KNOWN_CITY = city_context
|
| 313 |
+
print(f"desc_results_df is retained?????????? {desc_results_df}")
|
| 314 |
+
# Case: User previously gave intent, now they gave the city. Proceed to RAG.
|
| 315 |
+
print(f"[DEBUG] We've LAST_KNOWN_INTENT: '{LAST_KNOWN_INTENT}'. New city input: '{LAST_KNOWN_CITY}' as city_context.")
|
| 316 |
+
final_query = f"{LAST_KNOWN_INTENT} in {query}"
|
| 317 |
+
print(f"[DEBUG] FINAL QUERY created: '{final_query}'.")
|
| 318 |
+
LAST_KNOWN_INTENT = None # Clear the saved intent as we have the full context
|
| 319 |
+
# Skip the rest of this 'if' block and let RAG run below with effective_query
|
| 320 |
+
print(f"[DEBUG] Combined final_query: '{final_query}'. This is the ideal outcome, first get help areas then city info.")
|
| 321 |
+
else:
|
| 322 |
+
# Case: User gave city first, or gave city again. Ask for intent.
|
| 323 |
+
print(f"if the user gave the city first...")
|
| 324 |
+
LAST_KNOWN_CITY = query
|
| 325 |
+
return "Thank you for letting me know the city. Which areas do you need help with? For example: counselling, safe housing, or legal information?"
|
| 326 |
+
else:
|
| 327 |
+
# this is when query contains intent.
|
| 328 |
+
desc_results_df = get_df_filtered_by_desc(query)
|
| 329 |
+
# RETRIEVAL: Print similarity scores
|
| 330 |
+
print("--- Stage 1: Description Similarity Scores ---")
|
| 331 |
+
print(desc_results_df[['Title', 'similarity_desc']].head())
|
| 332 |
+
print(f"Max Similarity: {desc_results_df['similarity_desc'].max():.4f}")
|
| 333 |
+
print(f"Min Similarity: {desc_results_df['similarity_desc'].min():.4f}\n")
|
| 334 |
+
|
| 335 |
+
|
| 336 |
+
if desc_results_df.empty or desc_results_df.get('similarity_desc', pd.Series([-1])).max() < DESC_THRESHOLD:
|
| 337 |
+
if is_first_interaction:
|
| 338 |
+
return "Hello, I am happy that you have found me. My name is One Tap Away, designed for gender-based violence support services resources. Please note that my answer is only restricted to Metro Vancouver, BC! \n Could you please let me know what areas you need help for, and for what city?"
|
| 339 |
+
else:
|
| 340 |
+
return "I am sorry, could you please elaborate more on what areas you'd like help with?"
|
| 341 |
+
|
| 342 |
+
# --- Context Handling Logic ---
|
| 343 |
+
# effective_query = query
|
| 344 |
+
|
| 345 |
+
if not city_context:
|
| 346 |
+
# No city found, and no saved intent. This query becomes the new intent.
|
| 347 |
+
print(f"[DEBUG] no city_context with: {query}. Saving the query: '{query}' as INTENT. This is potentially when the user just provided help areas without city info.")
|
| 348 |
+
LAST_KNOWN_INTENT = query
|
| 349 |
+
return "Which city are you looking for services in? Vancouver, Surrey, Burnaby, Richmond, Langley, Coquitlam, Port Moody, Port Coquitlam, West Vancouver, North Vancouver, White Rock, Delta, Others?"
|
| 350 |
+
else:
|
| 351 |
+
# THIS IS WHEN city_context's detected, but the query's also NOT just city.
|
| 352 |
+
# That means city's detected in the current query (e.g., "counselling in surrey")
|
| 353 |
+
print(f"[DEBUG] I got BOTH query and city! Saving the query: '{final_query}' as FINAL_QUERY. This was done earlier even before 'if is_query_only_cities'")
|
| 354 |
+
LAST_KNOWN_CITY = city_context
|
| 355 |
+
print(f"LAST_KNOWN_CITY IS SET!: '{LAST_KNOWN_CITY}'")
|
| 356 |
+
LAST_KNOWN_INTENT = remove_substrings_from_string(query, city_context)
|
| 357 |
+
print(f"LAST_KNOWN_INTENT IS SET! '{LAST_KNOWN_INTENT}'")
|
| 358 |
+
|
| 359 |
+
# If we reach here, we have a city_context and an effective_query
|
| 360 |
+
# Re-run the RAG description retrieval with the effective_query
|
| 361 |
+
desc_results_df = get_df_filtered_by_desc(LAST_KNOWN_INTENT)
|
| 362 |
+
|
| 363 |
+
|
| 364 |
+
detected_cities, general_city_df = get_df_filtered_by_general_city(LAST_KNOWN_CITY, desc_results_df)
|
| 365 |
+
final_df = get_df_filtered_by_service_city(detected_cities, general_city_df)
|
| 366 |
+
# rag_context_text = generate_resources(final_df)
|
| 367 |
+
|
| 368 |
+
final_statement = "Thank you for the information. Here is some relevant information for you:\n\n"
|
| 369 |
+
final_output = generate_resources(final_df)
|
| 370 |
+
final_resource = "Also, I would like to highlight that VictimLink BC could be a good start for you. \n VictimLink BC is a toll-free, confidential and multilingual services available across B.C. and the Yukon. VictimLinkBC provides information and referral services to call victims of crime and immediate crisis support to victims of family and sexual violence, victims of human trafficking and sexual services. \n https://victimlinkbc.ca/ \n 1-800-563-0808 \n 211-victimlinkbc@uwbc.ca "
|
| 371 |
+
|
| 372 |
+
# combined_prompt = (
|
| 373 |
+
# f"{SYSTEM_PROMPT_RAG}\n"
|
| 374 |
+
# f"The user's request is: **{query}**.\n"
|
| 375 |
+
# f"Relevant service information:\n```\n{rag_context_text}\n```\n"
|
| 376 |
+
# f"```\n{final_statement}\n```\n"
|
| 377 |
+
# f"Generate a compassionate and informative response."
|
| 378 |
+
# )
|
| 379 |
+
LAST_KNOWN_INTENT = None
|
| 380 |
+
LAST_KNOWN_CITY = None
|
| 381 |
+
print(f"Wiping intent & city. Intent: '{LAST_KNOWN_INTENT}' City: '{LAST_KNOWN_CITY}'")
|
| 382 |
+
|
| 383 |
+
return final_statement + final_output + final_resource # used to be llm_generate_response(combined_prompt)
|
| 384 |
+
|
| 385 |
+
|
| 386 |
+
def respond(message, history, system_message, max_tokens, temperature, top_p):
|
| 387 |
+
|
| 388 |
+
llm_response = chatbot_loop(message, history)
|
| 389 |
+
|
| 390 |
+
# 2. Yield the complete string once, and let the function end.
|
| 391 |
+
yield llm_response
|
| 392 |
+
# DO NOT use `return llm_response` after the yield.
|
| 393 |
+
|
| 394 |
chatbot = gr.ChatInterface(
|
| 395 |
respond,
|
|
|
|
| 396 |
additional_inputs=[
|
| 397 |
+
gr.Textbox(value="You are a trauma-informed support agent for GBV in BC.", label="System message", visible=False),
|
| 398 |
+
gr.Slider(minimum=1, maximum=2048, value=512, step=1, label="Max new tokens", visible=False),
|
| 399 |
+
gr.Slider(minimum=0.1, maximum=4.0, value=0.7, step=0.1, label="Temperature", visible=False),
|
| 400 |
+
gr.Slider(minimum=0.1, maximum=1.0, value=0.95, step=0.05, label="Top-p", visible=False),
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 401 |
],
|
| 402 |
+
title="One Tap Away Chatbot",
|
| 403 |
+
theme="soft",
|
| 404 |
)
|
| 405 |
|
| 406 |
with gr.Blocks() as demo:
|
|
|
|
| 408 |
gr.LoginButton()
|
| 409 |
chatbot.render()
|
| 410 |
|
|
|
|
| 411 |
if __name__ == "__main__":
|
| 412 |
+
demo.launch(share=True)
|
requirements.txt
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
gradio
|
| 2 |
+
pandas
|
| 3 |
+
numpy
|
| 4 |
+
torch
|
| 5 |
+
transformers
|
| 6 |
+
accelerate
|
| 7 |
+
sentence-transformers
|
| 8 |
+
spaces
|