# If needed in Colab, install first: # !pip install -U gradio pinecone llama-index llama-index-vector-stores-pinecone llama-index-readers-file pypdf from llama_index.core import VectorStoreIndex, SimpleDirectoryReader, StorageContext, Settings # --- Imports --- import logging import sys import gradio as gr import os from dotenv import load_dotenv from pinecone import Pinecone, ServerlessSpec from llama_index.core import VectorStoreIndex, SimpleDirectoryReader, StorageContext , Settings from llama_index.vector_stores.pinecone import PineconeVectorStore from llama_index.readers.file import PDFReader from llama_index.llms.openai import OpenAI from llama_index.embeddings.openai import OpenAIEmbedding # --- Logging --- logging.basicConfig(stream=sys.stdout, level=logging.INFO) load_dotenv(dotenv_path=".env") Settings.llm = OpenAI(model="gpt-4o-mini", temperature=0.2) Settings.embed_model = OpenAIEmbedding(model="text-embedding-ada-002") Settings.chunk_size = 600 Settings.chunk_overlap = 200 # Define a system prompt system_prompt = ''' You are Lumen, the AI-powered Dubai Utility Bill & Energy Assistant. Answer questions exclusively using the attached DEWA Utility Bill Knowledge Base (electricity slab tariff, water slab tariff, fuel surcharge, housing fee, sewerage fee, VAT, and conservation guidance). Base all responses on the information available in that knowledge base. Only respond to queries directly related to DEWA residential electricity and water billing as outlined in the knowledge base. - If a question pertains to topics outside DEWA utility billing (e.g. district cooling providers like Empower/Emicool, mortgage or visa topics, live account access), respond politely, clarifying that you are a DEWA utility bill assistant and only answer questions within that scope. - Never provide exact account-specific billing figures or access live DEWA account data — you may only give illustrative estimates based on the knowledge base's sample tariff logic. - Never give legally binding statements on disputed charges or refunds. - For questions you cannot answer (e.g., account-specific figures, disputed charges, or anything outside DEWA billing scope), politely decline and direct the user to email nipun.mecheng@gmail.com. - Never answer questions about anything outside of your scope. - Persist in following these constraints for any follow-up questions. - Before answering, carefully check that the information and query are within the allowed scope. Follow chain-of-thought reasoning: 1. First, reason step-by-step whether the question is covered in the current knowledge base and is within DEWA utility billing scope. 2. Only after confirming, produce a final answer. Format answers as concise, practical responses. Do not wrap answers in code blocks or any special formatting. Output requirements: - For allowed DEWA billing questions, answer concisely based only on the knowledge base, showing the slab logic when relevant (e.g. which consumption band applies). - For forbidden topics, output: “I’m sorry, I can only answer questions about DEWA electricity and water bills based on my knowledge base. For account-specific or other queries, please email nipun.mecheng@gmail.com.” **Example 1** User: I used 3,500 kWh this month — how much will my electricity bill be? Reasoning: This is a bill-estimation question covered by the slab tariff and fuel surcharge sections of the knowledge base. Final Answer: [Walk through the slab bands, apply the fuel surcharge and VAT, and give an illustrative estimate] **Example 2** User: Can you pull up my actual DEWA account balance? Reasoning: This requires live account-specific data, which is outside this bot's scope. Final Answer: I’m sorry, I can only answer questions about DEWA electricity and water bills based on my knowledge base. For account-specific or other queries, please email nipun.mecheng@gmail.com. **Example 3** User: What's the best district cooling provider in my building? Reasoning: District cooling (Empower/Emicool) is explicitly out of scope for this DEWA-focused bot. Final Answer: I’m sorry, I only answer questions about DEWA electricity and water bills as outlined in my knowledge base. (Real-world examples should be longer and use precise wording from the knowledge base where appropriate.) **Important instructions:** - Only answer questions directly supported by the DEWA Utility Bill Knowledge Base. - Decline politely and redirect to the provided email address for any questions outside scope or for account-specific/confidential information. - Always reason before concluding. Only present the answer after checking scope and source. Remember: As Lumen, the Dubai Utility Bill & Energy Assistant, you must never provide information outside the authorized DEWA billing knowledge base and always respond respectfully according to these constraints. ''' # ── OpenAI API Key ──────────────────────────────────────────────── OPENAI_API_KEY = os.environ.get("OPENAI_API_KEY") PINECONE_API_KEY = os.environ.get("PINECONE_API_KEY") # --- Initialize Pinecone --- pc = Pinecone(api_key=PINECONE_API_KEY) index_name = "lumen-dewa-index" dimension = 1536 # --- Create Pinecone index only if it does not exist --- existing_indexes = [idx["name"] for idx in pc.list_indexes()] if index_name not in existing_indexes: pc.create_index( name=index_name, dimension=dimension, metric="euclidean", spec=ServerlessSpec(cloud="aws", region="us-east-1"), ) pinecone_index = pc.Index(index_name) # --- Load PDF documents from folder --- # Place the DEWA Utility Bill Knowledge Base PDF(s) inside the "data" folder. documents = SimpleDirectoryReader( input_dir="data", required_exts=[".pdf"], file_extractor={".pdf": PDFReader()} ).load_data() if not documents: raise ValueError("No PDF documents were loaded from the 'data' folder. Add your DEWA knowledge base PDF(s) to /data.") # --- Create Vector Index --- vector_store = PineconeVectorStore(pinecone_index=pinecone_index) storage_context = StorageContext.from_defaults(vector_store=vector_store) index = VectorStoreIndex.from_documents( documents, storage_context=storage_context ) # --- Query Engine --- query_engine = index.as_query_engine(system_prompt=system_prompt) # --- Gradio App --- def query_doc(prompt): try: response = query_engine.query(prompt) return str(response) except Exception as e: return f"Error: {str(e)}" # ========================== # DDS Enterprise UI # ========================== custom_css = """ * { font-family: 'Segoe UI', system-ui, sans-serif !important; } footer { display: none !important; } /* ── BANNER ── */ .dora-banner { position: relative; overflow: hidden; background: linear-gradient(135deg, #0a0f1e 0%, #0d1f3c 40%, #0a1628 100%); border-radius: 0 0 32px 32px; padding: 0 0 8px 0; margin-bottom: 24px; } .dora-banner-grid { position: absolute; inset: 0; background-image: linear-gradient(rgba(99,179,255,0.07) 1px, transparent 1px), linear-gradient(90deg, rgba(99,179,255,0.07) 1px, transparent 1px); background-size: 48px 48px; pointer-events: none; } .dora-banner-glow { position: absolute; top: -80px; left: 50%; transform: translateX(-50%); width: 600px; height: 300px; background: radial-gradient(ellipse, rgba(56,130,246,0.18) 0%, transparent 70%); pointer-events: none; } .dora-banner-inner { position: relative; z-index: 2; display: flex; align-items: center; justify-content: space-between; padding: 28px 40px 20px 40px; gap: 20px; } .dora-brand { display: flex; align-items: center; gap: 18px; } .dora-logo-wrap { width: 64px; height: 64px; border-radius: 18px; background: linear-gradient(135deg, #1a3a6e, #0f2347); border: 1.5px solid rgba(99,179,255,0.35); display: flex; align-items: center; justify-content: center; box-shadow: 0 0 28px rgba(56,130,246,0.25); flex-shrink: 0; } .dora-logo-wrap img { width: 38px; height: 38px; filter: brightness(0) invert(1) opacity(0.9); } .dora-brand-text {} .dora-brand-text .eyebrow { font-family: 'Sora', sans-serif !important; font-size: 10px; font-weight: 600; letter-spacing: 3px; text-transform: uppercase; color: rgba(99,179,255,0.75); margin-bottom: 4px; } .dora-brand-text .main-title { font-family: 'Sora', sans-serif !important; font-size: 26px; font-weight: 700; color: #ffffff; line-height: 1.15; letter-spacing: -0.4px; } .dora-brand-text .main-title span { color: #63b3ff; } .dora-brand-text .sub-title { font-size: 13px; color: rgba(180,210,255,0.6); margin-top: 3px; font-weight: 300; } .dora-badges { display: flex; flex-direction: column; align-items: flex-end; gap: 8px; } .dora-badge { display: inline-flex; align-items: center; gap: 6px; padding: 5px 13px; border-radius: 50px; font-size: 11.5px; font-weight: 500; letter-spacing: 0.3px; border: 1px solid; } .dora-badge.blue { background: rgba(56,130,246,0.12); color: #89c4ff; border-color: rgba(56,130,246,0.3); } .dora-badge.green { background: rgba(34,197,120,0.1); color: #6ee7b7; border-color: rgba(34,197,120,0.25); } .dora-badge.amber { background: rgba(234,179,8,0.1); color: #fcd34d; border-color: rgba(234,179,8,0.25); } .dora-divider { height: 1px; margin: 0 40px; background: linear-gradient(90deg, transparent, rgba(99,179,255,0.25), transparent); } .dora-stats-bar { display: flex; align-items: center; gap: 32px; padding: 14px 40px 4px 40px; } .dora-stat { display: flex; align-items: center; gap: 7px; } .dora-stat .dot { width: 7px; height: 7px; border-radius: 50%; } .dora-stat .dot.green { background: #22c55e; box-shadow: 0 0 6px #22c55e; } .dora-stat .dot.blue { background: #63b3ff; } .dora-stat .dot.amber { background: #fbbf24; } .dora-stat .stat-label { font-size: 11.5px; color: rgba(180,210,255,0.55); font-weight: 300; } .dora-stat .stat-val { font-size: 11.5px; color: rgba(180,210,255,0.85); font-weight: 500; } /* ── SIDEBAR PANEL ── */ .sidebar-panel { background: linear-gradient(160deg, #0e1929 0%, #0a1220 100%) !important; border: 1px solid rgba(99,179,255,0.12) !important; border-radius: 18px !important; padding: 20px !important; } .sidebar-section-title { font-family: 'Sora', sans-serif !important; font-size: 11px !important; font-weight: 600 !important; letter-spacing: 2.5px !important; text-transform: uppercase !important; color: rgba(99,179,255,0.6) !important; margin-bottom: 14px !important; margin-top: 0 !important; } .topic-chip { display: flex; align-items: center; gap: 9px; padding: 9px 13px; margin-bottom: 7px; background: rgba(56,130,246,0.07); border: 1px solid rgba(56,130,246,0.15); border-radius: 10px; font-size: 13px; color: rgba(180,210,255,0.8); transition: all 0.2s; } .topic-chip:hover { background: rgba(56,130,246,0.13); border-color: rgba(99,179,255,0.3); color: #a8d4ff; } .topic-icon { font-size: 15px; flex-shrink: 0; } .alert-box { margin-top: 18px; padding: 12px 14px; background: rgba(234,179,8,0.07); border: 1px solid rgba(234,179,8,0.2); border-radius: 10px; border-left: 3px solid rgba(234,179,8,0.55); } .alert-box p { font-size: 12px !important; color: rgba(252,211,77,0.8) !important; margin: 0 !important; line-height: 1.5 !important; } /* ── CHAT AREA ── */ .chat-card { background: linear-gradient(160deg, #0e1929 0%, #0a1220 100%) !important; border: 1px solid rgba(99,179,255,0.12) !important; border-radius: 18px !important; padding: 24px !important; } .chat-input-label { font-family: 'Sora', sans-serif !important; font-size: 11px !important; font-weight: 600 !important; letter-spacing: 2px !important; text-transform: uppercase !important; color: rgba(99,179,255,0.6) !important; margin-bottom: 8px !important; } .ask-box textarea { background: rgba(255,255,255,0.04) !important; border: 1px solid rgba(99,179,255,0.2) !important; border-radius: 12px !important; color: rgba(220,235,255,0.9) !important; font-size: 14px !important; padding: 14px 16px !important; transition: border-color 0.2s !important; resize: none !important; } .ask-box textarea:focus { border-color: rgba(99,179,255,0.5) !important; box-shadow: 0 0 0 3px rgba(56,130,246,0.1) !important; outline: none !important; } .ask-box textarea::placeholder { color: rgba(140,170,210,0.35) !important; } .response-box textarea { background: rgba(255,255,255,0.025) !important; border: 1px solid rgba(99,179,255,0.1) !important; border-radius: 12px !important; color: rgba(210,230,255,0.85) !important; font-size: 14px !important; line-height: 1.7 !important; padding: 14px 16px !important; } /* Buttons */ .btn-ask { background: linear-gradient(135deg, #1a56db, #1e40af) !important; color: #fff !important; border: none !important; border-radius: 12px !important; font-family: 'Sora', sans-serif !important; font-weight: 600 !important; font-size: 14px !important; letter-spacing: 0.3px !important; transition: all 0.2s !important; box-shadow: 0 4px 18px rgba(26,86,219,0.35) !important; } .btn-ask:hover { transform: translateY(-1px) !important; box-shadow: 0 6px 24px rgba(26,86,219,0.45) !important; } .btn-clear { background: rgba(255,255,255,0.04) !important; color: rgba(180,210,255,0.6) !important; border: 1px solid rgba(99,179,255,0.15) !important; border-radius: 12px !important; font-size: 14px !important; transition: all 0.2s !important; } .btn-clear:hover { background: rgba(255,255,255,0.08) !important; color: rgba(180,210,255,0.85) !important; } /* Examples */ .gr-examples .gr-button { background: rgba(56,130,246,0.07) !important; border: 1px solid rgba(56,130,246,0.2) !important; border-radius: 8px !important; color: rgba(150,200,255,0.8) !important; font-size: 12px !important; padding: 6px 12px !important; transition: all 0.2s !important; } .gr-examples .gr-button:hover { background: rgba(56,130,246,0.15) !important; border-color: rgba(99,179,255,0.4) !important; } /* ── TABS ── */ .tabs > .tab-nav { background: transparent !important; border-bottom: 1px solid rgba(99,179,255,0.12) !important; padding: 0 8px !important; gap: 4px !important; } .tabs > .tab-nav button { font-family: 'Sora', sans-serif !important; font-size: 13px !important; font-weight: 500 !important; color: rgba(140,175,220,0.55) !important; border-radius: 10px 10px 0 0 !important; padding: 10px 20px !important; border: none !important; background: transparent !important; transition: all 0.2s !important; } .tabs > .tab-nav button.selected { color: #89c4ff !important; background: rgba(56,130,246,0.1) !important; border-bottom: 2px solid #4a90e2 !important; } /* ── ACCORDION (FAQ) ── */ .gr-accordion { background: rgba(255,255,255,0.03) !important; border: 1px solid rgba(99,179,255,0.1) !important; border-radius: 12px !important; margin-bottom: 10px !important; } .gr-accordion .label-wrap { font-family: 'Sora', sans-serif !important; font-weight: 500 !important; color: rgba(180,215,255,0.85) !important; font-size: 14px !important; } /* ── FOOTER ── */ .dora-footer { background: linear-gradient(135deg, #080e1a 0%, #0a1220 100%); border-top: 1px solid rgba(99,179,255,0.1); border-radius: 24px 24px 0 0; margin-top: 32px; padding: 32px 40px 20px 40px; } .footer-grid { display: grid; grid-template-columns: 1.8fr 1fr 1fr; gap: 32px; margin-bottom: 24px; } .footer-col-title { font-family: 'Sora', sans-serif; font-size: 11px; font-weight: 600; letter-spacing: 2.5px; text-transform: uppercase; color: rgba(99,179,255,0.5); margin-bottom: 14px; } .footer-about-name { font-family: 'Sora', sans-serif; font-size: 16px; font-weight: 700; color: #c8e0ff; margin-bottom: 4px; } .footer-about-role { font-size: 12px; color: rgba(140,180,230,0.55); margin-bottom: 12px; font-weight: 300; } .footer-about-desc { font-size: 12.5px; color: rgba(160,200,240,0.5); line-height: 1.65; font-weight: 300; } .footer-link { display: flex; align-items: center; gap: 8px; font-size: 12.5px; color: rgba(150,190,235,0.6); margin-bottom: 9px; text-decoration: none; transition: color 0.2s; } .footer-link:hover { color: #89c4ff; } .footer-link .link-icon { width: 22px; height: 22px; border-radius: 6px; background: rgba(56,130,246,0.12); border: 1px solid rgba(56,130,246,0.2); display: flex; align-items: center; justify-content: center; font-size: 12px; flex-shrink: 0; } .footer-tech-pill { display: inline-flex; align-items: center; gap: 5px; padding: 4px 10px; background: rgba(56,130,246,0.08); border: 1px solid rgba(56,130,246,0.15); border-radius: 6px; font-size: 11.5px; color: rgba(150,195,245,0.7); margin: 0 5px 6px 0; } .footer-bottom { border-top: 1px solid rgba(99,179,255,0.07); padding-top: 18px; display: flex; align-items: center; justify-content: space-between; } .footer-copyright { font-size: 11.5px; color: rgba(120,160,210,0.35); } .footer-dds-brand { font-family: 'Sora', sans-serif; font-size: 11.5px; color: rgba(99,179,255,0.45); letter-spacing: 1px; } /* General dark fixes */ body, .gradio-container { background: #070d18 !important; } label, .gr-form label { color: rgba(150,195,245,0.7) !important; font-size: 12px !important; font-weight: 500 !important; letter-spacing: 0.5px !important; } """ # ── THEME ── theme = gr.themes.Base( primary_hue=gr.themes.colors.blue, neutral_hue=gr.themes.colors.slate, font=[gr.themes.GoogleFont("DM Sans"), "sans-serif"], ).set( body_background_fill="#070d18", block_background_fill="#0e1929", block_border_color="rgba(99,179,255,0.12)", block_label_text_color="rgba(150,195,245,0.7)", input_background_fill="rgba(255,255,255,0.04)", input_border_color="rgba(99,179,255,0.2)", input_border_color_focus="rgba(99,179,255,0.5)", button_primary_background_fill="linear-gradient(135deg,#1a56db,#1e40af)", button_primary_text_color="#ffffff", button_secondary_background_fill="rgba(255,255,255,0.04)", button_secondary_text_color="rgba(180,210,255,0.7)", border_color_primary="rgba(99,179,255,0.15)", color_accent_soft="rgba(56,130,246,0.1)", ) with gr.Blocks(css=custom_css, theme=theme) as demo: # ══════════════════════════════════════════ # BANNER / HEADER # ══════════════════════════════════════════ gr.HTML("""
Lumen AI Logo
Dubai  •  Utility Bill & Energy AI
Lumen AI — Dubai Utility Bill Assistant
AI-powered DEWA Bill & Energy Support
⚡ RAG-Powered ● Live 💡 Energy Focused
Status Operational
""") # ══════════════════════════════════════════ # CHAT # ══════════════════════════════════════════ with gr.Row(equal_height=True): # LEFT SIDEBAR with gr.Column(scale=1, elem_classes="sidebar-panel"): gr.HTML("""
Electricity Slab Tariff
💧 Water Slab Tariff
Fuel Surcharge
🏠 Housing & Sewerage Fee
🧾 VAT on Bills
🌱 Conservation Tips

