import os import faiss import numpy as np import requests import gradio as gr from openai import OpenAI from pypdf import PdfReader from sentence_transformers import SentenceTransformer css = """ @import url('https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap'); *, *::before, *::after { box-sizing: border-box; } :root { --M: #E20074; --MD: #B5005C; --MBG: rgba(226,0,116,0.07); --bg: #F6F3F8; --white: #FFFFFF; --surf2: #F2EEF5; --border: #E6DEED; --border2: #D0C4DC; --text: #1A1525; --sub: #5A5272; --muted: #9990AA; --ok: #00A878; --okbg: #E8F8F3; --okborder: #AADFC8; --f: 'Plus Jakarta Sans', sans-serif; --mono: 'JetBrains Mono', monospace; --sh-sm: 0 1px 4px rgba(0,0,0,0.06); --sh-md: 0 3px 14px rgba(0,0,0,0.08); --sh-mg: 0 4px 20px rgba(226,0,116,0.18); } /* ── BASE ── */ body { font-family: var(--f) !important; background: var(--bg) !important; color: var(--text) !important; } .gradio-container { max-width: 1060px !important; margin: 0 auto !important; padding: 0 20px 48px !important; background: transparent !important; box-shadow: none !important; } /* ── HEADING (gr.Markdown title) ── */ h1 { font-family: var(--f) !important; font-size: 1.6rem !important; font-weight: 800 !important; letter-spacing: -0.03em !important; color: var(--text) !important; padding: 28px 0 4px !important; border-bottom: 3px solid var(--M) !important; margin-bottom: 24px !important; position: relative; } /* animated stripe under title */ h1::after { content: ''; position: absolute; bottom: -3px; left: 0; width: 80px; height: 3px; background: #FF6BB5; animation: titleSlide 2s ease-in-out infinite alternate; } @keyframes titleSlide { 0% { width: 60px; opacity: 0.6; } 100% { width: 160px; opacity: 1; } } /* emoji in title stays natural */ h1 .emoji { color: inherit !important; } /* ── ALL CARDS / PANELS ── */ .gr-box, .gr-panel, .gr-group, .gr-form, [class*="panel"], [class*="block"] { background: var(--white) !important; border: 1px solid var(--border) !important; border-radius: 12px !important; box-shadow: var(--sh-sm) !important; transition: box-shadow 0.2s, border-color 0.2s !important; } .gr-box:hover, .gr-panel:hover { box-shadow: 0 4px 20px rgba(226,0,116,0.10) !important; border-color: rgba(226,0,116,0.22) !important; } /* ── LABELS ── */ label, .gr-label, .label-wrap span { font-family: var(--f) !important; font-size: 0.63rem !important; font-weight: 700 !important; letter-spacing: 0.10em !important; text-transform: uppercase !important; color: var(--muted) !important; } /* ── INPUTS (link box + question box) ── */ textarea, input[type="text"] { font-family: var(--f) !important; background: var(--surf2) !important; color: var(--text) !important; border: 1px solid var(--border2) !important; border-radius: 8px !important; padding: 12px 15px !important; font-size: 0.88rem !important; line-height: 1.55 !important; transition: border-color 0.2s, box-shadow 0.2s !important; } textarea:focus, input[type="text"]:focus { border-color: var(--M) !important; box-shadow: 0 0 0 3px rgba(226,0,116,0.12) !important; outline: none !important; background: var(--white) !important; } textarea::placeholder, input[type="text"]::placeholder { color: var(--muted) !important; } /* Status textbox (readonly) gets green monospace style */ textarea[readonly] { font-family: var(--mono) !important; font-size: 0.76rem !important; color: var(--ok) !important; background: var(--okbg) !important; border-color: var(--okborder) !important; } /* ── BUTTONS ── */ button, .gr-button { font-family: var(--f) !important; font-weight: 700 !important; font-size: 0.75rem !important; letter-spacing: 0.08em !important; text-transform: uppercase !important; border-radius: 8px !important; padding: 12px 24px !important; border: none !important; cursor: pointer !important; transition: all 0.2s ease !important; } /* Primary = magenta gradient */ button.primary, .gr-button-primary, button[variant="primary"] { background: linear-gradient(135deg, #E20074 0%, #B5005C 100%) !important; color: #fff !important; box-shadow: var(--sh-mg) !important; } button.primary:hover, .gr-button-primary:hover { box-shadow: 0 6px 28px rgba(226,0,116,0.40) !important; transform: translateY(-1px) !important; } button.primary:active, .gr-button-primary:active { transform: translateY(0) !important; } /* ── CHATBOT CONTAINER ── */ .gr-chatbot, [data-testid="chatbot"] { background: #FAFAFA !important; border: 1px solid var(--border) !important; border-radius: 12px !important; box-shadow: var(--sh-sm) !important; } /* User bubble — right, magenta */ .message.user, [data-testid="user"] .message { background: linear-gradient(135deg, #E20074, #B5005C) !important; color: #fff !important; border-radius: 12px 12px 3px 12px !important; padding: 11px 15px !important; font-size: 0.86rem !important; line-height: 1.65 !important; max-width: 72% !important; margin-left: auto !important; box-shadow: 0 3px 14px rgba(226,0,116,0.22) !important; animation: slideR 0.22s ease !important; } /* Bot bubble — left, white with magenta stripe */ .message.bot, [data-testid="bot"] .message { background: var(--white) !important; border: 1px solid var(--border) !important; border-left: 3px solid var(--M) !important; color: var(--text) !important; border-radius: 2px 12px 12px 12px !important; padding: 11px 15px !important; font-size: 0.86rem !important; line-height: 1.65 !important; max-width: 80% !important; box-shadow: var(--sh-sm) !important; animation: slideL 0.22s ease !important; } @keyframes slideR { from { opacity: 0; transform: translateX(10px); } to { opacity: 1; transform: translateX(0); } } @keyframes slideL { from { opacity: 0; transform: translateX(-10px); } to { opacity: 1; transform: translateX(0); } } /* ── SCROLLBAR ── */ ::-webkit-scrollbar { width: 5px; height: 5px; } ::-webkit-scrollbar-track { background: var(--surf2); border-radius: 3px; } ::-webkit-scrollbar-thumb { background: rgba(226,0,116,0.28); border-radius: 3px; } ::-webkit-scrollbar-thumb:hover { background: var(--M); } /* ── HIDE GRADIO FOOTER ── */ footer { display: none !important; } .gap { gap: 14px !important; } """ # ── GLOBALS ──────────────────────────────────────────────────────────────────── embed_model = SentenceTransformer("all-MiniLM-L6-v2") index = None chunks = [] chat_history = [] client = OpenAI( api_key=os.getenv("GROQ_API_KEY"), base_url="https://api.groq.com/openai/v1", ) def convert_drive_link(link): try: return f"https://drive.google.com/uc?id={link.split('/d/')[1].split('/')[0]}" except Exception: return link def load_pdf_from_link(link): global index, chunks resp = requests.get(convert_drive_link(link), timeout=30) with open("temp.pdf", "wb") as f: f.write(resp.content) reader = PdfReader("temp.pdf") texts = [p.extract_text() for p in reader.pages if p.extract_text()] chunks = [] for t in texts: words = t.split() for i in range(0, len(words), 500): chunks.append(" ".join(words[i:i+500])) emb = embed_model.encode(chunks) index = faiss.IndexFlatL2(emb.shape[1]) index.add(np.array(emb, dtype="float32")) return f"✅ {len(chunks)} chunks indexed · {len(texts)} pages parsed · ready" def retrieve(query, k=3): if index is None: return [] q = embed_model.encode([query]) _, idx = index.search(np.array(q, dtype="float32"), k) return [chunks[i] for i in idx[0]] def generate_answer(query, ctx=""): if index is None: return "⚠️ Please load a PDF first." rag = "\n\n".join(retrieve(query)) prompt = f"""You are a senior financial analyst at Deutsche Telekom. You have memory of past conversation and access to official report data. Conversation history: {ctx} Report excerpts (cite ONLY from these for facts): {rag} Question: {query} Respond precisely, use numbers, and keep it concise.""" resp = client.chat.completions.create( model="llama-3.1-8b-instant", messages=[{"role": "user", "content": prompt}], temperature=0.4, max_tokens=650, ) return resp.choices[0].message.content def chat(user_input, history): global chat_history ctx = "\n".join(f"User: {h['user']}\nAssistant: {h['bot']}" for h in chat_history[-5:]) answer = generate_answer(user_input, ctx) chat_history.append({"user": user_input, "bot": answer}) new_history = history + [ {"role": "user", "content": user_input}, {"role": "assistant", "content": answer}, ] return new_history, new_history LOGO = "https://upload.wikimedia.org/wikipedia/commons/thumb/e/e0/Deutsche_Telekom_logo_simple.svg/120px-Deutsche_Telekom_logo_simple.svg.png" with gr.Blocks(title="Telekom Finance RAG", css=css) as app: gr.HTML(f"""