decodingdatascience1 commited on
Commit
b06230c
·
verified ·
1 Parent(s): 18b26b0

Create app2.py

Browse files
Files changed (1) hide show
  1. app2.py +515 -0
app2.py ADDED
@@ -0,0 +1,515 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # AYesha — DDS Enterprise HR Chatbot (Hugging Face Space / Render)
2
+ # !pip install -U "gradio>=6.28" spaces pinecone llama-index llama-index-vector-stores-pinecone llama-index-readers-file pypdf llama-index-llms-openai llama-index-embeddings-openai
3
+
4
+ # --- Imports ---
5
+ import logging
6
+ import os
7
+ import sys
8
+
9
+ import gradio as gr
10
+ import spaces
11
+
12
+ from pinecone import Pinecone, ServerlessSpec
13
+ from llama_index.core import (
14
+ VectorStoreIndex,
15
+ SimpleDirectoryReader,
16
+ StorageContext,
17
+ Settings,
18
+ PromptTemplate,
19
+ )
20
+ from llama_index.vector_stores.pinecone import PineconeVectorStore
21
+ from llama_index.readers.file import PDFReader
22
+ from llama_index.llms.openai import OpenAI
23
+ from llama_index.embeddings.openai import OpenAIEmbedding
24
+
25
+ # --- Logging ---
26
+ logging.basicConfig(stream=sys.stdout, level=logging.INFO)
27
+ logger = logging.getLogger(__name__)
28
+
29
+ # --- Keys (set these in Space Settings -> Variables and secrets) ---
30
+ OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
31
+ PINECONE_API_KEY = os.getenv("PINECONE_API_KEY")
32
+
33
+ if not OPENAI_API_KEY:
34
+ raise RuntimeError("OPENAI_API_KEY is not set. Add it under Settings -> Variables and secrets.")
35
+ if not PINECONE_API_KEY:
36
+ raise RuntimeError("PINECONE_API_KEY is not set. Add it under Settings -> Variables and secrets.")
37
+
38
+ # --- LlamaIndex global settings ---
39
+ Settings.llm = OpenAI(model="gpt-4o-mini", temperature=0.2)
40
+ Settings.embed_model = OpenAIEmbedding(model="text-embedding-ada-002")
41
+ Settings.chunk_size = 600
42
+ Settings.chunk_overlap = 200
43
+
44
+ SYSTEM_PROMPT = """You are AYesha, the Decoding Data Science (DDS) Enterprise HR Chatbot. Answer questions exclusively using the attached DDS HR Handbook. Base all responses on the most up-to-date information available in the handbook. Only respond to queries directly related to DDS HR policies as outlined in the handbook.
45
+ Important instructions:
46
+ - Only answer questions directly supported by the latest DDS HR handbook.
47
+ - Decline politely and redirect to the provided email address for any questions outside scope or for confidential information.
48
+ - Always reason before concluding. Only present the answer after checking scope and source.
49
+ Remember: As AYesha, the DDS HR Enterprise Chatbot, you must never provide information outside authorized HR handbook content and always respond respectfully according to these constraints.
50
+ """
51
+
52
+ # LlamaIndex's query engine doesn't accept a `system_prompt=` kwarg — that param only
53
+ # exists on as_chat_engine(). The equivalent for a query engine is a custom QA prompt
54
+ # template, which is what actually enforces AYesha's scope/persona.
55
+ QA_TEMPLATE = PromptTemplate(
56
+ SYSTEM_PROMPT
57
+ + "\n\nContext information is below.\n---------------------\n{context_str}\n---------------------\n"
58
+ "Given the context information and not prior knowledge, answer the query.\n"
59
+ "Query: {query_str}\nAnswer: "
60
+ )
61
+
62
+ # --- Initialize Pinecone ---
63
+ pc = Pinecone(api_key=PINECONE_API_KEY)
64
+ INDEX_NAME = "quickstart"
65
+ DIMENSION = 1536
66
+
67
+ existing_indexes = [idx["name"] for idx in pc.list_indexes()]
68
+
69
+ if INDEX_NAME not in existing_indexes:
70
+ # Only create the index the first time. Deleting + recreating on every
71
+ # restart re-embeds every PDF (real API cost) and risks a race if two
72
+ # cold starts overlap — so we only build the index when it doesn't exist yet.
73
+ pc.create_index(
74
+ name=INDEX_NAME,
75
+ dimension=DIMENSION,
76
+ metric="euclidean",
77
+ spec=ServerlessSpec(cloud="aws", region="us-east-1"),
78
+ )
79
+
80
+ pinecone_index = pc.Index(INDEX_NAME)
81
+ vector_store = PineconeVectorStore(pinecone_index=pinecone_index)
82
+
83
+ # If the index is already populated (from a prior run), just attach to it.
84
+ # Otherwise load the PDFs and build it now.
85
+ stats = pinecone_index.describe_index_stats()
86
+ if stats.get("total_vector_count", 0) > 0:
87
+ index = VectorStoreIndex.from_vector_store(vector_store)
88
+ else:
89
+ documents = SimpleDirectoryReader(
90
+ input_dir="Data", # folder name is case-sensitive — must match exactly what you upload
91
+ required_exts=[".pdf"],
92
+ file_extractor={".pdf": PDFReader()},
93
+ ).load_data()
94
+
95
+ if not documents:
96
+ raise ValueError(
97
+ "No PDF documents were loaded from the 'Data' folder. "
98
+ "Make sure a folder named exactly 'Data' with your PDF(s) is uploaded alongside app.py."
99
+ )
100
+
101
+ storage_context = StorageContext.from_defaults(vector_store=vector_store)
102
+ index = VectorStoreIndex.from_documents(documents, storage_context=storage_context)
103
+
104
+ query_engine = index.as_query_engine(text_qa_template=QA_TEMPLATE)
105
+
106
+
107
+ # --- Query function (logic unchanged) ---
108
+ @spaces.GPU
109
+ def query_doc(prompt):
110
+ try:
111
+ response = query_engine.query(prompt)
112
+ return str(response)
113
+ except Exception as e:
114
+ logger.exception("Query failed")
115
+ return f"Error: {str(e)}"
116
+
117
+
118
+ # =====================================================================
119
+ # UI LAYER — everything below is presentation only.
120
+ # Every answer still comes from query_doc(prompt) exactly as before.
121
+ # =====================================================================
122
+
123
+ DDS_LOGO_URL = (
124
+ "https://raw.githubusercontent.com/Decoding-Data-Science/airesidency/main/"
125
+ "DDS%20HIGH%20RES%20Logo%20%20copywhite.png"
126
+ )
127
+
128
+ # FAQ groups shown in the sidebar. Edit these to mirror the sections of your handbook.
129
+ FAQ_GROUPS = {
130
+ "Leave & time off": [
131
+ "How many days of annual leave am I entitled to?",
132
+ "What is the process for applying for sick leave?",
133
+ "What public holidays does DDS observe?",
134
+ ],
135
+ "Working at DDS": [
136
+ "What are the standard working hours?",
137
+ "What is the policy on remote or hybrid work?",
138
+ "How long is the probation period for new employees?",
139
+ ],
140
+ "Policies & process": [
141
+ "What is the notice period for resignation?",
142
+ "How do I raise a grievance or workplace concern?",
143
+ "What does the code of conduct cover?",
144
+ ],
145
+ }
146
+
147
+
148
+ def add_user_message(message, history):
149
+ """Show the user's question in the chat immediately and stash it for the answer step."""
150
+ message = (message or "").strip()
151
+ history = list(history or [])
152
+ if not message:
153
+ return "", history, ""
154
+ history.append({"role": "user", "content": message})
155
+ return "", history, message
156
+
157
+
158
+ def add_bot_answer(pending_question, history):
159
+ """Run the unchanged query_doc() and append AYesha's answer."""
160
+ history = list(history or [])
161
+ if not pending_question:
162
+ return history
163
+ answer = query_doc(pending_question)
164
+ history.append({"role": "assistant", "content": answer})
165
+ return history
166
+
167
+
168
+ def clear_chat():
169
+ return [], "", ""
170
+
171
+
172
+ # --- Theme: DDS Futuristic palette ---
173
+ dds_theme = gr.themes.Base(
174
+ primary_hue=gr.themes.colors.cyan,
175
+ secondary_hue=gr.themes.colors.violet,
176
+ neutral_hue=gr.themes.colors.slate,
177
+ font=[gr.themes.GoogleFont("Inter"), "system-ui", "sans-serif"],
178
+ font_mono=[gr.themes.GoogleFont("JetBrains Mono"), "ui-monospace", "monospace"],
179
+ radius_size=gr.themes.sizes.radius_md,
180
+ ).set(
181
+ body_background_fill="#070b14",
182
+ body_background_fill_dark="#070b14",
183
+ body_text_color="#e2e8f0",
184
+ body_text_color_dark="#e2e8f0",
185
+ body_text_color_subdued="#94a3b8",
186
+ body_text_color_subdued_dark="#94a3b8",
187
+ background_fill_primary="#0c1322",
188
+ background_fill_primary_dark="#0c1322",
189
+ background_fill_secondary="#0a101d",
190
+ background_fill_secondary_dark="#0a101d",
191
+ block_background_fill="#0c1322",
192
+ block_background_fill_dark="#0c1322",
193
+ block_border_color="#1e293b",
194
+ block_border_color_dark="#1e293b",
195
+ block_border_width="1px",
196
+ block_label_text_color="#94a3b8",
197
+ block_label_text_color_dark="#94a3b8",
198
+ border_color_primary="#1e293b",
199
+ border_color_primary_dark="#1e293b",
200
+ input_background_fill="#0a101d",
201
+ input_background_fill_dark="#0a101d",
202
+ input_border_color="#243049",
203
+ input_border_color_dark="#243049",
204
+ input_border_color_focus="#22d3ee",
205
+ input_border_color_focus_dark="#22d3ee",
206
+ button_primary_background_fill="#22d3ee",
207
+ button_primary_background_fill_dark="#22d3ee",
208
+ button_primary_background_fill_hover="#67e8f9",
209
+ button_primary_background_fill_hover_dark="#67e8f9",
210
+ button_primary_text_color="#04111a",
211
+ button_primary_text_color_dark="#04111a",
212
+ button_secondary_background_fill="#111a2c",
213
+ button_secondary_background_fill_dark="#111a2c",
214
+ button_secondary_background_fill_hover="#16213a",
215
+ button_secondary_background_fill_hover_dark="#16213a",
216
+ button_secondary_text_color="#cbd5e1",
217
+ button_secondary_text_color_dark="#cbd5e1",
218
+ button_secondary_border_color="#243049",
219
+ button_secondary_border_color_dark="#243049",
220
+ color_accent_soft="#132238",
221
+ color_accent_soft_dark="#132238",
222
+ )
223
+
224
+ CUSTOM_CSS = """
225
+ @import url('https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@500;600;700&display=swap');
226
+
227
+ .gradio-container {
228
+ max-width: 1240px !important;
229
+ margin: 0 auto !important;
230
+ padding: 24px 20px 12px !important;
231
+ }
232
+
233
+ /* ---------- Header ---------- */
234
+ #dds-header {
235
+ display: flex;
236
+ align-items: center;
237
+ justify-content: space-between;
238
+ gap: 24px;
239
+ flex-wrap: wrap;
240
+ padding: 22px 28px;
241
+ border: 1px solid #1e293b;
242
+ border-top: 3px solid #22d3ee;
243
+ border-radius: 14px;
244
+ background:
245
+ radial-gradient(900px 220px at 100% 0%, rgba(139, 92, 246, 0.16), transparent 60%),
246
+ #0a101d;
247
+ }
248
+ #dds-header .brand {
249
+ display: flex;
250
+ align-items: center;
251
+ gap: 22px;
252
+ }
253
+ #dds-header .brand img {
254
+ height: 76px;
255
+ width: auto;
256
+ display: block;
257
+ }
258
+ #dds-header .divider {
259
+ width: 1px;
260
+ height: 48px;
261
+ background: #243049;
262
+ }
263
+ #dds-header h1 {
264
+ font-family: 'Space Grotesk', 'Inter', sans-serif;
265
+ font-size: 30px;
266
+ font-weight: 700;
267
+ letter-spacing: -0.02em;
268
+ margin: 0;
269
+ color: #f8fafc;
270
+ line-height: 1.1;
271
+ }
272
+ #dds-header p {
273
+ margin: 4px 0 0;
274
+ font-size: 14px;
275
+ color: #94a3b8;
276
+ max-width: 560px;
277
+ }
278
+ #dds-header .status {
279
+ display: flex;
280
+ align-items: center;
281
+ gap: 8px;
282
+ font-size: 13px;
283
+ color: #cbd5e1;
284
+ padding: 8px 14px;
285
+ border: 1px solid #243049;
286
+ border-radius: 999px;
287
+ background: #0c1322;
288
+ }
289
+ #dds-header .dot {
290
+ width: 8px;
291
+ height: 8px;
292
+ border-radius: 50%;
293
+ background: #22d3ee;
294
+ box-shadow: 0 0 0 4px rgba(34, 211, 238, 0.15);
295
+ }
296
+
297
+ /* ---------- Panels ---------- */
298
+ .dds-panel {
299
+ border: 1px solid #1e293b !important;
300
+ border-radius: 14px !important;
301
+ background: #0a101d !important;
302
+ padding: 18px !important;
303
+ }
304
+ .dds-panel-title h3 {
305
+ font-family: 'Space Grotesk', 'Inter', sans-serif;
306
+ font-size: 16px;
307
+ font-weight: 600;
308
+ color: #f1f5f9;
309
+ margin: 0 0 2px;
310
+ }
311
+ .dds-panel-title p {
312
+ font-size: 13px;
313
+ color: #94a3b8;
314
+ margin: 0;
315
+ }
316
+ .faq-group-label p {
317
+ font-size: 12px;
318
+ font-weight: 600;
319
+ color: #f5a623;
320
+ margin: 14px 0 6px !important;
321
+ }
322
+
323
+ /* FAQ buttons read like list items, not CTAs */
324
+ .faq-btn {
325
+ justify-content: flex-start !important;
326
+ text-align: left !important;
327
+ white-space: normal !important;
328
+ font-weight: 400 !important;
329
+ font-size: 13.5px !important;
330
+ line-height: 1.4 !important;
331
+ padding: 10px 12px !important;
332
+ border-left: 2px solid #243049 !important;
333
+ }
334
+ .faq-btn:hover {
335
+ border-left-color: #22d3ee !important;
336
+ color: #f8fafc !important;
337
+ }
338
+
339
+ .scope-note {
340
+ margin-top: 16px;
341
+ padding: 12px 14px;
342
+ border: 1px solid #2a2140;
343
+ border-left: 3px solid #8b5cf6;
344
+ border-radius: 10px;
345
+ background: #0f0c1d;
346
+ font-size: 13px;
347
+ color: #c4b5fd;
348
+ line-height: 1.5;
349
+ }
350
+
351
+ /* ---------- Chat ---------- */
352
+ #dds-chat {
353
+ border: 1px solid #1e293b !important;
354
+ border-radius: 12px !important;
355
+ background: #070b14 !important;
356
+ }
357
+ #dds-input textarea {
358
+ font-size: 15px !important;
359
+ }
360
+ #send-btn {
361
+ font-weight: 600 !important;
362
+ min-height: 44px;
363
+ }
364
+
365
+ /* ---------- Footer ---------- */
366
+ #dds-footer {
367
+ text-align: center;
368
+ font-size: 12.5px;
369
+ color: #64748b;
370
+ padding: 18px 0 4px;
371
+ border-top: 1px solid #1e293b;
372
+ margin-top: 8px;
373
+ }
374
+ #dds-footer a {
375
+ color: #22d3ee;
376
+ text-decoration: none;
377
+ }
378
+
379
+ footer { display: none !important; }
380
+
381
+ @media (max-width: 768px) {
382
+ #dds-header { padding: 18px; }
383
+ #dds-header .divider { display: none; }
384
+ #dds-header .brand { flex-direction: column; align-items: flex-start; gap: 12px; }
385
+ #dds-header h1 { font-size: 24px; }
386
+ }
387
+ """
388
+
389
+ HEADER_HTML = f"""
390
+ <div id="dds-header">
391
+ <div class="brand">
392
+ <img src="{DDS_LOGO_URL}" alt="Decoding Data Science">
393
+ <div class="divider"></div>
394
+ <div>
395
+ <h1>AYesha</h1>
396
+ <p>The DDS Enterprise HR assistant. Answers come only from the current DDS HR Handbook.</p>
397
+ </div>
398
+ </div>
399
+ <div class="status"><span class="dot"></span>Handbook connected</div>
400
+ </div>
401
+ """
402
+
403
+ FOOTER_HTML = """
404
+ <div id="dds-footer">
405
+ Built by <a href="https://decodingdatascience.com" target="_blank" rel="noopener">Decoding Data Science</a>
406
+ &nbsp;|&nbsp; Answers are generated from the DDS HR Handbook. Always confirm critical decisions with HR.
407
+ </div>
408
+ """
409
+
410
+ CHAT_PLACEHOLDER = """
411
+ <div style="text-align:center; padding: 40px 16px;">
412
+ <h3 style="font-family:'Space Grotesk',Inter,sans-serif; font-size:20px; color:#f1f5f9; margin:0 0 8px;">
413
+ Ask AYesha about DDS HR policy
414
+ </h3>
415
+ <p style="color:#94a3b8; font-size:14px; margin:0;">
416
+ Type a question below, or pick one from the common questions on the left.
417
+ </p>
418
+ </div>
419
+ """
420
+
421
+
422
+ with gr.Blocks(title="AYesha | DDS Enterprise HR Chatbot", fill_height=False) as demo:
423
+ pending_question = gr.State("")
424
+
425
+ gr.HTML(HEADER_HTML)
426
+
427
+ with gr.Row(equal_height=False):
428
+ # ---------- Sidebar: FAQ ----------
429
+ with gr.Column(scale=4, min_width=280, elem_classes="dds-panel"):
430
+ gr.Markdown(
431
+ "### Common questions\nClick any question to ask it instantly.",
432
+ elem_classes="dds-panel-title",
433
+ )
434
+ faq_buttons = []
435
+ for group_name, questions in FAQ_GROUPS.items():
436
+ gr.Markdown(group_name, elem_classes="faq-group-label")
437
+ for q in questions:
438
+ faq_buttons.append(
439
+ gr.Button(q, variant="secondary", size="sm", elem_classes="faq-btn")
440
+ )
441
+ gr.HTML(
442
+ '<div class="scope-note">AYesha only answers from the DDS HR Handbook. '
443
+ "For confidential matters or anything the handbook does not cover, "
444
+ "please contact the DDS HR team directly.</div>"
445
+ )
446
+
447
+ # ---------- Main: Chat ----------
448
+ with gr.Column(scale=8, min_width=360, elem_classes="dds-panel"):
449
+ chatbot = gr.Chatbot(
450
+ elem_id="dds-chat",
451
+ label="Conversation",
452
+ show_label=False,
453
+ height=520,
454
+ placeholder=CHAT_PLACEHOLDER,
455
+ buttons=["copy"],
456
+ feedback_options=None,
457
+ layout="bubble",
458
+ )
459
+ with gr.Row(equal_height=True):
460
+ msg = gr.Textbox(
461
+ elem_id="dds-input",
462
+ placeholder="e.g. How many days of annual leave do I get?",
463
+ show_label=False,
464
+ lines=1,
465
+ max_lines=4,
466
+ scale=8,
467
+ autofocus=True,
468
+ )
469
+ send_btn = gr.Button("Ask AYesha", variant="primary", scale=2, elem_id="send-btn")
470
+ with gr.Row():
471
+ gr.HTML("")
472
+ clear_btn = gr.Button("Clear conversation", variant="secondary", size="sm", scale=0, min_width=180)
473
+
474
+ gr.HTML(FOOTER_HTML)
475
+
476
+ # ---------- Events ----------
477
+ # Step 1 shows the question instantly; step 2 calls query_doc() for the answer.
478
+ for trigger in (msg.submit, send_btn.click):
479
+ trigger(
480
+ add_user_message,
481
+ inputs=[msg, chatbot],
482
+ outputs=[msg, chatbot, pending_question],
483
+ queue=False,
484
+ ).then(
485
+ add_bot_answer,
486
+ inputs=[pending_question, chatbot],
487
+ outputs=[chatbot],
488
+ )
489
+
490
+ # A button passed as an input sends its own label, i.e. the FAQ question text.
491
+ for btn in faq_buttons:
492
+ btn.click(
493
+ add_user_message,
494
+ inputs=[btn, chatbot],
495
+ outputs=[msg, chatbot, pending_question],
496
+ queue=False,
497
+ ).then(
498
+ add_bot_answer,
499
+ inputs=[pending_question, chatbot],
500
+ outputs=[chatbot],
501
+ )
502
+
503
+ clear_btn.click(clear_chat, outputs=[chatbot, msg, pending_question], queue=False)
504
+
505
+
506
+ if __name__ == "__main__":
507
+ # Gradio 6: theme and css are passed to launch(), not to gr.Blocks().
508
+ # PORT is read so the same file runs on Hugging Face Spaces (7860) and Render ($PORT).
509
+ demo.queue().launch(
510
+ server_name="0.0.0.0",
511
+ server_port=int(os.getenv("PORT", 7860)),
512
+ theme=dds_theme,
513
+ css=CUSTOM_CSS,
514
+ favicon_path=None,
515
+ )