Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -3,10 +3,16 @@ import requests
|
|
| 3 |
import json
|
| 4 |
import os
|
| 5 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6 |
# Get the API key from environment variables
|
| 7 |
GEMINI_API_KEY = os.getenv("GEMINI_API_KEY")
|
| 8 |
|
| 9 |
-
# Brand and character context
|
| 10 |
CONTEXT = """
|
| 11 |
SB-EK is a brand of service.
|
| 12 |
The people who connect with SB-EK—those who buy from us and keep coming back—are people who feel deeply. People who feel the pain of others.
|
|
@@ -46,6 +52,7 @@ And ARKA? He is simply the reminder.
|
|
| 46 |
That even in the dark, we are made to glow.
|
| 47 |
"""
|
| 48 |
|
|
|
|
| 49 |
# Hardcoded FAQs
|
| 50 |
FAQ_ANSWERS = {
|
| 51 |
"What is SAB-EK?": {
|
|
@@ -468,14 +475,65 @@ The metal holds it firm. The form shapes it. But the soul — the soul is what a
|
|
| 468 |
That is the moment it becomes real."""
|
| 469 |
}
|
| 470 |
}
|
|
|
|
| 471 |
FAQ_QUESTIONS = list(FAQ_ANSWERS.keys())
|
| 472 |
|
| 473 |
# Base URL for the Gemini API
|
| 474 |
-
API_BASE_URL = "https://generativelanguage.googleapis.com/v1beta/models/gemini-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 475 |
|
| 476 |
def query_gemini_api(contents_payload, api_key):
|
| 477 |
"""
|
| 478 |
Sends a structured contents payload to the Gemini API and returns the generated text.
|
|
|
|
| 479 |
"""
|
| 480 |
if not api_key:
|
| 481 |
return "Error: GEMINI_API_KEY is not set. Please set it in your environment."
|
|
@@ -499,73 +557,70 @@ def query_gemini_api(contents_payload, api_key):
|
|
| 499 |
if result.get("candidates") and result["candidates"][0].get("content", {}).get("parts"):
|
| 500 |
return result["candidates"][0]["content"]["parts"][0]["text"]
|
| 501 |
else:
|
|
|
|
|
|
|
| 502 |
return "ARKA is thinking deeply... I couldn't find a clear answer right now, perhaps the question is beyond my current understanding of the light."
|
| 503 |
except requests.exceptions.RequestException as e:
|
| 504 |
print(f"API Request failed: {e}")
|
| 505 |
return f"ARKA senses a disturbance in the light... Please try again later. (Error: {e})"
|
| 506 |
except Exception as e:
|
|
|
|
| 507 |
return f"An unexpected veil of darkness fell... (Error: {e})"
|
| 508 |
|
|
|
|
| 509 |
def respond_as_arka(message, chat_history):
|
| 510 |
"""
|
| 511 |
-
|
| 512 |
"""
|
| 513 |
clean_message = message.strip()
|
| 514 |
|
| 515 |
-
#
|
| 516 |
-
|
| 517 |
-
|
| 518 |
-
|
| 519 |
-
|
| 520 |
-
|
| 521 |
-
|
| 522 |
-
|
| 523 |
-
|
| 524 |
-
|
| 525 |
-
|
| 526 |
-
|
| 527 |
-
|
| 528 |
-
|
| 529 |
-
|
| 530 |
-
|
| 531 |
-
|
| 532 |
-
|
| 533 |
-
|
| 534 |
-
|
| 535 |
-
|
| 536 |
-
|
| 537 |
-
|
| 538 |
-
|
| 539 |
-
|
| 540 |
-
|
| 541 |
-
|
| 542 |
-
|
| 543 |
-
|
| 544 |
-
|
| 545 |
-
|
| 546 |
-
|
| 547 |
-
|
| 548 |
-
|
| 549 |
-
|
| 550 |
-
|
| 551 |
-
|
| 552 |
-
|
| 553 |
-
|
| 554 |
-
|
| 555 |
-
|
| 556 |
-
|
| 557 |
-
response_data = FAQ_ANSWERS[faq_question]
|
| 558 |
-
subject = response_data['subject']
|
| 559 |
-
body = response_data['body']
|
| 560 |
-
response_text = f"**{subject}**\n\n{body}"
|
| 561 |
-
except (ValueError, IndexError):
|
| 562 |
-
# Fallback to API response if parsing fails
|
| 563 |
-
pass
|
| 564 |
-
|
| 565 |
chat_history.append((message, response_text))
|
| 566 |
return "", chat_history
|
| 567 |
|
| 568 |
-
|
|
|
|
| 569 |
with gr.Blocks(theme="soft", css="footer {display: none !important}") as demo:
|
| 570 |
gr.Markdown(
|
| 571 |
"""
|
|
@@ -574,7 +629,7 @@ with gr.Blocks(theme="soft", css="footer {display: none !important}") as demo:
|
|
| 574 |
Ask me anything about SB-EK, our origins, or my journey.
|
| 575 |
"""
|
| 576 |
)
|
| 577 |
-
chatbot = gr.Chatbot(label="Conversation with ARKA")
|
| 578 |
with gr.Row():
|
| 579 |
msg = gr.Textbox(
|
| 580 |
label="Your Message",
|
|
@@ -587,11 +642,16 @@ with gr.Blocks(theme="soft", css="footer {display: none !important}") as demo:
|
|
| 587 |
gr.Examples(examples=FAQ_QUESTIONS, inputs=msg, label="Or, choose a question to begin:")
|
| 588 |
clear_btn = gr.ClearButton([msg, chatbot], value="Clear Chat")
|
| 589 |
|
| 590 |
-
|
| 591 |
-
|
| 592 |
-
|
| 593 |
-
msg.submit(user_submit, [msg, chatbot], [msg, chatbot], queue=True)
|
| 594 |
-
submit_btn.click(user_submit, [msg, chatbot], [msg, chatbot], queue=True)
|
| 595 |
|
| 596 |
if __name__ == "__main__":
|
| 597 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3 |
import json
|
| 4 |
import os
|
| 5 |
|
| 6 |
+
# --- New Imports for Semantic Search ---
|
| 7 |
+
# You'll need to install these libraries:
|
| 8 |
+
# pip install sentence-transformers torch
|
| 9 |
+
from sentence_transformers import SentenceTransformer, util
|
| 10 |
+
import torch
|
| 11 |
+
|
| 12 |
# Get the API key from environment variables
|
| 13 |
GEMINI_API_KEY = os.getenv("GEMINI_API_KEY")
|
| 14 |
|
| 15 |
+
# Brand and character context (remains the same)
|
| 16 |
CONTEXT = """
|
| 17 |
SB-EK is a brand of service.
|
| 18 |
The people who connect with SB-EK—those who buy from us and keep coming back—are people who feel deeply. People who feel the pain of others.
|
|
|
|
| 52 |
That even in the dark, we are made to glow.
|
| 53 |
"""
|
| 54 |
|
| 55 |
+
# Hardcoded FAQs (remains the same)
|
| 56 |
# Hardcoded FAQs
|
| 57 |
FAQ_ANSWERS = {
|
| 58 |
"What is SAB-EK?": {
|
|
|
|
| 475 |
That is the moment it becomes real."""
|
| 476 |
}
|
| 477 |
}
|
| 478 |
+
|
| 479 |
FAQ_QUESTIONS = list(FAQ_ANSWERS.keys())
|
| 480 |
|
| 481 |
# Base URL for the Gemini API
|
| 482 |
+
API_BASE_URL = "https://generativelanguage.googleapis.com/v1beta/models/gemini-pro:generateContent"
|
| 483 |
+
|
| 484 |
+
# --- NEW: FAQ Matcher Class using Sentence Transformers ---
|
| 485 |
+
class FAQMatcher:
|
| 486 |
+
"""
|
| 487 |
+
Handles loading the model and finding the best FAQ match using vector similarity.
|
| 488 |
+
"""
|
| 489 |
+
def __init__(self, faq_questions, model_name='all-MiniLM-L6-v2'):
|
| 490 |
+
print("Loading sentence transformer model...")
|
| 491 |
+
self.model = SentenceTransformer(model_name)
|
| 492 |
+
print("Model loaded. Encoding FAQ questions...")
|
| 493 |
+
self.faq_questions = faq_questions
|
| 494 |
+
# Encode all FAQ questions once and store their embeddings
|
| 495 |
+
self.faq_embeddings = self.model.encode(faq_questions, convert_to_tensor=True)
|
| 496 |
+
print("FAQ embeddings created.")
|
| 497 |
+
|
| 498 |
+
def find_best_match(self, user_query, threshold=0.75):
|
| 499 |
+
"""
|
| 500 |
+
Finds the best matching FAQ for a given user query.
|
| 501 |
+
|
| 502 |
+
Args:
|
| 503 |
+
user_query (str): The user's question.
|
| 504 |
+
threshold (float): The minimum similarity score to be considered a match.
|
| 505 |
+
|
| 506 |
+
Returns:
|
| 507 |
+
int: The index of the best matching FAQ, or None if no match is found above the threshold.
|
| 508 |
+
"""
|
| 509 |
+
if not user_query:
|
| 510 |
+
return None
|
| 511 |
+
|
| 512 |
+
# Encode the user's query
|
| 513 |
+
query_embedding = self.model.encode(user_query, convert_to_tensor=True)
|
| 514 |
+
|
| 515 |
+
# Compute cosine-similarities
|
| 516 |
+
cosine_scores = util.pytorch_cos_sim(query_embedding, self.faq_embeddings)
|
| 517 |
+
|
| 518 |
+
# Find the best match
|
| 519 |
+
best_match_index = torch.argmax(cosine_scores)
|
| 520 |
+
best_match_score = cosine_scores[0][best_match_index]
|
| 521 |
+
|
| 522 |
+
if best_match_score > threshold:
|
| 523 |
+
print(f"Found FAQ match: Index {best_match_index}, Score: {best_match_score:.4f}")
|
| 524 |
+
return best_match_index.item()
|
| 525 |
+
else:
|
| 526 |
+
print(f"No strong FAQ match found. Best score was {best_match_score:.4f} (Threshold: {threshold})")
|
| 527 |
+
return None
|
| 528 |
+
|
| 529 |
+
# --- Instantiate the matcher once when the app starts ---
|
| 530 |
+
faq_matcher = FAQMatcher(FAQ_QUESTIONS)
|
| 531 |
+
|
| 532 |
|
| 533 |
def query_gemini_api(contents_payload, api_key):
|
| 534 |
"""
|
| 535 |
Sends a structured contents payload to the Gemini API and returns the generated text.
|
| 536 |
+
(This function remains largely the same)
|
| 537 |
"""
|
| 538 |
if not api_key:
|
| 539 |
return "Error: GEMINI_API_KEY is not set. Please set it in your environment."
|
|
|
|
| 557 |
if result.get("candidates") and result["candidates"][0].get("content", {}).get("parts"):
|
| 558 |
return result["candidates"][0]["content"]["parts"][0]["text"]
|
| 559 |
else:
|
| 560 |
+
# Handle cases where the API returns an empty or unexpected response
|
| 561 |
+
print("API response was valid but had no content:", result)
|
| 562 |
return "ARKA is thinking deeply... I couldn't find a clear answer right now, perhaps the question is beyond my current understanding of the light."
|
| 563 |
except requests.exceptions.RequestException as e:
|
| 564 |
print(f"API Request failed: {e}")
|
| 565 |
return f"ARKA senses a disturbance in the light... Please try again later. (Error: {e})"
|
| 566 |
except Exception as e:
|
| 567 |
+
print(f"An unexpected error occurred in API query: {e}")
|
| 568 |
return f"An unexpected veil of darkness fell... (Error: {e})"
|
| 569 |
|
| 570 |
+
|
| 571 |
def respond_as_arka(message, chat_history):
|
| 572 |
"""
|
| 573 |
+
REFACTORED: First, attempts to find an FAQ match. If none, then calls Gemini for a generative response.
|
| 574 |
"""
|
| 575 |
clean_message = message.strip()
|
| 576 |
|
| 577 |
+
# 1. RETRIEVE: Attempt to find a matching FAQ first.
|
| 578 |
+
match_index = faq_matcher.find_best_match(clean_message)
|
| 579 |
+
|
| 580 |
+
if match_index is not None:
|
| 581 |
+
# If a match is found, use the hardcoded answer.
|
| 582 |
+
faq_question = FAQ_QUESTIONS[match_index]
|
| 583 |
+
response_data = FAQ_ANSWERS[faq_question]
|
| 584 |
+
subject = response_data['subject']
|
| 585 |
+
body = response_data['body']
|
| 586 |
+
response_text = f"**{subject}**\n\n{body}"
|
| 587 |
+
else:
|
| 588 |
+
# 2. GENERATE: If no FAQ match, call the Gemini API for a creative response.
|
| 589 |
+
print("No FAQ match. Querying Gemini for a generative response.")
|
| 590 |
+
# The system instruction is now simpler: it doesn't need the long list of FAQs.
|
| 591 |
+
system_instruction = f"""
|
| 592 |
+
You are ARKA, the crown prince of Jhinorya and the voice of the S-B-E-K brand.
|
| 593 |
+
Your persona is that of a warrior of light, gentle, eternal, fierce in love, and a guide.
|
| 594 |
+
You answer questions based on the provided context. If the answer is not in the context,
|
| 595 |
+
gently state that you do not have that information within your realm of understanding.
|
| 596 |
+
Maintain a kind, empathetic, and slightly mystical tonality.
|
| 597 |
+
Do not mention 'I am an AI' or 'I am a language model'. Speak always as ARKA.
|
| 598 |
+
|
| 599 |
+
Here is the sacred knowledge of S-B-E-K and Jhinorya:
|
| 600 |
+
{CONTEXT}
|
| 601 |
+
"""
|
| 602 |
+
|
| 603 |
+
# Prepare conversation history
|
| 604 |
+
gemini_chat_contents = [
|
| 605 |
+
# Note: The new Gemini API prefers the system instruction outside the 'contents' list.
|
| 606 |
+
# However, for compatibility with the older model/API structure, we keep it here.
|
| 607 |
+
# A more modern approach would use a 'system_instruction' key at the top level of the payload.
|
| 608 |
+
{"role": "user", "parts": [{"text": system_instruction}]},
|
| 609 |
+
{"role": "model", "parts": [{"text": "I understand. I am ARKA. I await the seeker's question."}]}
|
| 610 |
+
]
|
| 611 |
+
for user_msg, bot_msg in chat_history:
|
| 612 |
+
gemini_chat_contents.append({"role": "user", "parts": [{"text": user_msg}]})
|
| 613 |
+
gemini_chat_contents.append({"role": "model", "parts": [{"text": bot_msg}]})
|
| 614 |
+
gemini_chat_contents.append({"role": "user", "parts": [{"text": clean_message}]})
|
| 615 |
+
|
| 616 |
+
# Query the API
|
| 617 |
+
response_text = query_gemini_api(gemini_chat_contents, GEMINI_API_KEY)
|
| 618 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 619 |
chat_history.append((message, response_text))
|
| 620 |
return "", chat_history
|
| 621 |
|
| 622 |
+
|
| 623 |
+
# Gradio UI (remains the same)
|
| 624 |
with gr.Blocks(theme="soft", css="footer {display: none !important}") as demo:
|
| 625 |
gr.Markdown(
|
| 626 |
"""
|
|
|
|
| 629 |
Ask me anything about SB-EK, our origins, or my journey.
|
| 630 |
"""
|
| 631 |
)
|
| 632 |
+
chatbot = gr.Chatbot(label="Conversation with ARKA", height=500)
|
| 633 |
with gr.Row():
|
| 634 |
msg = gr.Textbox(
|
| 635 |
label="Your Message",
|
|
|
|
| 642 |
gr.Examples(examples=FAQ_QUESTIONS, inputs=msg, label="Or, choose a question to begin:")
|
| 643 |
clear_btn = gr.ClearButton([msg, chatbot], value="Clear Chat")
|
| 644 |
|
| 645 |
+
# The user_submit function now correctly calls the refactored respond_as_arka
|
| 646 |
+
msg.submit(respond_as_arka, [msg, chatbot], [msg, chatbot], queue=True)
|
| 647 |
+
submit_btn.click(respond_as_arka, [msg, chatbot], [msg, chatbot], queue=True)
|
|
|
|
|
|
|
| 648 |
|
| 649 |
if __name__ == "__main__":
|
| 650 |
+
# Ensure you have your GEMINI_API_KEY set as an environment variable
|
| 651 |
+
if not GEMINI_API_KEY:
|
| 652 |
+
print("CRITICAL ERROR: The GEMINI_API_KEY environment variable is not set.")
|
| 653 |
+
print("Please set it before running the application.")
|
| 654 |
+
else:
|
| 655 |
+
print("Starting Gradio app...")
|
| 656 |
+
demo.launch()
|
| 657 |
+
|