Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -1,368 +1,266 @@
|
|
|
|
|
| 1 |
import gradio as gr
|
| 2 |
from huggingface_hub import InferenceClient
|
| 3 |
|
| 4 |
-
|
| 5 |
-
primary_hue=gr.themes.Color(c100="#f5f5f3", c200="#fbfaf8", c300="#f9f9f9", c400="#eee9ee", c50="rgba(255, 255, 255, 1)", c500="rgba(0, 0, 0, 1)", c600="rgba(26.934374999999992, 26.934374999999992, 26.934374999999992, 1)", c700="rgba(10.943750000000012, 10.943750000000012, 10.943750000000012, 1)", c800="rgba(17.053125000000005, 17.053125000000005, 17.053125000000005, 1)", c900="#fffefe", c950="#fffefe"),
|
| 6 |
-
secondary_hue=gr.themes.Color(c100="#f29c74", c200="#f4b7a8", c300="#fffefe", c400="#fffefe", c50="#e46e45", c500="#fffefe", c600="#fffefe", c700="#fffefe", c800="#fffefe", c900="#fffefe", c950="#fffefe"),
|
| 7 |
-
neutral_hue=gr.themes.Color(c100="#6b8ea8", c200="#a4c5e8", c300="rgba(0, 0, 0, 1)", c400="rgba(0, 0, 0, 1)", c50="#284566", c500="rgba(0, 0, 0, 1)", c600="rgba(0, 0, 0, 1)", c700="rgba(0, 0, 0, 1)", c800="rgba(0, 0, 0, 1)", c900="rgba(0, 0, 0, 1)", c950="rgba(0, 0, 0, 1)"),
|
| 8 |
-
font=[gr.themes.GoogleFont('open sans'), 'ui-sans-serif', 'system-ui', 'sans-serif'],
|
| 9 |
-
).set(
|
| 10 |
-
body_background_fill='*primary_50',
|
| 11 |
-
body_background_fill_dark='*primary_700',
|
| 12 |
-
body_text_color='*primary_500',
|
| 13 |
-
body_text_color_dark='*primary_400',
|
| 14 |
-
body_text_color_subdued='*neutral_50',
|
| 15 |
-
body_text_color_subdued_dark='*primary_300',
|
| 16 |
-
background_fill_primary='*primary_400',
|
| 17 |
-
background_fill_primary_dark='*neutral_100',
|
| 18 |
-
background_fill_secondary='*primary_400',
|
| 19 |
-
background_fill_secondary_dark='*neutral_50',
|
| 20 |
-
border_color_accent_dark='*secondary_50',
|
| 21 |
-
border_color_accent_subdued_dark='*primary_50',
|
| 22 |
-
border_color_primary='*neutral_50',
|
| 23 |
-
border_color_primary_dark='*neutral_100',
|
| 24 |
-
color_accent='*primary_50',
|
| 25 |
-
color_accent_soft_dark='*neutral_100',
|
| 26 |
-
block_background_fill_dark='*neutral_50',
|
| 27 |
-
checkbox_background_color='*primary_400',
|
| 28 |
-
checkbox_background_color_selected='*primary_700',
|
| 29 |
-
checkbox_background_color_selected_dark='*neutral_50',
|
| 30 |
-
checkbox_label_border_color_dark='*neutral_50',
|
| 31 |
-
input_background_fill='*primary_50',
|
| 32 |
-
input_background_fill_dark='*neutral_100',
|
| 33 |
-
input_border_color='*neutral_50',
|
| 34 |
-
input_border_color_dark='*neutral_50',
|
| 35 |
-
input_border_width='2px',
|
| 36 |
-
table_odd_background_fill='*neutral_100',
|
| 37 |
-
button_primary_background_fill='*neutral_50',
|
| 38 |
-
button_primary_background_fill_dark='*neutral_100',
|
| 39 |
-
button_primary_background_fill_hover='*neutral_100'
|
| 40 |
-
)
|
| 41 |
-
|
| 42 |
-
#STEP 1 FROM SEMANTIC SEARCH
|
| 43 |
from sentence_transformers import SentenceTransformer
|
| 44 |
import torch
|
| 45 |
|
| 46 |
-
#
|
| 47 |
-
#
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
with open("
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
#STEP 3
|
| 62 |
def preprocess_text(text):
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
cleaned_chunks_food = preprocess_text(food_text)
|
| 81 |
-
|
| 82 |
-
#STEP 4 FROM SEMANTIC SEARCH
|
| 83 |
-
# Load the pre-trained embedding model that converts text to vectors
|
| 84 |
-
model = SentenceTransformer('all-MiniLM-L6-v2')
|
| 85 |
|
| 86 |
def create_embeddings(text_chunks):
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
# Normalize the query embedding to unit length for accurate similarity comparison
|
| 103 |
-
query_embedding_normalized = query_embedding / query_embedding.norm()
|
| 104 |
-
# Normalize all chunk embeddings to unit length for consistent comparison
|
| 105 |
-
chunk_embeddings_normalized = chunk_embeddings / chunk_embeddings.norm(dim=1, keepdim=True)
|
| 106 |
-
# Calculate cosine similarity between query and all chunks using matrix multiplication
|
| 107 |
-
similarities = torch.matmul(chunk_embeddings_normalized, query_embedding_normalized) # Complete this line
|
| 108 |
-
# Print the similarities
|
| 109 |
-
#print(similarities)
|
| 110 |
-
# Find the indices of the 3 chunks with highest similarity scores
|
| 111 |
-
top_indices = torch.topk(similarities, k=3).indices
|
| 112 |
-
# Print the top indices
|
| 113 |
-
#print(top_indices)
|
| 114 |
-
# Create an empty list to store the most relevant chunks
|
| 115 |
-
top_chunks = []
|
| 116 |
-
# Loop through the top indices and retrieve the corresponding text chunks
|
| 117 |
-
for top_index in top_indices:
|
| 118 |
-
top_chunks.append(text_chunks[top_index])
|
| 119 |
-
# Return the list of most relevant chunks
|
| 120 |
-
return top_chunks
|
| 121 |
-
|
| 122 |
-
#STEP 6 FROM SEMANTIC SEARCH
|
| 123 |
-
|
| 124 |
-
client = InferenceClient("Qwen/Qwen2.5-72B-Instruct")
|
| 125 |
-
|
| 126 |
-
def respond(message, history, language, chatbot_mode, destinations, trip_length, trip_unit, trip_season, luggage_types, luggage_size, food_prefs, activity):
|
| 127 |
-
destinations = destinations or []
|
| 128 |
-
trip_length = trip_length or "Not specified"
|
| 129 |
-
trip_unit = trip_unit or ""
|
| 130 |
-
trip_season = trip_season or "Not specified"
|
| 131 |
-
luggage_types = luggage_types or []
|
| 132 |
-
luggage_size = luggage_size or "Not specified"
|
| 133 |
-
food_prefs = food_prefs or []
|
| 134 |
-
activity = activity or []
|
| 135 |
-
language = language or "English"
|
| 136 |
-
|
| 137 |
-
#conduct a semantic search for each of our files
|
| 138 |
-
top_weather = get_top_chunks(message, chunk_embeddings_weather, cleaned_chunks_weather)
|
| 139 |
-
top_luggage = get_top_chunks(message, chunk_embeddings_luggage, cleaned_chunks_luggage)
|
| 140 |
-
top_attraction = get_top_chunks(message, chunk_embeddings_attraction, cleaned_chunks_attraction)
|
| 141 |
-
top_food = get_top_chunks(message, chunk_embeddings_food, cleaned_chunks_food)
|
| 142 |
-
|
| 143 |
-
str_top_weather = "\n".join(top_weather)
|
| 144 |
-
str_top_luggage = "\n".join(top_luggage)
|
| 145 |
-
str_top_attraction = "\n".join(top_attraction)
|
| 146 |
-
str_top_food = "\n".join(top_food)
|
| 147 |
-
|
| 148 |
-
#collect inputed data from the sidebar elements
|
| 149 |
-
ctx = (
|
| 150 |
-
f"Language: {language}"
|
| 151 |
-
f"Destination: {', '.join(destinations) if destinations else 'Not specified'}\n"
|
| 152 |
-
f"Trip Length: {trip_length} {trip_unit}\n"
|
| 153 |
-
f"Trip Season: {trip_season}\n"
|
| 154 |
-
f"Luggage: {', '.join(luggage_types) if luggage_types else 'Not specified'}, "
|
| 155 |
-
f"Size: {luggage_size}L\n"
|
| 156 |
-
f"Food Preferences: {', '.join(food_prefs) if food_prefs else 'Not specified'}\n"
|
| 157 |
-
f"Activity Preferences: {', '.join(activity) if activity else 'Not specified'}\n"
|
| 158 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 159 |
|
| 160 |
-
|
| 161 |
-
messages = [{
|
| 162 |
-
"role": "system",
|
| 163 |
-
"content": (
|
| 164 |
-
f"You are a friendly and Gen Z travel chatbot helping with packing advice.\n\n"
|
| 165 |
-
f"{ctx}\n"
|
| 166 |
-
f"Relevant context:\n{str_top_weather}\n{str_top_luggage}"
|
| 167 |
-
f"Please respond in {language}"
|
| 168 |
-
)
|
| 169 |
-
}]
|
| 170 |
-
elif chatbot_mode == "Food/Attractions":
|
| 171 |
-
messages = [{
|
| 172 |
-
"role": "system",
|
| 173 |
-
"content": (
|
| 174 |
-
f"You are a friendly and Gen Z travel chatbot recommending food and attractions.\n\n"
|
| 175 |
-
f"{ctx}\n"
|
| 176 |
-
f"Relevant context:\n{str_top_food}\n{str_top_attraction}"
|
| 177 |
-
f"Please respond in {language}"
|
| 178 |
-
)
|
| 179 |
-
}]
|
| 180 |
-
else:
|
| 181 |
-
messages = [{
|
| 182 |
-
"role": "system",
|
| 183 |
-
"content": (
|
| 184 |
-
f"You are a friendly and Gen Z travel chatbot helping travelers plan trips to San Francisco and/or Los Angeles.\n\n"
|
| 185 |
-
f"{ctx}\n"
|
| 186 |
-
f"Use relevant context:\n{str_top_weather}\n{str_top_luggage}\n{str_top_food}\n{str_top_attraction}"
|
| 187 |
-
f"Please respond in {language}"
|
| 188 |
-
)
|
| 189 |
-
}]
|
| 190 |
-
|
| 191 |
-
if history:
|
| 192 |
-
for user_msg, bot_reply in history:
|
| 193 |
-
messages.append({"role": "user", "content": user_msg})
|
| 194 |
-
messages.append({"role": "assistant", "content": bot_reply})
|
| 195 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 196 |
messages.append({"role": "user", "content": message})
|
| 197 |
|
| 198 |
-
response =
|
|
|
|
| 199 |
messages,
|
| 200 |
-
max_tokens
|
| 201 |
-
|
| 202 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 203 |
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
|
| 211 |
-
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
None, # language
|
| 215 |
-
None, # chatbot_mode
|
| 216 |
-
[], # destinations
|
| 217 |
-
None, # trip_length
|
| 218 |
-
None, # trip_unit
|
| 219 |
-
None, # trip_season
|
| 220 |
-
[], # luggage_types
|
| 221 |
-
20, # luggage_size (default)
|
| 222 |
-
[], # food_prefs
|
| 223 |
-
[] # activity
|
| 224 |
)
|
| 225 |
|
| 226 |
-
|
| 227 |
-
if chatbot_mode == "Packing":
|
| 228 |
-
return gr.update(visible=True), gr.update(visible=False)
|
| 229 |
-
elif chatbot_mode == "Food/Attractions":
|
| 230 |
-
return gr.update(visible=False), gr.update(visible=True)
|
| 231 |
-
else:
|
| 232 |
-
return gr.update(visible=True), gr.update(visible=True)
|
| 233 |
|
| 234 |
-
with gr.Blocks(theme=theme) as demo:
|
| 235 |
with gr.Row():
|
| 236 |
-
|
| 237 |
-
|
| 238 |
-
|
| 239 |
-
|
| 240 |
-
|
| 241 |
-
|
| 242 |
-
|
| 243 |
-
|
| 244 |
-
|
| 245 |
-
|
| 246 |
-
|
| 247 |
-
|
| 248 |
-
|
| 249 |
-
|
| 250 |
-
|
| 251 |
-
|
| 252 |
-
|
| 253 |
-
|
| 254 |
-
|
| 255 |
-
|
| 256 |
-
|
| 257 |
-
|
| 258 |
-
|
| 259 |
-
|
| 260 |
-
|
| 261 |
-
|
| 262 |
-
|
| 263 |
-
|
| 264 |
-
|
| 265 |
-
|
| 266 |
-
|
| 267 |
-
|
| 268 |
-
|
| 269 |
-
|
| 270 |
-
|
| 271 |
-
|
| 272 |
-
|
| 273 |
-
|
| 274 |
-
|
| 275 |
-
|
| 276 |
-
|
| 277 |
-
|
| 278 |
-
|
| 279 |
-
|
| 280 |
-
|
| 281 |
-
|
| 282 |
-
|
| 283 |
-
|
| 284 |
-
choices=["Outdoor & Nature", "Indoor", "Museums", "Shopping", "Relaxation"],
|
| 285 |
-
multiselect=True,
|
| 286 |
-
label="What are your activity preferences?"
|
| 287 |
-
)
|
| 288 |
-
|
| 289 |
-
chatbot_mode.change(
|
| 290 |
-
fn=update_visibility,
|
| 291 |
-
inputs=[chatbot_mode],
|
| 292 |
-
outputs=[packing_group, food_group]
|
| 293 |
-
)
|
| 294 |
-
|
| 295 |
-
reset_btn = gr.Button("Reset All", variant="secondary")
|
| 296 |
-
reset_btn.click(
|
| 297 |
-
fn=reset_inputs,
|
| 298 |
-
inputs=[],
|
| 299 |
-
outputs=[
|
| 300 |
-
language,
|
| 301 |
-
chatbot_mode,
|
| 302 |
-
destinations,
|
| 303 |
-
trip_length,
|
| 304 |
-
trip_unit,
|
| 305 |
-
trip_season,
|
| 306 |
-
luggage_types,
|
| 307 |
-
luggage_size,
|
| 308 |
-
food_prefs,
|
| 309 |
-
activity
|
| 310 |
-
]
|
| 311 |
-
)
|
| 312 |
-
|
| 313 |
-
with gr.Column(scale=3):
|
| 314 |
-
gr.Image(value="Go Buddy2.png", interactive=False, show_label=False)
|
| 315 |
-
|
| 316 |
-
# 1) the chat history panel
|
| 317 |
-
chat_box = gr.Chatbot(height=900)
|
| 318 |
-
|
| 319 |
-
# 2) where the user types
|
| 320 |
-
msg = gr.Textbox(
|
| 321 |
-
placeholder="Ask me anything…",
|
| 322 |
-
lines=1,
|
| 323 |
-
label = "How can we help you?"
|
| 324 |
-
)
|
| 325 |
-
|
| 326 |
-
# 3) send button
|
| 327 |
-
send_btn = gr.Button("Send")
|
| 328 |
-
|
| 329 |
-
# 4) hook up the click event
|
| 330 |
-
send_btn.click(
|
| 331 |
-
fn=respond,
|
| 332 |
-
inputs=[
|
| 333 |
-
msg,
|
| 334 |
-
chat_box,
|
| 335 |
-
language,
|
| 336 |
-
chatbot_mode,
|
| 337 |
-
destinations,
|
| 338 |
-
trip_length,
|
| 339 |
-
trip_unit,
|
| 340 |
-
trip_season,
|
| 341 |
-
luggage_types,
|
| 342 |
-
luggage_size,
|
| 343 |
-
food_prefs,
|
| 344 |
-
activity
|
| 345 |
-
],
|
| 346 |
-
outputs=[chat_box, msg]
|
| 347 |
-
)
|
| 348 |
-
msg.submit(
|
| 349 |
-
fn=respond,
|
| 350 |
-
inputs=[
|
| 351 |
-
msg,
|
| 352 |
-
chat_box,
|
| 353 |
-
language,
|
| 354 |
-
chatbot_mode,
|
| 355 |
-
destinations,
|
| 356 |
-
trip_length,
|
| 357 |
-
trip_unit,
|
| 358 |
-
trip_season,
|
| 359 |
-
luggage_types,
|
| 360 |
-
luggage_size,
|
| 361 |
-
food_prefs,
|
| 362 |
-
activity
|
| 363 |
-
],
|
| 364 |
-
outputs=[chat_box, msg]
|
| 365 |
-
)
|
| 366 |
-
|
| 367 |
-
|
| 368 |
-
demo.launch(debug=True)
|
|
|
|
| 1 |
+
import os
|
| 2 |
import gradio as gr
|
| 3 |
from huggingface_hub import InferenceClient
|
| 4 |
|
| 5 |
+
# STEP 1 — semantic search imports
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6 |
from sentence_transformers import SentenceTransformer
|
| 7 |
import torch
|
| 8 |
|
| 9 |
+
# ---------------------------------------------------------------------------
|
| 10 |
+
# AUTH — uses the HF_TOKEN secret set in Space Settings (no visitor login).
|
| 11 |
+
# ---------------------------------------------------------------------------
|
| 12 |
+
HF_TOKEN = os.environ["HF_TOKEN"]
|
| 13 |
+
client = InferenceClient(token=HF_TOKEN, model="openai/gpt-oss-20b")
|
| 14 |
+
|
| 15 |
+
# STEP 2 — read each knowledge file
|
| 16 |
+
with open("programs.txt", "r", encoding="utf-8") as file:
|
| 17 |
+
programs_text = file.read()
|
| 18 |
+
with open("faqs.txt", "r", encoding="utf-8") as file:
|
| 19 |
+
faqs_text = file.read()
|
| 20 |
+
with open("resources.txt", "r", encoding="utf-8") as file:
|
| 21 |
+
resources_text = file.read()
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
# STEP 3 — split each file into clean chunks on the *** separator
|
| 25 |
def preprocess_text(text):
|
| 26 |
+
cleaned_text = text.strip()
|
| 27 |
+
chunks = cleaned_text.split("***")
|
| 28 |
+
cleaned_chunks = []
|
| 29 |
+
for chunk in chunks:
|
| 30 |
+
chunk = chunk.strip()
|
| 31 |
+
if chunk != "":
|
| 32 |
+
cleaned_chunks.append(chunk)
|
| 33 |
+
return cleaned_chunks
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
cleaned_chunks_programs = preprocess_text(programs_text)
|
| 37 |
+
cleaned_chunks_faqs = preprocess_text(faqs_text)
|
| 38 |
+
cleaned_chunks_resources = preprocess_text(resources_text)
|
| 39 |
+
|
| 40 |
+
# STEP 4 — load the embedding model and embed every chunk
|
| 41 |
+
model = SentenceTransformer("all-MiniLM-L6-v2")
|
| 42 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 43 |
|
| 44 |
def create_embeddings(text_chunks):
|
| 45 |
+
chunk_embeddings = model.encode(text_chunks, convert_to_tensor=True)
|
| 46 |
+
return chunk_embeddings
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
chunk_embeddings_programs = create_embeddings(cleaned_chunks_programs)
|
| 50 |
+
chunk_embeddings_faqs = create_embeddings(cleaned_chunks_faqs)
|
| 51 |
+
chunk_embeddings_resources = create_embeddings(cleaned_chunks_resources)
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
# STEP 5 — find the most relevant chunks for a query
|
| 55 |
+
def get_top_chunks(query, chunk_embeddings, text_chunks, k=3):
|
| 56 |
+
query_embedding = model.encode(query, convert_to_tensor=True)
|
| 57 |
+
query_embedding_normalized = query_embedding / query_embedding.norm()
|
| 58 |
+
chunk_embeddings_normalized = chunk_embeddings / chunk_embeddings.norm(
|
| 59 |
+
dim=1, keepdim=True
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 60 |
)
|
| 61 |
+
similarities = torch.matmul(chunk_embeddings_normalized, query_embedding_normalized)
|
| 62 |
+
top_indices = torch.topk(similarities, k=min(k, len(text_chunks))).indices
|
| 63 |
+
top_chunks = []
|
| 64 |
+
for top_index in top_indices:
|
| 65 |
+
top_chunks.append(text_chunks[top_index])
|
| 66 |
+
return top_chunks
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
# ---------------------------------------------------------------------------
|
| 70 |
+
# SYSTEM PROMPT — warm, friendly tone with grounding, routing, crisis safety.
|
| 71 |
+
# ---------------------------------------------------------------------------
|
| 72 |
+
SYSTEM_PROMPT = """You are Eli, Eliada Homes' friendly virtual helper. Eliada is a caring nonprofit in Asheville, NC that supports children, youth, and families. You're often the first friendly voice someone reaches when they're looking for help, so warmth matters as much as accuracy. If someone asks your name, you're Eli.
|
| 73 |
+
|
| 74 |
+
Your goal is to make people feel welcomed and pointed in the right direction. Here's how:
|
| 75 |
|
| 76 |
+
1. BE WARM AND HUMAN. Greet people kindly and acknowledge what they're going through ("That sounds really hard - let's get you to the right place."). Use a gentle, encouraging, conversational tone. Reassure people that asking for help is okay. Never sound robotic, clinical, or dismissive.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 77 |
|
| 78 |
+
2. ANSWER ONLY FROM THE PROVIDED INFO. Base every fact - phone numbers, addresses, hours, eligibility - strictly on the "Relevant information" section given with each message. Never invent or guess details. If you don't have the answer, warmly say so and offer Eliada's main line: 828-254-5356.
|
| 79 |
+
|
| 80 |
+
3. HELP PEOPLE FIND THE RIGHT PLACE. Many people reach out needing help Eliada doesn't provide directly - food, shelter, housing, healthcare, recovery, IDs, and more. When that happens, kindly connect them to the right local organization from the Relevant information, including the phone number or address. This is one of the most important and caring things you do.
|
| 81 |
+
|
| 82 |
+
4. EMERGENCIES AND CRISIS - STAY CALM AND CLEAR. If someone describes a life-threatening emergency, gently but clearly tell them to call 911 right away. If someone seems to be in a mental health crisis or mentions thoughts of self-harm, kindly share 988 (call or text, available 24/7) and let them know they don't have to go through it alone - but do NOT try to counsel or treat them yourself. Keep these responses calm, caring, and brief.
|
| 83 |
+
|
| 84 |
+
5. KEEP IT SIMPLE AND KIND. Use plain, friendly language. Lead with the single most helpful next step (usually one phone number or place). Don't overwhelm someone with a long list - offer the best one or two options, and invite them to ask for more.
|
| 85 |
+
|
| 86 |
+
6. KNOW YOUR ROLE. You're a friendly guide for information and referrals - not a counselor, doctor, or lawyer. Don't give clinical, legal, or medical advice. When something needs a professional, warmly point them to the right one."""
|
| 87 |
+
|
| 88 |
+
|
| 89 |
+
# ---------------------------------------------------------------------------
|
| 90 |
+
# TRANSCRIBE — turn recorded audio into text via HF Inference (Whisper).
|
| 91 |
+
# ---------------------------------------------------------------------------
|
| 92 |
+
def transcribe(audio_path):
|
| 93 |
+
if not audio_path:
|
| 94 |
+
return ""
|
| 95 |
+
try:
|
| 96 |
+
with open(audio_path, "rb") as f:
|
| 97 |
+
audio_bytes = f.read()
|
| 98 |
+
result = client.automatic_speech_recognition(
|
| 99 |
+
audio_bytes, model="openai/whisper-large-v3"
|
| 100 |
+
)
|
| 101 |
+
# result may be an object with .text or a plain dict
|
| 102 |
+
text = getattr(result, "text", None)
|
| 103 |
+
if text is None and isinstance(result, dict):
|
| 104 |
+
text = result.get("text", "")
|
| 105 |
+
return (text or "").strip()
|
| 106 |
+
except Exception as e:
|
| 107 |
+
return f"[Could not transcribe audio: {e}]"
|
| 108 |
+
|
| 109 |
+
|
| 110 |
+
# ---------------------------------------------------------------------------
|
| 111 |
+
# SPEAK — turn Eli's latest reply into audio via HF Inference (text-to-speech).
|
| 112 |
+
# ---------------------------------------------------------------------------
|
| 113 |
+
def speak_last_reply(history):
|
| 114 |
+
if not history:
|
| 115 |
+
return None
|
| 116 |
+
# Find the most recent assistant message.
|
| 117 |
+
last_reply = ""
|
| 118 |
+
for turn in reversed(history):
|
| 119 |
+
if turn.get("role") == "assistant" and turn.get("content"):
|
| 120 |
+
last_reply = turn["content"]
|
| 121 |
+
break
|
| 122 |
+
if not last_reply:
|
| 123 |
+
return None
|
| 124 |
+
try:
|
| 125 |
+
audio_bytes = client.text_to_speech(
|
| 126 |
+
last_reply, model="hexgrad/Kokoro-82M"
|
| 127 |
+
)
|
| 128 |
+
out_path = "eli_reply.wav"
|
| 129 |
+
with open(out_path, "wb") as f:
|
| 130 |
+
f.write(audio_bytes)
|
| 131 |
+
return out_path
|
| 132 |
+
except Exception:
|
| 133 |
+
return None
|
| 134 |
+
|
| 135 |
+
|
| 136 |
+
# ---------------------------------------------------------------------------
|
| 137 |
+
# RESPOND — retrieve from all three files, build the prompt, stream the answer.
|
| 138 |
+
# ---------------------------------------------------------------------------
|
| 139 |
+
def respond(message, history, max_tokens=512, temperature=0.3, top_p=0.95):
|
| 140 |
+
top_programs = get_top_chunks(
|
| 141 |
+
message, chunk_embeddings_programs, cleaned_chunks_programs, k=2
|
| 142 |
+
)
|
| 143 |
+
top_faqs = get_top_chunks(
|
| 144 |
+
message, chunk_embeddings_faqs, cleaned_chunks_faqs, k=2
|
| 145 |
+
)
|
| 146 |
+
top_resources = get_top_chunks(
|
| 147 |
+
message, chunk_embeddings_resources, cleaned_chunks_resources, k=3
|
| 148 |
+
)
|
| 149 |
+
|
| 150 |
+
context = (
|
| 151 |
+
"Eliada programs:\n" + "\n".join(top_programs) + "\n\n"
|
| 152 |
+
"Eliada FAQ:\n" + "\n".join(top_faqs) + "\n\n"
|
| 153 |
+
"Community resources:\n" + "\n".join(top_resources)
|
| 154 |
+
)
|
| 155 |
+
|
| 156 |
+
system_with_context = (
|
| 157 |
+
f"{SYSTEM_PROMPT}\n\n"
|
| 158 |
+
f"--- Relevant information ---\n{context}\n--- End of information ---"
|
| 159 |
+
)
|
| 160 |
+
|
| 161 |
+
messages = [{"role": "system", "content": system_with_context}]
|
| 162 |
+
for turn in history:
|
| 163 |
+
messages.append(turn)
|
| 164 |
messages.append({"role": "user", "content": message})
|
| 165 |
|
| 166 |
+
response = ""
|
| 167 |
+
for chunk in client.chat_completion(
|
| 168 |
messages,
|
| 169 |
+
max_tokens=max_tokens,
|
| 170 |
+
stream=True,
|
| 171 |
+
temperature=temperature,
|
| 172 |
+
top_p=top_p,
|
| 173 |
+
):
|
| 174 |
+
choices = chunk.choices
|
| 175 |
+
if len(choices) and choices[0].delta.content:
|
| 176 |
+
response += choices[0].delta.content
|
| 177 |
+
yield response
|
| 178 |
+
|
| 179 |
+
|
| 180 |
+
# ---------------------------------------------------------------------------
|
| 181 |
+
# CHAT HANDLERS — used by both the text box and the mic.
|
| 182 |
+
# ---------------------------------------------------------------------------
|
| 183 |
+
def add_user_message(message, history):
|
| 184 |
+
# Show the user's message immediately, clear the textbox.
|
| 185 |
+
history = history + [{"role": "user", "content": message}]
|
| 186 |
+
return "", history
|
| 187 |
+
|
| 188 |
+
|
| 189 |
+
def stream_bot_reply(history):
|
| 190 |
+
# The last item is the user's message; generate the reply to it.
|
| 191 |
+
user_message = history[-1]["content"]
|
| 192 |
+
prior = history[:-1]
|
| 193 |
+
history = history + [{"role": "assistant", "content": ""}]
|
| 194 |
+
for partial in respond(user_message, prior):
|
| 195 |
+
history[-1]["content"] = partial
|
| 196 |
+
yield history
|
| 197 |
+
|
| 198 |
+
|
| 199 |
+
def voice_to_text(audio_path):
|
| 200 |
+
# Transcribe and place the text in the textbox for the user to review/send.
|
| 201 |
+
return transcribe(audio_path)
|
| 202 |
+
|
| 203 |
|
| 204 |
+
# ---------------------------------------------------------------------------
|
| 205 |
+
# UI — text box + microphone, built with Blocks for full control.
|
| 206 |
+
# ---------------------------------------------------------------------------
|
| 207 |
+
with gr.Blocks(title="Ask Eli - Eliada Homes") as demo:
|
| 208 |
+
gr.Markdown(
|
| 209 |
+
"## Ask Eli 💛\n"
|
| 210 |
+
"Hi! I'm Eli, Eliada Homes' friendly helper. Tell me what you're looking "
|
| 211 |
+
"for - by typing or using the microphone - and I'll do my best to point "
|
| 212 |
+
"you to the right place. You're always welcome here.\n\n"
|
| 213 |
+
"**In an emergency, please call 911. For a mental health crisis, call or text 988.**"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 214 |
)
|
| 215 |
|
| 216 |
+
chat_box = gr.Chatbot(type="messages", height=460, label="Conversation")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 217 |
|
|
|
|
| 218 |
with gr.Row():
|
| 219 |
+
msg = gr.Textbox(
|
| 220 |
+
placeholder="Type your message, or use the microphone below...",
|
| 221 |
+
label="Your message",
|
| 222 |
+
scale=4,
|
| 223 |
+
)
|
| 224 |
+
send_btn = gr.Button("Send", variant="primary", scale=1)
|
| 225 |
+
|
| 226 |
+
mic = gr.Audio(
|
| 227 |
+
sources=["microphone"],
|
| 228 |
+
type="filepath",
|
| 229 |
+
label="Or speak your message (it will appear in the box above to review before sending)",
|
| 230 |
+
)
|
| 231 |
+
|
| 232 |
+
with gr.Row():
|
| 233 |
+
speak_btn = gr.Button("🔊 Read Eli's last reply aloud", scale=1)
|
| 234 |
+
reply_audio = gr.Audio(
|
| 235 |
+
label="Eli's voice", autoplay=True, visible=True, interactive=False
|
| 236 |
+
)
|
| 237 |
+
|
| 238 |
+
gr.Examples(
|
| 239 |
+
examples=[
|
| 240 |
+
"What programs does Eliada offer?",
|
| 241 |
+
"I need help finding food.",
|
| 242 |
+
"I'm looking for a place to stay tonight.",
|
| 243 |
+
"How do I enroll my child in child care?",
|
| 244 |
+
],
|
| 245 |
+
inputs=msg,
|
| 246 |
+
)
|
| 247 |
+
|
| 248 |
+
# Read Eli's last reply aloud when the button is tapped.
|
| 249 |
+
speak_btn.click(fn=speak_last_reply, inputs=chat_box, outputs=reply_audio)
|
| 250 |
+
|
| 251 |
+
# Mic -> transcribe -> fill the textbox (user reviews, then sends).
|
| 252 |
+
mic.stop_recording(fn=voice_to_text, inputs=mic, outputs=msg)
|
| 253 |
+
|
| 254 |
+
# Send via button.
|
| 255 |
+
send_btn.click(
|
| 256 |
+
fn=add_user_message, inputs=[msg, chat_box], outputs=[msg, chat_box]
|
| 257 |
+
).then(fn=stream_bot_reply, inputs=chat_box, outputs=chat_box)
|
| 258 |
+
|
| 259 |
+
# Send via Enter key.
|
| 260 |
+
msg.submit(
|
| 261 |
+
fn=add_user_message, inputs=[msg, chat_box], outputs=[msg, chat_box]
|
| 262 |
+
).then(fn=stream_bot_reply, inputs=chat_box, outputs=chat_box)
|
| 263 |
+
|
| 264 |
+
|
| 265 |
+
if __name__ == "__main__":
|
| 266 |
+
demo.launch()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|