finbot / server.py
prakhar146's picture
Update server.py
f55c190 verified
Raw
History Blame Contribute Delete
13.7 kB
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>"""
@app.get("/", response_class=HTMLResponse)
async def root():
return HTML
@app.post("/v1/chat/completions")
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)