Spaces:
Sleeping
Sleeping
Update src/streamlit_app.py
Browse files- src/streamlit_app.py +130 -223
src/streamlit_app.py
CHANGED
|
@@ -1,248 +1,155 @@
|
|
| 1 |
import os
|
| 2 |
-
import io
|
| 3 |
import re
|
| 4 |
-
import pandas as pd
|
| 5 |
-
import streamlit as st
|
| 6 |
from dataclasses import dataclass
|
| 7 |
from typing import List, Dict, Optional
|
|
|
|
|
|
|
|
|
|
| 8 |
|
|
|
|
| 9 |
try:
|
| 10 |
from transformers import pipeline
|
| 11 |
HF_AVAILABLE = True
|
| 12 |
except Exception:
|
| 13 |
HF_AVAILABLE = False
|
| 14 |
|
| 15 |
-
#
|
| 16 |
-
|
| 17 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
|
| 19 |
-
# --------- USER PROFILE ---------
|
| 20 |
@dataclass
|
| 21 |
-
class
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
monthly_income: float
|
| 27 |
-
risk: str # "Low"/"Medium"/"High"
|
| 28 |
-
goals: str
|
| 29 |
-
def style_prompt(self):
|
| 30 |
-
if self.user_type.lower().startswith("stud"):
|
| 31 |
-
return "Respond as a friendly mentor to a student. Use clear, simple, supportive language, with practical examples."
|
| 32 |
-
return "Respond as a professional financial advisor for a working adult. Use precise, structured language, include trade-offs."
|
| 33 |
-
|
| 34 |
-
# --------- DATA & CATEGORIZATION ---------
|
| 35 |
-
CATEGORIES = {
|
| 36 |
-
"groceries": ["grocery", "supermarket", "food", "mart"],
|
| 37 |
-
"rent": ["rent", "landlord"],
|
| 38 |
-
"utilities": ["electric", "water", "gas", "utility", "internet"],
|
| 39 |
-
"transport": ["uber", "ola", "fuel", "bus", "metro", "train", "cab", "petrol"],
|
| 40 |
-
"entertainment": ["netflix", "spotify", "movie", "cinema", "concert", "game"],
|
| 41 |
-
"health": ["pharmacy", "doctor", "hospital", "clinic", "medicine"],
|
| 42 |
-
"eating_out": ["restaurant", "cafe", "bar", "eatery", "diner"],
|
| 43 |
-
"shopping": ["amazon", "flipkart", "myntra", "shop", "store"],
|
| 44 |
-
"income": ["salary", "stipend", "bonus", "interest", "dividend"],
|
| 45 |
-
}
|
| 46 |
-
def categorize(desc: str) -> str:
|
| 47 |
-
desc_l = (desc or "").lower()
|
| 48 |
-
for cat, keys in CATEGORIES.items():
|
| 49 |
-
if any(k in desc_l for k in keys):
|
| 50 |
-
return cat
|
| 51 |
-
return "other"
|
| 52 |
-
|
| 53 |
-
def load_transactions(uploaded_file: Optional[io.BytesIO]) -> pd.DataFrame:
|
| 54 |
-
# Demo data for new users or failed upload:
|
| 55 |
-
data = {
|
| 56 |
-
"date": pd.date_range("2025-07-01", periods=24, freq="D"),
|
| 57 |
-
"description": [
|
| 58 |
-
"Salary", "Rent", "Grocery Store", "Restaurant", "Metro Card", "Internet Bill",
|
| 59 |
-
"Pharmacy", "Movie", "Amazon", "Fuel", "Bonus", "Electric Bill",
|
| 60 |
-
"Café", "Supermarket", "Hospital", "Netflix", "Ola Ride", "Water Bill",
|
| 61 |
-
"Gym", "Flipkart", "Bus", "Medicine", "Dividend", "Train"
|
| 62 |
-
],
|
| 63 |
-
"amount": [
|
| 64 |
-
70000, -15000, -2500, -900, -300, -800, -1200, -500, -2200, -1500, 8000, -1200,
|
| 65 |
-
-450, -2100, -5000, -500, -350, -400, -1200, -1800, -200, -600, 1200, -250
|
| 66 |
-
],
|
| 67 |
-
}
|
| 68 |
-
if uploaded_file is None:
|
| 69 |
-
df = pd.DataFrame(data)
|
| 70 |
-
else:
|
| 71 |
-
try:
|
| 72 |
-
df = pd.read_csv(uploaded_file)
|
| 73 |
-
except Exception:
|
| 74 |
-
df = pd.DataFrame(data)
|
| 75 |
-
df["category"] = df["description"].apply(categorize)
|
| 76 |
-
return df
|
| 77 |
-
|
| 78 |
-
def budget_summary(df: pd.DataFrame, monthly_income_hint: Optional[float]=None) -> Dict[str, float]:
|
| 79 |
-
income = df.loc[df["amount"] > 0, "amount"].sum()
|
| 80 |
-
expenses = -df.loc[df["amount"] < 0, "amount"].sum()
|
| 81 |
-
net = income - expenses
|
| 82 |
-
if monthly_income_hint and monthly_income_hint > 0:
|
| 83 |
-
income = max(income, monthly_income_hint)
|
| 84 |
-
net = income - expenses
|
| 85 |
-
savings_rate = (net / income) * 100 if income > 0 else 0.0
|
| 86 |
-
top_spend = (-df[df["amount"] < 0].groupby("category")["amount"].sum()).nlargest(5)
|
| 87 |
-
return {
|
| 88 |
-
"income_total": float(round(income, 2)),
|
| 89 |
-
"expense_total": float(round(expenses, 2)),
|
| 90 |
-
"net_savings": float(round(net, 2)),
|
| 91 |
-
"savings_rate_pct": float(round(savings_rate, 2)),
|
| 92 |
-
"top_spend_json": top_spend.to_json(),
|
| 93 |
-
}
|
| 94 |
-
|
| 95 |
-
def spending_suggestions(df: pd.DataFrame, profile: UserProfile) -> List[str]:
|
| 96 |
-
tips = []
|
| 97 |
-
summary = budget_summary(df, monthly_income_hint=profile.monthly_income)
|
| 98 |
-
if summary["net_savings"] < profile.monthly_income * 0.1:
|
| 99 |
-
tips.append("Build or maintain a 3–6 month emergency fund; automate a monthly transfer to high‑yield savings.")
|
| 100 |
-
cat_spend = -df[df["amount"] < 0].groupby("category")["amount"].sum()
|
| 101 |
-
for cat, amt in cat_spend.sort_values(ascending=False).head(3).items():
|
| 102 |
-
if amt > profile.monthly_income * 0.15:
|
| 103 |
-
tips.append(f"{cat.capitalize()} spending is high (₹{int(amt)}): Set a spending cap and leverage cash-back offers where possible.")
|
| 104 |
-
eat_out = -df[(df["category"] == "eating_out") & (df["amount"] < 0)]["amount"].sum()
|
| 105 |
-
if eat_out > 0.07 * profile.monthly_income:
|
| 106 |
-
tips.append("You are spending >7% of income on eating out. Consider meal planning and limit eating out to weekends.")
|
| 107 |
-
transport = -df[(df["category"] == "transport") & (df["amount"] < 0)]["amount"].sum()
|
| 108 |
-
if transport > 0.08 * profile.monthly_income:
|
| 109 |
-
tips.append("Transport spend is sizable. Consider monthly passes, rideshares or optimizing travel days.")
|
| 110 |
-
if profile.risk.lower() == "low":
|
| 111 |
-
tips.append("Consider a conservative portfolio: higher allocation to bonds, fixed income, low volatility funds.")
|
| 112 |
-
elif profile.risk.lower() == "high":
|
| 113 |
-
tips.append("For high risk tolerance: diversify, use low-cost index funds with limited exposure to growth sectors.")
|
| 114 |
-
if profile.user_type.lower().startswith("stud"):
|
| 115 |
-
tips.append("As a student, use student discounts, avoid high-interest credit, and keep credit utilization <30%.")
|
| 116 |
-
else:
|
| 117 |
-
tips.append("As a professional, automate investments, optimize tax, and annually review insurance cover.")
|
| 118 |
-
return tips
|
| 119 |
-
|
| 120 |
-
# --- INTENT FILTER (Optional, for finance/numbers only) ---
|
| 121 |
-
FINANCE_KEYWORDS = ["finance", "money", "budget", "expense", "savings", "tax", "investment", "loan", "credit", "debit", "stock", "rate", "income", "emi", "pay", "salary", "roi", "interest", "dividend", "bond", "sip", "fd", "rd", "fixed deposit", "asset", "liability", "capital"]
|
| 122 |
-
|
| 123 |
-
def is_finance_related(text):
|
| 124 |
-
text_l = text.lower()
|
| 125 |
-
if any(word in text_l for word in FINANCE_KEYWORDS):
|
| 126 |
-
return True
|
| 127 |
-
if any(char.isdigit() for char in text):
|
| 128 |
-
return True
|
| 129 |
-
return False
|
| 130 |
|
| 131 |
-
# ----------- AI PROVIDER WRAPPERS -------------
|
| 132 |
class HuggingFaceProvider:
|
| 133 |
def __init__(self):
|
| 134 |
-
|
|
|
|
|
|
|
|
|
|
| 135 |
try:
|
| 136 |
-
self.
|
| 137 |
except Exception:
|
| 138 |
-
self.
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
self.
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 147 |
|
| 148 |
class GraniteWatsonProvider:
|
| 149 |
def __init__(self):
|
| 150 |
-
# These env vars are expected to be set on Hugging Face Spaces for secure production
|
| 151 |
-
self.api_key = os.getenv("IBM_WATSON_API_KEY", "")
|
| 152 |
-
self.url = os.getenv("IBM_WATSON_URL", "")
|
| 153 |
self.name = "granite_watson"
|
|
|
|
| 154 |
def ok(self):
|
| 155 |
-
return
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
return "[Granite/Watson
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 183 |
hf_provider = HuggingFaceProvider()
|
| 184 |
granite_provider = GraniteWatsonProvider()
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
|
| 211 |
-
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
|
| 217 |
-
# Demographic-aware + context-aware system prompt
|
| 218 |
-
sys_prompt = (
|
| 219 |
-
f"You are a finance-focused AI chatbot, expert in Indian personal finance. "
|
| 220 |
-
f"User: {profile.user_type}, Age {profile.age}, Location {profile.country}, "
|
| 221 |
-
f"Monthly Income ₹{profile.monthly_income:.0f}, Risk Tolerance {profile.risk}, Goals: {profile.goals}. "
|
| 222 |
-
f"{profile.style_prompt()} "
|
| 223 |
-
"Do NOT answer non-finance queries. Always use friendly, supportive, and context-aware explanations."
|
| 224 |
-
)
|
| 225 |
-
context = (
|
| 226 |
-
f"Context: User's Current Budget - Income ₹{summary['income_total']}, "
|
| 227 |
-
f"Expenses ₹{summary['expense_total']}, Net ₹{summary['net_savings']}, "
|
| 228 |
-
f"Savings Rate {summary['savings_rate_pct']}%."
|
| 229 |
-
)
|
| 230 |
-
user_prompt = (
|
| 231 |
-
f"{context}\nUser asked: {user_msg}\n"
|
| 232 |
-
"Split your answer into: 1) Quick answer, 2) Why it matters, 3) Next steps (bullets), 4) Caution notes."
|
| 233 |
-
)
|
| 234 |
-
full_prompt = sys_prompt + "\n\n" + user_prompt
|
| 235 |
-
with st.chat_message("assistant"):
|
| 236 |
-
with st.spinner(f"Thinking with {provider.name}…"):
|
| 237 |
-
try:
|
| 238 |
-
ai = provider.generate(full_prompt, max_tokens=768)
|
| 239 |
-
except Exception as e:
|
| 240 |
-
ai = f"Provider error: {e}\nFallback: Use only rule-based advice."
|
| 241 |
-
st.markdown(ai)
|
| 242 |
-
st.session_state.chat_history.append({"role": "assistant", "content": ai})
|
| 243 |
-
|
| 244 |
-
st.markdown("""
|
| 245 |
-
---
|
| 246 |
-
**Disclaimer:** This chatbot provides educational information only and is _not_ financial, tax, or legal advice.
|
| 247 |
-
Consult a licensed professional for tailored guidance. Tax laws and investment products change frequently.
|
| 248 |
-
""")
|
|
|
|
| 1 |
import os
|
|
|
|
| 2 |
import re
|
|
|
|
|
|
|
| 3 |
from dataclasses import dataclass
|
| 4 |
from typing import List, Dict, Optional
|
| 5 |
+
import pandas as pd
|
| 6 |
+
import streamlit as st
|
| 7 |
+
from dotenv import load_dotenv
|
| 8 |
|
| 9 |
+
# HuggingFace optional
|
| 10 |
try:
|
| 11 |
from transformers import pipeline
|
| 12 |
HF_AVAILABLE = True
|
| 13 |
except Exception:
|
| 14 |
HF_AVAILABLE = False
|
| 15 |
|
| 16 |
+
# OpenAI
|
| 17 |
+
try:
|
| 18 |
+
from openai import OpenAI
|
| 19 |
+
OPENAI_AVAILABLE = True
|
| 20 |
+
except Exception:
|
| 21 |
+
OPENAI_AVAILABLE = False
|
| 22 |
+
|
| 23 |
+
# Load environment variables
|
| 24 |
+
load_dotenv()
|
| 25 |
+
OPENAI_API_KEY = os.getenv("OPENAI_API_KEY", "")
|
| 26 |
+
MODEL = os.getenv("MODEL", "gpt-3.5-turbo")
|
| 27 |
+
|
| 28 |
+
# Streamlit config
|
| 29 |
+
st.set_page_config(page_title="Personal Finance Chatbot", page_icon="💬", layout="wide")
|
| 30 |
|
|
|
|
| 31 |
@dataclass
|
| 32 |
+
class FinanceRecord:
|
| 33 |
+
date: str
|
| 34 |
+
description: str
|
| 35 |
+
amount: float
|
| 36 |
+
category: Optional[str] = None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 37 |
|
|
|
|
| 38 |
class HuggingFaceProvider:
|
| 39 |
def __init__(self):
|
| 40 |
+
self.available = HF_AVAILABLE
|
| 41 |
+
self.name = "huggingface"
|
| 42 |
+
self.generator = None
|
| 43 |
+
if self.available:
|
| 44 |
try:
|
| 45 |
+
self.generator = pipeline("text2text-generation", model="google/flan-t5-small")
|
| 46 |
except Exception:
|
| 47 |
+
self.available = False
|
| 48 |
+
|
| 49 |
+
def ok(self):
|
| 50 |
+
return self.available and self.generator is not None
|
| 51 |
+
|
| 52 |
+
def generate(self, prompt: str, max_tokens: int = 256):
|
| 53 |
+
if not self.ok():
|
| 54 |
+
return "[HF provider unavailable]"
|
| 55 |
+
try:
|
| 56 |
+
result = self.generator(prompt, max_length=max_tokens, do_sample=True)
|
| 57 |
+
return result[0]['generated_text']
|
| 58 |
+
except Exception as e:
|
| 59 |
+
return f"[HF error] {e}"
|
| 60 |
|
| 61 |
class GraniteWatsonProvider:
|
| 62 |
def __init__(self):
|
|
|
|
|
|
|
|
|
|
| 63 |
self.name = "granite_watson"
|
| 64 |
+
|
| 65 |
def ok(self):
|
| 66 |
+
return True
|
| 67 |
+
|
| 68 |
+
def generate(self, prompt: str, max_tokens: int = 256):
|
| 69 |
+
return "[Granite/Watson] This is a placeholder response. Connect IBM SDK here."
|
| 70 |
+
|
| 71 |
+
class OpenAIProvider:
|
| 72 |
+
def __init__(self):
|
| 73 |
+
self.api_key = OPENAI_API_KEY
|
| 74 |
+
self.model = MODEL
|
| 75 |
+
self.client = None
|
| 76 |
+
if self.api_key and OPENAI_AVAILABLE:
|
| 77 |
+
try:
|
| 78 |
+
self.client = OpenAI(api_key=self.api_key)
|
| 79 |
+
except Exception:
|
| 80 |
+
self.client = None
|
| 81 |
+
self.name = "openai"
|
| 82 |
+
|
| 83 |
+
def ok(self):
|
| 84 |
+
return self.client is not None
|
| 85 |
+
|
| 86 |
+
def generate(self, prompt: str, max_tokens: int = 512):
|
| 87 |
+
if not self.client:
|
| 88 |
+
return "[OpenAI] API not configured. Please set OPENAI_API_KEY in your environment."
|
| 89 |
+
try:
|
| 90 |
+
resp = self.client.chat.completions.create(
|
| 91 |
+
model=self.model,
|
| 92 |
+
messages=[
|
| 93 |
+
{"role": "system", "content": "You are a financial assistant."},
|
| 94 |
+
{"role": "user", "content": prompt},
|
| 95 |
+
],
|
| 96 |
+
max_tokens=max_tokens,
|
| 97 |
+
temperature=0.7,
|
| 98 |
+
)
|
| 99 |
+
return resp.choices[0].message.content.strip()
|
| 100 |
+
except Exception as e:
|
| 101 |
+
return f"[OpenAI error] {e}"
|
| 102 |
+
|
| 103 |
+
def categorize_with_ai(provider, description: str):
|
| 104 |
+
prompt = f"Categorize this financial transaction description into: Food, Rent, Utilities, Entertainment, Transport, Other.\nDescription: {description}\nCategory:"
|
| 105 |
+
return provider.generate(prompt)
|
| 106 |
+
|
| 107 |
+
def get_ai_suggestions(provider, records: List[FinanceRecord]):
|
| 108 |
+
df = pd.DataFrame([r.__dict__ for r in records])
|
| 109 |
+
prompt = (
|
| 110 |
+
"You are a financial advisor. Here are the user's transactions:\n"
|
| 111 |
+
f"{df.to_string(index=False)}\n\n"
|
| 112 |
+
"Provide insights and suggestions to improve savings and manage money better."
|
| 113 |
+
)
|
| 114 |
+
return provider.generate(prompt, max_tokens=400)
|
| 115 |
+
|
| 116 |
+
# Streamlit UI
|
| 117 |
+
st.title("💬 Personal Finance Chatbot")
|
| 118 |
+
st.write("Manage savings, taxes, and investments with AI guidance.")
|
| 119 |
+
|
| 120 |
+
provider_choice = st.selectbox("AI Provider", ["HuggingFace", "Granite/Watson", "OpenAI"], index=0)
|
| 121 |
+
|
| 122 |
hf_provider = HuggingFaceProvider()
|
| 123 |
granite_provider = GraniteWatsonProvider()
|
| 124 |
+
openai_provider = OpenAIProvider()
|
| 125 |
+
|
| 126 |
+
if provider_choice == "HuggingFace":
|
| 127 |
+
provider = hf_provider
|
| 128 |
+
elif provider_choice == "Granite/Watson":
|
| 129 |
+
provider = granite_provider
|
| 130 |
+
else:
|
| 131 |
+
provider = openai_provider
|
| 132 |
+
|
| 133 |
+
if "records" not in st.session_state:
|
| 134 |
+
st.session_state.records: List[FinanceRecord] = []
|
| 135 |
+
|
| 136 |
+
st.sidebar.header("Add Transaction")
|
| 137 |
+
date = st.sidebar.text_input("Date", "2025-08-30")
|
| 138 |
+
description = st.sidebar.text_input("Description", "")
|
| 139 |
+
amount = st.sidebar.number_input("Amount", 0.0, 1e9, step=100.0)
|
| 140 |
+
if st.sidebar.button("Add Record"):
|
| 141 |
+
record = FinanceRecord(date=date, description=description, amount=amount)
|
| 142 |
+
record.category = categorize_with_ai(provider, record.description)
|
| 143 |
+
st.session_state.records.append(record)
|
| 144 |
+
st.sidebar.success("Record added!")
|
| 145 |
+
|
| 146 |
+
if st.session_state.records:
|
| 147 |
+
st.subheader("Transaction Records")
|
| 148 |
+
df = pd.DataFrame([r.__dict__ for r in st.session_state.records])
|
| 149 |
+
st.dataframe(df)
|
| 150 |
+
|
| 151 |
+
st.subheader("AI Suggestions")
|
| 152 |
+
suggestions = get_ai_suggestions(provider, st.session_state.records)
|
| 153 |
+
st.write(suggestions)
|
| 154 |
+
else:
|
| 155 |
+
st.info("No records yet. Add transactions from the sidebar.")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|