File size: 11,261 Bytes
84dec6c
 
 
262b9e5
84dec6c
 
 
 
eaa6966
3726abc
5d5dbc7
cd4f89c
4a5e65f
 
 
 
 
 
 
cd4f89c
3726abc
 
 
 
 
 
 
 
4a5e65f
 
 
fe573a1
3726abc
 
 
4a5e65f
3726abc
cd4f89c
 
eaa6966
cd4f89c
3726abc
cd4f89c
 
 
eaa6966
5d5dbc7
84dec6c
 
 
eaa6966
cd4f89c
5d5dbc7
cd4f89c
84dec6c
cd4f89c
 
 
 
 
 
5d5dbc7
cd4f89c
 
 
 
 
 
 
 
84dec6c
5d5dbc7
 
cd4f89c
84dec6c
cd4f89c
 
 
 
84dec6c
 
cd4f89c
84dec6c
 
 
 
 
 
 
 
cd4f89c
84dec6c
 
cd4f89c
 
84dec6c
cd4f89c
84dec6c
cd4f89c
84dec6c
 
cd4f89c
84dec6c
 
 
cd4f89c
 
 
84dec6c
 
5d5dbc7
84dec6c
cd4f89c
 
 
84dec6c
 
cd4f89c
84dec6c
cd4f89c
84dec6c
cd4f89c
 
84dec6c
cd4f89c
5d5dbc7
 
84dec6c
cd4f89c
84dec6c
5d5dbc7
cd4f89c
 
 
 
84dec6c
cd4f89c
 
84dec6c
cd4f89c
84dec6c
cd4f89c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5d5dbc7
7c8096a
cd4f89c
7c8096a
 
 
84dec6c
5d5dbc7
 
7c8096a
84dec6c
cd4f89c
84dec6c
5d5dbc7
84dec6c
 
cd4f89c
 
 
 
84dec6c
 
 
cd4f89c
 
fdddaba
84dec6c
 
 
 
 
 
 
eaa6966
fdddaba
84dec6c
cd4f89c
5d5dbc7
84dec6c
cd4f89c
84dec6c
5d5dbc7
 
 
84dec6c
cd4f89c
84dec6c
5d5dbc7
 
 
cd4f89c
 
 
 
 
84dec6c
 
cd4f89c
 
 
 
 
 
84dec6c
cd4f89c
 
 
 
84dec6c
cd4f89c
deeb6cb
7324a6c
4a5e65f
1
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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
# ==============================================================================
# 1. IMPORTS & SETUP
# ==============================================================================
import os
import re
import difflib
import pandas as pd
from datetime import datetime
import gradio as gr
from transformers import pipeline, set_seed ,BitsAndBytesConfig

# --- Model Loading ---
# Read the Hugging Face token from the environment secrets
hf_token = os.environ.get("HUGGING_FACE_HUB_TOKEN")

# Check if the token is available
if not hf_token:
    print("Warning: HUGGING_FACE_HUB_TOKEN not found. This may fail if the model is private.")

print("Loading conversational model...")
# Use the token to authenticate when loading the model
bnb_config = BitsAndBytesConfig(
    load_in_4bit=True,               # Set to True for 4-bit quantization
    bnb_4bit_quant_type="nf4",       # "nf4" is usually the most stable
    bnb_4bit_use_double_quant=True,  # Double quantization helps reduce memory
    bnb_4bit_compute_dtype="float16" # Safer than float32, faster on GPUs
)

# Use the token to authenticate when loading the model
generator = pipeline(
    'text-generation',
    model='Warrior786/acc-llama-1b-merged-v10-lite',
    token=hf_token,
    device_map="auto",               # Let HF infer best device usage
    model_kwargs={"quantization_config": bnb_config}
)

set_seed(42)
print("Model loaded successfully.")



# --- File Paths ---
# Log conversations to a local CSV file within the Hugging Face Space
LOG_FILE = "ACC_chat_history.csv"


# ==============================================================================
# 2. CONSTANTS (Allowed Links & Emails)
# ==============================================================================