⚠️  This assistant exclusively answers DEWA electricity & water bill questions from its knowledge base. For account-specific or confidential queries, email nipun.mecheng@gmail.com

""") # RIGHT CHAT PANEL with gr.Column(scale=2, elem_classes="chat-card"): question = gr.Textbox( label="Ask About Your DEWA Bill", placeholder="e.g. I used 3,500 kWh this month — how much will my bill be? What is the fuel surcharge?", lines=3, elem_classes="ask-box" ) answer = gr.Textbox( label="Lumen's Response", lines=10, elem_classes="response-box" ) with gr.Row(): ask_btn = gr.Button("🚀 Send Question", variant="primary", elem_classes="btn-ask") clear_btn = gr.Button("✕ Clear", elem_classes="btn-clear") gr.Examples( label="Quick Questions", examples=[ ["I used 3,500 kWh this month — how much will my electricity bill be?"], ["Why did my bill double in July compared to February?"], ["What's the difference between residential and commercial water tariffs?"], ["What is the fuel surcharge and how is it applied?"], ["How can I reduce my DEWA bill this summer?"], ], inputs=question ) ask_btn.click(fn=query_doc, inputs=question, outputs=answer) question.submit(fn=query_doc, inputs=question, outputs=answer) clear_btn.click(lambda: ("", ""), outputs=[question, answer]) # ══════════════════════════════════════════ # FOOTER # ══════════════════════════════════════════ gr.HTML(""" """) demo.launch()