| 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; } |
| """ |
|
|
| |
| 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""" |
| <div class="app-header"> |
| <div class="header-brand"> |
| <div class="t-logo">T</div> |
| <div> |
| <div class="header-title">Finance <em>RAG</em> Intelligence</div> |
| <div class="header-sub">Deutsche Telekom Β· AI-Powered Document Analysis</div> |
| </div> |
| </div> |
| <div class="status-pill"> |
| <div class="pulse-dot"></div> |
| <span class="status-label">System Live</span> |
| </div> |
| </div> |
| <div class="info-chips"> |
| <div class="chip"><div class="chip-dot"></div>Groq Β· LLaMA 3 8B</div> |
| <div class="chip"><div class="chip-dot" style="background:#6600AA"></div>FAISS Vector Search</div> |
| <div class="chip"><div class="chip-dot" style="background:#00C896"></div>MiniLM-L6 Embeddings</div> |
| <div class="chip"><div class="chip-dot" style="background:#378ADD"></div>RAG Β· Top-K = 3</div> |
| <div class="chip"><div class="chip-dot" style="background:#FFB700"></div>Conversation Memory</div> |
| </div> |
| """) |
|
|
| gr.HTML('<div class="section-label">π Document Loader</div>') |
|
|
| with gr.Row(equal_height=True): |
| with gr.Column(scale=6): |
| link_input = gr.Textbox( |
| label="Google Drive PDF Link", |
| placeholder="https://drive.google.com/file/d/xxxxxxx/view", |
| ) |
| with gr.Column(scale=1, min_width=150): |
| load_btn = gr.Button("π₯ Load & Index", variant="primary") |
|
|
| status = gr.Textbox(label="System Status", interactive=False, placeholder="Waiting for PDFβ¦") |
|
|
| gr.HTML("<div style='height:5px;background:linear-gradient(90deg,#E20074,transparent);border-radius:3px;opacity:.4;margin:16px 0 20px'></div>") |
|
|
| gr.HTML('<div class="section-label">π€ AI Financial Analyst</div>') |
|
|
| chatbot = gr.Chatbot(height=460, show_label=False, avatar_images=(None, LOGO)) |
|
|
| with gr.Row(equal_height=True): |
| with gr.Column(scale=8): |
| msg = gr.Textbox( |
| show_label=False, |
| placeholder="Ask about revenue, EBITDA, KPIs, capex, strategic outlookβ¦", |
| container=False, |
| ) |
| with gr.Column(scale=1, min_width=120): |
| send_btn = gr.Button("Send β", variant="primary") |
|
|
| gr.HTML(""" |
| <div class="app-footer"> |
| <div class="f-text">Powered by <span>Groq</span> Β· <span>LLaMA 3</span> Β· <span>FAISS</span> Β· <span>MiniLM</span></div> |
| <div class="f-text">Deutsche Telekom AG Β· Internal AI Tool Β· Confidential</div> |
| </div> |
| """) |
|
|
| load_btn.click(load_pdf_from_link, inputs=link_input, outputs=status) |
| msg.submit(chat, inputs=[msg, chatbot], outputs=[chatbot, chatbot]) |
| msg.submit(lambda: "", outputs=msg) |
| send_btn.click(chat, inputs=[msg, chatbot], outputs=[chatbot, chatbot]) |
| send_btn.click(lambda: "", outputs=msg) |
|
|
| if __name__ == "__main__": |
| app.launch() |