# List of all approved URLs for the chatbot to share
ALLOWED_LINKS = [
    "https://acceducate.org/", "https://acceducate.org/apply/", "https://acceducate.org/faqs/",
    "https://acceducate.org/programs/a-continuous-charity-acc-student-loan-refinance-program/",
    "https://acceducate.org/programs/", "https://acceducate.org/programs/help-a-student-with-sadaqah-jariyah/",
    "https://acceducate.org/programs/zakat/", "https://acceducate.org/programs/change-maker/",
    "https://acceducate.org/programs/break-the-chains-of-riba/", "https://acceducate.org/programs/women-empowerment/",
    "https://acceducate.org/programs/educational-justice/", "https://acceducate.org/ways-to-give/",
    "https://acceducate.org/ways-to-give/donate-stocks/", "https://acceducate.org/ways-to-give/open-an-educational-partnership-fund/",
    "https://acceducate.org/ways-to-give/legacy-fund/", "https://acceducate.org/ways-to-give/monthly-giving/",
    "https://acceducate.org/ways-to-give/support-accs-waqf-endowment-a-legacy-of-sustainable-charity/",
    "https://acceducate.org/ways-to-give/donate-by-zelle/", "https://acceducate.org/ways-to-give/matchinggift/",
    "https://acceducate.org/ways-to-give/donate-your-ira-to-acc/", "https://acceducate.org/ways-to-give/corporate-sponsored-volunteering/",
    "https://acceducate.org/ways-to-give/buy-coffee-give-sadaqah/", "https://acceducate.org/ways-to-give/host-an-iftar/",
    "https://acceducate.org/campaigns/grants/", "https://acceducate.org/student-initiated-fundraising-initiative/",
    "https://acceducate.org/about-us/", "https://acceducate.org/our-team/", "https://acceducate.org/financials/",
    "https://acceducate.org/careers/", "https://acceducate.org/why-acc/", "https://acceducate.org/invite-us/",
    "https://acceducate.org/events/", "https://acceducate.org/testimonials/", "https://www.facebook.com/acontinuouscharity/",
    "https://www.instagram.com/acontinuouscharity/", "https://www.youtube.com/channel/UCkq8wAvAqt54yzjzL_QIq3w",
    "https://twitter.com/accnational",
]

# Mapping of keywords to specific ACC pages for better matching
ACC_LINKS = {
    "About Us": "https://acceducate.org/about-us/", "Apply": "https://acceducate.org/apply/",
    "Careers": "https://acceducate.org/careers/", "Donate": "https://acceducate.org/ways-to-give/",
    "Events": "https://acceducate.org/events/", "FAQs": "https://acceducate.org/faqs/",
    "Zakat": "https://acceducate.org/programs/zakat/", "Loan Refinance": "https://acceducate.org/programs/a-continuous-charity-acc-student-loan-refinance-program/",
}

# Allowed email addresses
ALLOWED_EMAILS = ["donate@acceducate.org"]


# ==============================================================================
# 3. HELPER FUNCTIONS
# ==============================================================================

def normalize_acc_link(url: str, user_msg: str = "") -> str:
    """Cleans and restricts a given URL to the official list of allowed ACC links."""
    url = re.sub(r"https?://[^/\s]*acc[^/\s]*\.org", "https://acceducate.org", url)
    url = re.sub(r"(/(\w+))(\s*/\2)+", r"\1", url)
    if not url.endswith("/"): url += "/"
    if url in ALLOWED_LINKS: return url
    for keyword, link in ACC_LINKS.items():
        if keyword.lower() in user_msg.lower(): return link
    closest = difflib.get_close_matches(url, ALLOWED_LINKS, n=1, cutoff=0.6)
    return closest[0] if closest else "https://acceducate.org/"

def sanitize_response_links(text: str, user_msg: str = "") -> str:
    """Finds and normalizes all URLs in the bot's response."""
    text = re.sub(r"https?://\S+", lambda m: normalize_acc_link(m.group(0), user_msg), text)
    def replace_md(match):
        display, url = match.groups()
        final_url = normalize_acc_link(url, user_msg)
        norm_display = re.sub(r"^https?://", "", final_url).rstrip("/")
        return f"[{norm_display}]({final_url})"
    text = re.sub(r"\[([^\]]+)\]\((https?://[^\)]+)\)", replace_md, text)
    return text

def is_valid_email(email: str) -> bool:
    """Checks if a string is a validly formatted email address."""
    pattern = r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$"
    return re.match(pattern, email) is not None

def log_to_csv(email, user_msg, bot_msg):
    """Saves the conversation turn to a CSV file."""
    timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
    new_row = pd.DataFrame([[timestamp, email, user_msg, bot_msg]], columns=["Time", "Email", "User", "Bot"])
    try:
        header = not os.path.exists(LOG_FILE)
        new_row.to_csv(LOG_FILE, mode='a', header=header, index=False)
    except Exception as e:
        print(f"Error logging to CSV: {e}")


