Spaces:
Sleeping
Sleeping
| import os | |
| import uuid | |
| import time | |
| import json | |
| import urllib.request | |
| from fastapi import FastAPI, Request | |
| from fastapi.responses import HTMLResponse, JSONResponse, StreamingResponse | |
| from llama_cpp import Llama | |
| import uvicorn | |
| import xml.etree.ElementTree as ET | |
| from datetime import datetime | |
| # --- Config --- | |
| MODEL_PATH = "/app/model.gguf" | |
| MODEL_URL = "https://huggingface.co/prakhar146/indian-finance-llm-v1/resolve/main/qwen2.5-7b-instruct-Q4_K_M.gguf?download=true" | |
| # --- Top 1% System Prompt (Short = Smart) --- | |
| ELITE_SYSTEM_PROMPT = """You are FinBot, a world-class Indian financial advisor. | |
| - Respond only in professional English. | |
| - Use Markdown formatting (bold, lists) for perfect readability. Do not use tables. | |
| - Be concise, factual, and actionable. No fluff.""" | |
| # --- Live Micro-Stuffing Engine --- | |
| cached_news = "" | |
| last_fetch_time = None | |
| def get_market_news(): | |
| global cached_news, last_fetch_time | |
| now = datetime.now() | |
| # Fetch only once per hour to avoid rate limits | |
| if last_fetch_time and (now - last_fetch_time).seconds < 3600 and cached_news: | |
| return cached_news | |
| try: | |
| url = "https://economictimes.indiatimes.com/markets/rssfeeds/1977021501.cms" | |
| req = urllib.request.Request(url, headers={'User-Agent': 'Mozilla/5.0'}) | |
| response = urllib.request.urlopen(req, timeout=5) | |
| root = ET.fromstring(response.read()) | |
| headlines = [item.find('title').text for item in root.findall('.//item')[:3]] | |
| cached_news = "\n".join([f"- {h}" for h in headlines]) | |
| last_fetch_time = now | |
| return cached_news | |
| except: | |
| return cached_news if cached_news else "Market data temporarily unavailable." | |
| # --- Live Stock Price Tool --- | |
| STOCK_MAP = { | |
| "reliance": "RELIANCE.NS", "tcs": "TCS.NS", "infosys": "INFY.NS", "infy": "INFY.NS", | |
| "hdfc": "HDFCBANK.NS", "icici": "ICICIBANK.NS", "sbi": "SBIN.NS", | |
| "tata motors": "TATAMOTORS.NS", "itc": "ITC.NS", "wipro": "WIPRO.NS", | |
| "bajaj finance": "BAJFINANCE.NS", "adani": "ADANIENT.NS", "nifty": "^NSEI", | |
| "sensex": "^BSESN" | |
| } | |
| def get_live_price(query): | |
| query_lower = query.lower() | |
| ticker = None | |
| for name, tick in STOCK_MAP.items(): | |
| if name in query_lower: | |
| ticker = tick | |
| break | |
| if not ticker: | |
| return None | |
| try: | |
| url = f"https://query1.finance.yahoo.com/v8/finance/chart/{ticker}?range=1d&interval=1m" | |
| req = urllib.request.Request(url, headers={'User-Agent': 'Mozilla/5.0'}) | |
| response = urllib.request.urlopen(req, timeout=5) | |
| data = json.loads(response.read()) | |
| price = data['chart']['result'][0]['meta']['regularMarketPrice'] | |
| symbol = data['chart']['result'][0]['meta']['symbol'] | |
| return f"[Live Data: {symbol} is currently trading at ₹{price}]" | |
| except: | |
| return None | |
| # --- Download model if it doesn't exist --- | |
| if not os.path.exists(MODEL_PATH): | |
| print("⏳ Downloading model...") | |
| urllib.request.urlretrieve(MODEL_URL, MODEL_PATH) | |
| print("✅ Model downloaded!") | |
| # --- Load model (MAXIMUM SPEED) --- | |
| print("⏳ Loading model...") | |
| llm = Llama( | |
| model_path=MODEL_PATH, | |
| n_ctx=2048, | |
| n_batch=512, | |
| n_threads=2, | |
| n_threads_batch=2, | |
| use_mlock=True, | |
| use_mmap=True, | |
| verbose=False | |
| ) | |
| print("✅ Model loaded!") | |
| app = FastAPI(title="FinBot") | |
| # --- Top 1% Premium UI --- | |
| HTML = """<!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>FinBot | Elite Finance AI</title> | |
| <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script> | |
| <style> | |
| @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap'); | |
| * { box-sizing: border-box; margin: 0; padding: 0; } | |
| body { | |
| font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif; | |
| background: #f7f7f8; | |
| color: #1a1a1a; | |
| height: 100vh; | |
| display: flex; | |
| justify-content: center; | |
| } | |
| .app { | |
| width: 100%; | |
| max-width: 800px; | |
| height: 100%; | |
| display: flex; | |
| flex-direction: column; | |
| background: #ffffff; | |
| box-shadow: 0 0 40px rgba(0,0,0,0.04); | |
| } | |
| .header { | |
| padding: 20px 24px; | |
| border-bottom: 1px solid #eaeaea; | |
| display: flex; | |
| align-items: center; | |
| gap: 12px; | |
| } | |
| .logo { | |
| width: 36px; height: 36px; | |
| background: linear-gradient(135deg, #6366f1, #8b5cf6); | |
| border-radius: 10px; | |
| display: flex; align-items: center; justify-content: center; | |
| color: white; font-weight: 700; font-size: 1.1em; | |
| box-shadow: 0 4px 12px rgba(99, 102, 241, 0.2); | |
| } | |
| .title { font-size: 1.15em; font-weight: 700; color: #1a1a1a; } | |
| .subtitle { font-size: 0.8em; color: #6b7280; font-weight: 500; } | |
| .chat-box { | |
| flex: 1; | |
| overflow-y: auto; | |
| padding: 24px 0; | |
| display: flex; | |
| flex-direction: column; | |
| } | |
| .message-row { | |
| padding: 12px 24px; | |
| display: flex; | |
| gap: 16px; | |
| animation: fadeSlide 0.3s ease; | |
| } | |
| @keyframes fadeSlide { | |
| from { opacity: 0; transform: translateY(8px); } | |
| to { opacity: 1; transform: translateY(0); } | |
| } | |
| .avatar { | |
| width: 32px; height: 32px; | |
| border-radius: 50%; | |
| display: flex; align-items: center; justify-content: center; | |
| font-size: 1em; flex-shrink: 0; | |
| margin-top: 2px; | |
| } | |
| .avatar.bot { background: #f3f4f6; } | |
| .avatar.user { background: #6366f1; color: white; font-size: 0.8em; font-weight: 600; } | |
| .message-content { | |
| flex: 1; | |
| line-height: 1.7; | |
| font-size: 0.95em; | |
| } | |
| .message-content.user-msg { | |
| font-weight: 500; | |
| } | |
| /* Premium Markdown Rendering */ | |
| .message-content h1, .message-content h2, .message-content h3 { font-size: 1.05em; font-weight: 700; margin: 10px 0 4px 0; color: #111; } | |
| .message-content ul, .message-content ol { margin: 4px 0 4px 20px; } | |
| .message-content li { margin-bottom: 4px; } | |
| .message-content strong { color: #6366f1; font-weight: 600; } | |
| .message-content p { margin: 4px 0; } | |
| .message-content code { background: #f3f4f6; padding: 2px 6px; border-radius: 4px; font-size: 0.9em; color: #6366f1; } | |
| .typing-indicator { | |
| display: flex; gap: 5px; padding-top: 6px; | |
| } | |
| .typing-indicator span { | |
| width: 7px; height: 7px; background: #d1d5db; border-radius: 50%; | |
| animation: bounce 1.2s infinite; | |
| } | |
| .typing-indicator span:nth-child(2) { animation-delay: 0.2s; } | |
| .typing-indicator span:nth-child(3) { animation-delay: 0.4s; } | |
| @keyframes bounce { | |
| 0%, 60%, 100% { transform: translateY(0); } | |
| 30% { transform: translateY(-6px); } | |
| } | |
| .input-area { | |
| padding: 20px 24px; | |
| background: #ffffff; | |
| border-top: 1px solid #eaeaea; | |
| } | |
| .input-wrapper { | |
| display: flex; | |
| align-items: center; | |
| background: #f7f7f8; | |
| border: 1px solid #e5e7eb; | |
| border-radius: 16px; | |
| padding: 4px 4px 4px 18px; | |
| transition: all 0.2s; | |
| } | |
| .input-wrapper:focus-within { | |
| border-color: #6366f1; | |
| background: #ffffff; | |
| box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.1); | |
| } | |
| input { | |
| flex: 1; border: none; background: transparent; | |
| font-size: 1em; outline: none; color: #1a1a1a; font-family: 'Inter', sans-serif; | |
| padding: 8px 0; | |
| } | |
| input::placeholder { color: #9ca3af; } | |
| .send-btn { | |
| width: 42px; height: 42px; | |
| background: #6366f1; border: none; border-radius: 12px; | |
| color: white; cursor: pointer; display: flex; align-items: center; justify-content: center; | |
| transition: all 0.2s; | |
| } | |
| .send-btn:hover { background: #4f46e5; transform: scale(1.05); } | |
| .send-btn:disabled { background: #d1d5db; cursor: not-allowed; transform: none; } | |
| .arrow-up { | |
| width: 0; height: 0; | |
| border-left: 6px solid transparent; border-right: 6px solid transparent; | |
| border-bottom: 8px solid white; | |
| } | |
| /* Scrollbar */ | |
| .chat-box::-webkit-scrollbar { width: 6px; } | |
| .chat-box::-webkit-scrollbar-track { background: transparent; } | |
| .chat-box::-webkit-scrollbar-thumb { background: #e5e7eb; border-radius: 3px; } | |
| .welcome { text-align: center; padding: 60px 24px; color: #6b7280; } | |
| .welcome h2 { color: #1a1a1a; font-size: 1.4em; margin-bottom: 8px; } | |
| .welcome p { font-size: 0.95em; } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="app"> | |
| <div class="header"> | |
| <div class="logo">₹</div> | |
| <div> | |
| <div class="title">FinBot</div> | |
| <div class="subtitle">Elite Indian Finance AI</div> | |
| </div> | |
| </div> | |
| <div class="chat-box" id="chatBox"> | |
| <div class="welcome"> | |
| <h2>Welcome to FinBot</h2> | |
| <p>Your elite Indian financial advisor. Ask anything about finance.</p> | |
| </div> | |
| </div> | |
| <div class="input-area"> | |
| <div class="input-wrapper"> | |
| <input type="text" id="inp" placeholder="Ask about taxes, stocks, mutual funds..." onkeypress="if(event.key==='Enter')send()"> | |
| <button class="send-btn" id="btn" onclick="send()" disabled> | |
| <div class="arrow-up"></div> | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| const box = document.getElementById('chatBox'); | |
| const inp = document.getElementById('inp'); | |
| const btn = document.getElementById('btn'); | |
| inp.addEventListener('input', () => { | |
| btn.disabled = inp.value.trim() === ''; | |
| }); | |
| function scrollToBottom() { | |
| box.scrollTop = box.scrollHeight; | |
| } | |
| async function send() { | |
| const text = inp.value.trim(); | |
| if (!text) return; | |
| // Remove welcome message | |
| const welcome = document.querySelector('.welcome'); | |
| if (welcome) welcome.remove(); | |
| inp.value = ''; | |
| btn.disabled = true; | |
| // User message | |
| box.innerHTML += ` | |
| <div class="message-row"> | |
| <div class="avatar user">P</div> | |
| <div class="message-content user-msg">${text}</div> | |
| </div>`; | |
| scrollToBottom(); | |
| // Bot placeholder | |
| const botRowId = 'bot-' + Date.now(); | |
| box.innerHTML += ` | |
| <div class="message-row" id="${botRowId}"> | |
| <div class="avatar bot">₹</div> | |
| <div class="message-content"> | |
| <div class="typing-indicator"><span></span><span></span><span></span></div> | |
| </div> | |
| </div>`; | |
| scrollToBottom(); | |
| const botContent = document.querySelector(`#${botRowId} .message-content`); | |
| let fullText = ''; | |
| try { | |
| const res = await fetch('/v1/chat/completions', { | |
| method: 'POST', | |
| headers: {'Content-Type': 'application/json'}, | |
| body: JSON.stringify({ messages: [{role:'user', content:text}], stream: true }) | |
| }); | |
| const reader = res.body.getReader(); | |
| const decoder = new TextDecoder(); | |
| let buffer = ''; | |
| let isFirstToken = true; | |
| while (true) { | |
| const { done, value } = await reader.read(); | |
| if (done) break; | |
| buffer += decoder.decode(value, { stream: true }); | |
| const lines = buffer.split('\\n'); | |
| buffer = lines.pop(); | |
| for (const line of lines) { | |
| if (line.startsWith('data: ')) { | |
| const data = line.slice(6).trim(); | |
| if (data === '[DONE]') continue; | |
| try { | |
| const json = JSON.parse(data); | |
| const token = json.choices[0].delta.content; | |
| if (token) { | |
| if (isFirstToken) { | |
| botContent.innerHTML = ''; | |
| isFirstToken = false; | |
| } | |
| fullText += token; | |
| botContent.innerHTML = marked.parse(fullText); | |
| scrollToBottom(); | |
| } | |
| } catch(e) {} | |
| } | |
| } | |
| } | |
| } catch(e) { | |
| botContent.innerHTML = '<span style="color:#ef4444">Connection error. Please try again.</span>'; | |
| } finally { | |
| inp.value = ''; | |
| btn.disabled = true; | |
| } | |
| } | |
| </script> | |
| </body> | |
| </html>""" | |
| async def root(): | |
| return HTML | |
| async def chat(request: Request): | |
| body = await request.json() | |
| messages = body.get("messages", []) | |
| use_stream = body.get("stream", False) | |
| # ReAct Tool Router: Check if user wants a live price | |
| live_price_data = get_live_price(messages[-1].get("content", "")) | |
| if live_price_data: | |
| messages[-1]["content"] = f"{live_price_data}\nUser Query: {messages[-1]['content']}" | |
| if not messages or messages[0].get("role") != "system": | |
| live_news = get_market_news() | |
| dynamic_prompt = ELITE_SYSTEM_PROMPT + f"\n\n**Latest Indian Market Headlines (Live):**\n{live_news}" | |
| messages.insert(0, {"role": "system", "content": dynamic_prompt}) | |
| if use_stream: | |
| def generate(): | |
| response = llm.create_chat_completion( | |
| messages=messages, | |
| max_tokens=1024, | |
| temperature=0.7, | |
| top_p=0.9, | |
| stream=True | |
| ) | |
| for chunk in response: | |
| # The correct format for create_chat_completion stream | |
| if "content" in chunk["choices"][0]["delta"]: | |
| yield f"data: {json.dumps(chunk)}\n\n" | |
| yield "data: [DONE]\n\n" | |
| return StreamingResponse(generate(), media_type="text/event-stream") | |
| else: | |
| response = llm.create_chat_completion( | |
| messages=messages, | |
| max_tokens=1024, | |
| temperature=0.7, | |
| top_p=0.9 | |
| ) | |
| return JSONResponse(response) | |
| if __name__ == "__main__": | |
| uvicorn.run(app, host="0.0.0.0", port=7860) |