Rahul2298 commited on
Commit
430865f
·
verified ·
1 Parent(s): e5dcb69

Update src/streamlit_app.py

Browse files
Files changed (1) hide show
  1. 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
- # -------- SESSION STATE --------
16
- if "chat_history" not in st.session_state:
17
- st.session_state.chat_history = []
 
 
 
 
 
 
 
 
 
 
 
18
 
19
- # --------- USER PROFILE ---------
20
  @dataclass
21
- class UserProfile:
22
- name: str
23
- user_type: str # "Student"/"Professional"
24
- age: int
25
- country: str
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
- if HF_AVAILABLE:
 
 
 
135
  try:
136
- self.gen = pipeline("text2text-generation", model="google/flan-t5-base")
137
  except Exception:
138
- self.gen = None
139
- else:
140
- self.gen = None
141
- self.name = "huggingface"
142
- def generate(self, prompt, max_tokens=512):
143
- if self.gen is None:
144
- return ("[Fallback] Unable to answer with LLM. Please try again later.")
145
- out = self.gen(prompt, max_length=min(1024, max_tokens), do_sample=False)
146
- return out[0]['generated_text'].strip()
 
 
 
 
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 bool(self.api_key and self.url)
156
- def generate(self, prompt, max_tokens=512):
157
- # NO actual API call for demo/cost reasons – replace with real SDK/API in prod
158
- return "[Granite/Watson Simulated Response]\n\n" + prompt
159
-
160
- # ----------- STREAMLIT UI ----------------------
161
- st.set_page_config(page_title="FinanceBot", page_icon="💸", layout="wide")
162
-
163
- with st.sidebar:
164
- st.title("💸 FinanceBot")
165
- name = st.text_input("Name", value="Rahul")
166
- user_type = st.selectbox("You are a", ["Student", "Professional"], index=1)
167
- age = st.number_input("Age", min_value=16, max_value=90, value=24)
168
- country = st.text_input("Country", value="India")
169
- monthly_income = st.number_input("Monthly Income (₹)", min_value=0, value=70000, step=1000)
170
- risk = st.selectbox("Risk Tolerance", ["Low", "Medium", "High"], index=1)
171
- goals = st.text_area("Goals (comma-separated)", value="build emergency fund, start SIP, save tax")
172
- provider_choice = st.selectbox("AI Provider", ["HuggingFace", "Granite/Watson"], index=0)
173
- uploaded = st.file_uploader("Transaction CSV (date,description,amount)", type=["csv"])
174
-
175
- profile = UserProfile(
176
- name=name, user_type=user_type, age=int(age), country=country,
177
- monthly_income=float(monthly_income), risk=risk, goals=goals
178
- )
179
- df = load_transactions(uploaded)
180
- summary = budget_summary(df, monthly_income_hint=profile.monthly_income)
181
-
182
- # Providers
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
183
  hf_provider = HuggingFaceProvider()
184
  granite_provider = GraniteWatsonProvider()
185
- provider = hf_provider if provider_choice == "HuggingFace" else granite_provider
186
-
187
- # ----------- MAIN UI: Chat and Results -----------
188
- col_chat, col_right = st.columns([0.62, 0.38])
189
-
190
- with col_right:
191
- st.subheader("📊 Budget Summary")
192
- st.dataframe(df, use_container_width=True, height=240)
193
- m1, m2, m3, m4 = st.columns(4)
194
- m1.metric("Income (₹)", f"{summary['income_total']:.0f}")
195
- m2.metric("Expenses (₹)", f"{summary['expense_total']:.0f}")
196
- m3.metric("Net (₹)", f"{summary['net_savings']:.0f}")
197
- m4.metric("Savings Rate", f"{summary['savings_rate_pct']}%")
198
- st.markdown("#### 🧠 Spending & Investment Suggestions")
199
- for tip in spending_suggestions(df, profile):
200
- st.write("", tip)
201
-
202
- with col_chat:
203
- st.subheader("🗨️ Ask your finance question")
204
- for turn in st.session_state.chat_history:
205
- with st.chat_message(turn["role"]):
206
- st.markdown(turn["content"])
207
- user_msg = st.chat_input("Type your finance/numbers-related question…")
208
- if user_msg:
209
- # PREVENT OFF-TOPIC
210
- if not is_finance_related(user_msg):
211
- assistant_message = "Sorry, I can only answer questions related to finance or numbers. Please rephrase your query."
212
- st.session_state.chat_history.append({"role": "assistant", "content": assistant_message})
213
- with st.chat_message("assistant"):
214
- st.markdown(assistant_message)
215
- else:
216
- st.session_state.chat_history.append({"role": "user", "content": user_msg})
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.")