# ==============================================================================
# 4. CORE CHATBOT LOGIC
# ==============================================================================

def chat_fn(message: str, history: list, user_email_state: str):
    """Main function to handle the chatbot's logic and conversation flow."""
    # --- Step 1: Handle initial state and email collection ---
    if user_email_state is None:
        if is_valid_email(message.strip()):
            user_email_state = message.strip()
            response = f"βœ… Thank you! Your email `{user_email_state}` has been saved. How can I help you today?"
        else:
            response = "❌ That doesn't look like a valid email. Please enter a valid email address to continue."
        history.append((message, response))
        # Return empty textbox, updated history, and the new email state
        return "", history, user_email_state

    # --- Step 2: Generate a response using the AI model ---
    # Construct a conversation history string for the model
    # Use only the last 2 turns to keep the context concise
    history_context = history[-2:]
    conversation_text = ""
    for user_turn, bot_turn in history_context:
        if user_turn:
            conversation_text += f"User: {user_turn}\n"
        if bot_turn:
            conversation_text += f"Bot: {bot_turn}\n"
    
    prompt = f"{conversation_text}User: {message}\nBot:"

    # Generate response
    generated = generator(prompt, max_length=150, num_return_sequences=1, pad_token_id=generator.tokenizer.eos_token_id)
    raw_response = generated[0]['generated_text']

    # Extract only the last part of the response from the bot
    bot_response = raw_response.split("Bot:")[-1].strip()
    
    # Add a fallback if the model generates an empty response
    if not bot_response:
        bot_response = "I'm sorry, I'm not sure how to respond to that. Could you please rephrase?"

    # --- Step 3: Sanitize links and log the conversation ---
    sanitized_response = sanitize_response_links(bot_response, message)
    log_to_csv(user_email_state, message, sanitized_response)

    history.append((message, sanitized_response))
    return "", history, user_email_state

def welcome_message(history=None):
    """Initializes the chat with a welcome message."""
    if history is None:
        history = []
    history.append((None, " Welcome to the ACC Virtual Assistant! Please enter your email to start our chat."))
    return history



# ==============================================================================
# 5. GRADIO USER INTERFACE
# ==============================================================================

with gr.Blocks(
    css="""
    body, .gradio-container { background-color: #ffffff !important; color: #000000 !important; }
    #header { background-color: rgb(20, 90, 143); color: white; padding: 20px; border-radius: 12px; text-align: center; }
    #chatbot { height: 500px; }
    #footer { text-align: center; font-size: 14px; color: gray; margin-top: 15px; }
    """,
    title="IlmBot - ACC Assistant"
) as demo:
    # State object to store the user's email for the duration of the session
    user_email = gr.State(None)

    gr.Markdown(
        """
        <div id="header">
            <h1>πŸ“š IlmBot β€” Your Knowledge Companion</h1>
            <p><b>A Continuous Charity (ACC)</b> Virtual Assistant</p>
        </div>
        """
    )

    chatbot = gr.Chatbot(label="πŸ’¬ Conversation", elem_id="chatbot")
    msg_box = gr.Textbox(placeholder="Enter your email first, then your message...", label="✍️ Your Message")

    with gr.Row():
        send_btn = gr.Button("Send")
        clear_btn = gr.Button("Clear Chat")

    gr.Markdown(
        """
        <div id="footer">
            Powered by <b>ACC</b> β€’ Learn more at <a href="https://acceducate.org/" target="_blank">acceducate.org</a>
        </div>
        """
    )

    # --- Event Handlers ---
    def user_submit(message, history, email_state):
        if not message.strip():
            return "", history, email_state
        return chat_fn(message, history, email_state)

    def clear_chat():
        """Resets the chat and clears the user's email state."""
        new_history = welcome_message([])
        return "", new_history, None # Clear textbox, history, and email state

    # Load the initial welcome message when the app starts
    demo.load(welcome_message, inputs=None, outputs=[chatbot])

    # Connect UI components to functions
    msg_box.submit(user_submit, [msg_box, chatbot, user_email], [msg_box, chatbot, user_email])
    send_btn.click(user_submit, [msg_box, chatbot, user_email], [msg_box, chatbot, user_email])
    clear_btn.click(clear_chat, None, [msg_box, chatbot, user_email])

# Launch the Gradio app
demo.launch(debug=True,share=True)