Rahul2298 commited on
Commit
b437c2d
·
verified ·
1 Parent(s): ea1c363

Update src/streamlit_app.py

Browse files
Files changed (1) hide show
  1. src/streamlit_app.py +233 -280
src/streamlit_app.py CHANGED
@@ -1,313 +1,266 @@
 
 
 
 
 
 
 
 
1
  import os
2
- import io
3
- import re
4
- from dataclasses import dataclass
5
- from typing import List, Dict, Optional
6
  import pandas as pd
7
  import streamlit as st
 
 
 
8
 
9
- # Hugging Face (optional)
 
 
 
 
10
  try:
11
- from transformers import pipeline
12
- HF_AVAILABLE = True
13
  except Exception:
14
- HF_AVAILABLE = False
15
 
16
- # -------------------------
17
- # Streamlit Config
18
- # -------------------------
19
- st.set_page_config(page_title="Personal Finance Chatbot", page_icon="💬", layout="wide")
20
 
21
  # -------------------------
22
- # Session State Init
23
  # -------------------------
24
- if "chat_history" not in st.session_state:
25
- st.session_state["chat_history"] = []
26
- if "providers" not in st.session_state:
27
- st.session_state["providers"] = {}
28
- if "provider_inited" not in st.session_state:
29
- st.session_state["provider_inited"] = False
30
- if "provider_name" not in st.session_state:
31
- st.session_state["provider_name"] = "huggingface"
32
 
33
- # -------------------------
34
- # Categories & Intent Patterns
35
- # -------------------------
36
- CATEGORIES = {
37
- "food": ["grocery", "restaurant", "dining"],
38
- "transport": ["bus", "metro", "uber", "ola", "taxi"],
39
- "entertainment": ["movie", "netflix", "spotify"],
40
- "other": []
41
- }
42
 
43
- INTENT_PATTERNS = {
44
- "savings": r"\bsav(e|ings|ing)\b",
45
- "tax": r"\btax(es)?\b",
46
- "invest": r"\binvest(ment|ing)?\b",
47
- "budget": r"\bbudget(ing)?\b"
48
- }
49
 
50
  # -------------------------
51
- # AI Providers
52
  # -------------------------
53
- class AIProvider:
54
- def __init__(self):
55
- self.name = "base"
56
-
57
- def generate(self, prompt: str, max_tokens: int = 512) -> str:
58
- raise NotImplementedError
59
-
60
-
61
- class HuggingFaceProvider(AIProvider):
62
- def __init__(self):
63
- super().__init__()
64
- self.name = "huggingface"
65
- self.gen = None
66
- if HF_AVAILABLE:
67
- try:
68
- self.gen = pipeline("text2text-generation", model="google/flan-t5-base")
69
- except Exception as e:
70
- st.warning(f"HuggingFace pipeline failed to load: {e}")
 
 
 
 
 
 
 
 
 
71
  else:
72
- st.info("Transformers not installed; responses will be rule-based only.")
73
-
74
- def generate(self, prompt: str, max_tokens: int = 512) -> str:
75
- if self.gen is None:
76
- return (
77
- "[Rule-based fallback]\n"
78
- + prompt[:1000]
79
- + "\n\n(Summarized suggestion) Consider tracking expenses, setting goals, "
80
- "building an emergency fund, and using diversified, low-cost index funds aligned "
81
- "with your risk tolerance.)"
82
- )
83
- out = self.gen(prompt, max_length=min(1024, max_tokens), do_sample=False)
84
- return out[0]["generated_text"].strip()
85
-
86
-
87
- class IBMGraniteWatsonProvider(AIProvider):
88
- def __init__(self, watson_api_key: Optional[str], watson_url: Optional[str], granite_key: Optional[str]):
89
- super().__init__()
90
- self.name = "ibm_granite_watson"
91
- self.ok = bool(watson_api_key and watson_url) or bool(granite_key)
92
- self.watson_api_key = watson_api_key
93
- self.watson_url = watson_url
94
- self.granite_key = granite_key
95
-
96
- def generate(self, prompt: str, max_tokens: int = 512) -> str:
97
- if not self.ok:
98
- return "[IBM placeholder] Missing credentials — falling back text.\n" + prompt
99
- return (
100
- "[IBM Granite/Watson simulated response]\n"
101
- "(Replace this with real SDK call)\n\n"
102
- + prompt
103
- )
104
 
105
  # -------------------------
106
- # Helpers
107
  # -------------------------
108
- def categorize(desc: str) -> str:
109
- desc_l = (desc or "").lower()
110
- for cat, keys in CATEGORIES.items():
111
- if any(k in desc_l for k in keys):
112
- return cat
113
- return "other"
114
-
115
-
116
- @dataclass
117
- class UserProfile:
118
- name: str
119
- user_type: str
120
- age: int
121
- country: str
122
- monthly_income: float
123
- risk_tolerance: str
124
- goals: str
125
-
126
- def style_prompt(self) -> str:
127
- if self.user_type.lower().startswith("stud"):
128
- return (
129
- "Explain like a friendly mentor to a student. Keep it clear and concise, "
130
- "use practical examples and low-jargon."
131
- )
132
- return (
133
- "Explain like a professional financial coach. Be precise, structured, and include "
134
- "brief rationale with trade-offs."
135
- )
136
-
137
 
138
- def load_transactions(uploaded_file: Optional[io.BytesIO]) -> pd.DataFrame:
139
- if uploaded_file is None:
140
- data = {
141
- "date": ["2025-08-01", "2025-08-02", "2025-08-03"],
142
- "description": ["grocery store", "uber ride", "netflix subscription"],
143
- "amount": [-1500, -300, -5000]
144
- }
145
- df = pd.DataFrame(data)
146
- else:
147
- try:
148
- df = pd.read_csv(uploaded_file)
149
- except Exception as e:
150
- st.error(f"Could not read CSV: {e}. Showing demo data.")
151
- data = {
152
- "date": ["2025-08-01", "2025-08-02", "2025-08-03"],
153
- "description": ["grocery store", "uber ride", "netflix subscription"],
154
- "amount": [-1500, -300, -5000]
155
- }
156
- df = pd.DataFrame(data)
157
- df["category"] = df["description"].apply(categorize)
158
  return df
159
 
 
 
 
 
 
 
 
 
 
 
 
 
 
160
 
161
- def budget_summary(df: pd.DataFrame, monthly_income_hint: Optional[float] = None) -> Dict[str, float]:
162
- df["month"] = pd.to_datetime(df["date"]).dt.to_period("M").astype(str)
163
- income = df.loc[df["amount"] > 0, "amount"].sum()
164
- expenses = -df.loc[df["amount"] < 0, "amount"].sum()
165
- net = income - expenses
166
- if monthly_income_hint and monthly_income_hint > 0:
167
- income = max(income, monthly_income_hint)
168
- net = income - expenses
169
- savings_rate = (net / income) * 100 if income > 0 else 0.0
170
  by_cat = df.groupby("category")["amount"].sum().sort_values()
171
- top_spend = (-by_cat[by_cat < 0]).nlargest(5)
172
  return {
173
- "income_total": float(round(income, 2)),
174
- "expense_total": float(round(expenses, 2)),
175
- "net_savings": float(round(net, 2)),
176
- "savings_rate_pct": float(round(savings_rate, 2)),
177
- "top_spend_json": top_spend.to_json(),
178
  }
179
 
180
-
181
- def spending_suggestions(df: pd.DataFrame, profile: UserProfile) -> List[str]:
182
- tips = []
183
- summary = budget_summary(df, monthly_income_hint=profile.monthly_income)
184
- if summary["savings_rate_pct"] < 10:
185
- tips.append("Your savings rate is low. Try setting aside at least 20% of your income.")
186
- if "entertainment" in df["category"].values:
187
- tips.append("Entertainment spending is high. Consider limiting subscriptions or outings.")
188
- if "food" in df["category"].values:
189
- tips.append("Track food expenses. Cooking at home can save money.")
190
- if not tips:
191
- tips.append("Good job! Your spending looks balanced.")
192
- return tips
193
-
194
-
195
- def detect_intent(text: str) -> str:
196
- t = text.lower()
197
- for k, pat in INTENT_PATTERNS.items():
198
- if re.search(pat, t):
199
- return k
200
- return "general"
201
-
202
-
203
- def build_system_prompt(profile: UserProfile) -> str:
204
- return (
205
- f"You are a financial assistant. User profile: {profile}. "
206
- f"Respond in style: {profile.style_prompt()}"
207
- )
208
-
209
-
210
- def craft_user_prompt(query: str, intent: str, summary: Dict[str, float]) -> str:
211
- return f"User asked about {intent}: {query}\nBudget summary: {summary}"
212
-
213
- # -------------------------
214
- # Sidebar Inputs
215
- # -------------------------
216
- with st.sidebar:
217
- provider_choice = st.radio(
218
- "Select AI Provider",
219
- ["HuggingFace", "IBM Granite Watson", "Auto (Best Available)"]
220
- )
221
- uploaded = st.file_uploader("Upload your transactions CSV", type=["csv"])
222
- st.markdown("### Profile")
223
- profile = UserProfile(
224
- name=st.text_input("Name", "Rahul"),
225
- user_type=st.selectbox("User Type", ["Student", "Professional"]),
226
- age=st.number_input("Age", 18, 100, 25),
227
- country=st.text_input("Country", "India"),
228
- monthly_income=st.number_input("Monthly Income", 0.0, 1e7, 50000.0),
229
- risk_tolerance=st.selectbox("Risk Tolerance", ["Low", "Medium", "High"]),
230
- goals=st.text_area("Financial Goals", "Save for emergency fund and invest in mutual funds")
231
- )
232
-
233
  # -------------------------
234
- # Provider Initialization
235
  # -------------------------
236
- if not st.session_state["provider_inited"]:
237
- st.session_state["providers"]["ibm"] = IBMGraniteWatsonProvider(
238
- watson_api_key=os.getenv("IBM_WATSON_API_KEY"),
239
- watson_url=os.getenv("IBM_WATSON_URL"),
240
- granite_key=os.getenv("IBM_GRANITE_API_KEY"),
 
 
 
 
 
 
 
241
  )
242
- st.session_state["providers"]["hf"] = HuggingFaceProvider()
243
-
244
- chosen = "huggingface"
245
- ibm_ok = st.session_state["providers"]["ibm"].ok
246
- if provider_choice.startswith("IBM") and ibm_ok:
247
- chosen = "ibm_granite_watson"
248
- elif provider_choice.startswith("Auto"):
249
- chosen = "ibm_granite_watson" if ibm_ok else "huggingface"
250
- st.session_state["provider_name"] = chosen
251
- st.session_state["provider_inited"] = True
252
-
253
- provider_name = st.session_state["provider_name"]
254
- if provider_choice.startswith("IBM"):
255
- provider_name = "ibm_granite_watson" if st.session_state["providers"]["ibm"].ok else "huggingface"
256
- elif provider_choice.startswith("Auto"):
257
- provider_name = "ibm_granite_watson" if st.session_state["providers"]["ibm"].ok else "huggingface"
258
- else:
259
- provider_name = "huggingface"
260
- st.session_state["provider_name"] = provider_name
261
-
262
- provider = (
263
- st.session_state["providers"]["ibm"] if provider_name == "ibm_granite_watson"
264
- else st.session_state["providers"]["hf"]
265
- )
266
 
267
  # -------------------------
268
- # Layout
269
  # -------------------------
270
- col_chat, col_right = st.columns([0.62, 0.38])
271
-
272
- with col_right:
273
- st.subheader("📊 Budget Summary")
274
- df = load_transactions(uploaded)
275
- st.dataframe(df, use_container_width=True, height=250)
276
- summary = budget_summary(df, monthly_income_hint=profile.monthly_income)
277
- m1, m2, m3, m4 = st.columns(4)
278
- m1.metric("Income (₹)", f"{summary['income_total']:.0f}")
279
- m2.metric("Expenses (₹)", f"{summary['expense_total']:.0f}")
280
- m3.metric("Net ()", f"{summary['net_savings']:.0f}")
281
- m4.metric("Savings Rate", f"{summary['savings_rate_pct']:.1f}%")
282
-
283
- st.markdown("### 🧠 AI Spending Suggestions")
284
- for tip in spending_suggestions(df, profile):
285
- st.write("• ", tip)
286
-
287
- with col_chat:
288
- st.subheader("🗣️ Ask about savings, taxes, investments, or budgeting")
289
- for turn in st.session_state["chat_history"]:
290
- with st.chat_message(turn["role"]):
291
- st.markdown(turn["content"])
292
- user_msg = st.chat_input("Type your question… (e.g., How much should I invest monthly for a ₹10L goal?)")
293
- if user_msg:
294
- st.session_state["chat_history"].append({"role": "user", "content": user_msg})
295
- intent = detect_intent(user_msg)
296
- sys_prompt = build_system_prompt(profile)
297
- usr_prompt = craft_user_prompt(user_msg, intent, summary)
298
- final_prompt = sys_prompt + "\n\n" + usr_prompt
299
- with st.chat_message("assistant"):
300
- with st.spinner(f"Thinking with {provider.name}…"):
301
- try:
302
- ai = provider.generate(final_prompt, max_tokens=768)
303
- except Exception as e:
304
- ai = f"Provider error: {e}\nFalling back to heuristic guidance.\n" + usr_prompt
305
- st.markdown(ai)
306
- st.session_state["chat_history"].append({"role": "assistant", "content": ai})
307
-
308
- st.markdown("""
309
- ---
310
- **Disclaimers**
311
- This chatbot provides educational information only and is **not** financial, tax, or legal advice.
312
- Tax rules change frequently; consult a qualified professional for personalized advice.
313
- """)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # app.py
2
+ """
3
+ Personal Finance Chatbot (Streamlit + IBM watsonx + Watson Assistant)
4
+ - Single-file demo app
5
+ - Requirements: streamlit, pandas, python-dotenv, ibm-watsonx-ai, ibm-watson
6
+ - Set env vars before running (see README block below)
7
+ """
8
+
9
  import os
10
+ import json
 
 
 
11
  import pandas as pd
12
  import streamlit as st
13
+ from dotenv import load_dotenv
14
+ from datetime import datetime
15
+ from typing import Dict, Any, List
16
 
17
+ # IBM SDK imports
18
+ try:
19
+ from ibm_watsonx_ai import APIClient, Credentials
20
+ except Exception:
21
+ APIClient = None
22
  try:
23
+ from ibm_watson import AssistantV2
24
+ from ibm_cloud_sdk_core.authenticators import IAMAuthenticator
25
  except Exception:
26
+ AssistantV2 = None
27
 
28
+ # Load env vars from .env if present
29
+ load_dotenv()
 
 
30
 
31
  # -------------------------
32
+ # Configuration / ENV VARS
33
  # -------------------------
34
+ WATSONX_API_KEY = os.getenv("WATSONX_API_KEY") # watsonx IAM apikey
35
+ WATSONX_URL = os.getenv("WATSONX_URL") # watsonx url/endpoint (e.g. https://us-south.ml.cloud.ibm.com)
36
+ WATSONX_MODEL_ID = os.getenv("WATSONX_MODEL_ID", "ibm/granite-13b-instruct-v2") # default example model
 
 
 
 
 
37
 
38
+ ASSISTANT_APIKEY = os.getenv("ASSISTANT_APIKEY") # Watson Assistant api key (optional)
39
+ ASSISTANT_URL = os.getenv("ASSISTANT_URL") # Watson Assistant url (optional)
40
+ ASSISTANT_ID = os.getenv("ASSISTANT_ID") # Assistant ID (if using dialog)
 
 
 
 
 
 
41
 
42
+ # Minimal checks
43
+ if APIClient is None:
44
+ st.warning("ibm-watsonx-ai not installed. Install with: pip install ibm-watsonx-ai")
45
+ if AssistantV2 is None:
46
+ st.info("ibm-watson (Assistant) client not installed or optional. Install: pip install ibm-watson")
 
47
 
48
  # -------------------------
49
+ # Helper: WatsonX client
50
  # -------------------------
51
+ def make_watsonx_client() -> Any:
52
+ """Create and return a watsonx APIClient or None if not configured"""
53
+ if not (WATSONX_API_KEY and WATSONX_URL):
54
+ return None
55
+ credentials = Credentials(url=WATSONX_URL, api_key=WATSONX_API_KEY)
56
+ client = APIClient(credentials=credentials)
57
+ return client
58
+
59
+ def watsonx_generate(client: Any, prompt: str, model_id: str = None, max_tokens: int = 512) -> str:
60
+ """
61
+ Generate text using watsonx foundation model via ibm_watsonx_ai client.
62
+ This function uses ModelInference utilities available on the client.
63
+ """
64
+ if client is None:
65
+ return "Watsonx client is not configured. Please set WATSONX_API_KEY and WATSONX_URL."
66
+
67
+ model_id = model_id or WATSONX_MODEL_ID
68
+ # Use the client's models or model inference helper:
69
+ # many SDK variations exist; below uses a common pattern (generate_text).
70
+ try:
71
+ model_inference = client.model_inference(model_id=model_id)
72
+ # simple call; more params (temperature, top_k, max_output_tokens) can be passed via params
73
+ generated = model_inference.generate_text(prompt=prompt, max_output_tokens=max_tokens)
74
+ # returned type can be dict/json or string depending on sdk version
75
+ if isinstance(generated, (dict, list)):
76
+ # attempt to extract textual content
77
+ text = json.dumps(generated) # fallback representation
78
  else:
79
+ text = str(generated)
80
+ return text
81
+ except Exception as e:
82
+ return f"Error calling watsonx generate: {e}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
83
 
84
  # -------------------------
85
+ # Optional: Watson Assistant helper (for intents / dialog)
86
  # -------------------------
87
+ def make_assistant_client():
88
+ if not (ASSISTANT_APIKEY and ASSISTANT_URL):
89
+ return None
90
+ auth = IAMAuthenticator(ASSISTANT_APIKEY)
91
+ assistant = AssistantV2(version="2024-01-01", authenticator=auth)
92
+ assistant.set_service_url(ASSISTANT_URL)
93
+ return assistant
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
94
 
95
+ # -------------------------
96
+ # Budget parsing & analytics
97
+ # -------------------------
98
+ def parse_transactions_csv(uploaded_file) -> pd.DataFrame:
99
+ """
100
+ Expect CSV with columns: date, description, amount, category (category optional).
101
+ Returns dataframe with normalized date and numeric amount.
102
+ """
103
+ df = pd.read_csv(uploaded_file)
104
+ # basic normalization
105
+ if "date" in df.columns:
106
+ df["date"] = pd.to_datetime(df["date"], errors="coerce")
107
+ if "amount" in df.columns:
108
+ df["amount"] = pd.to_numeric(df["amount"], errors="coerce")
109
+ # create category if missing (simple rule-based)
110
+ if "category" not in df.columns:
111
+ df["category"] = df["description"].fillna("").str.lower().apply(guess_category_from_desc)
112
+ df = df.dropna(subset=["amount"])
 
 
113
  return df
114
 
115
+ def guess_category_from_desc(desc: str) -> str:
116
+ desc = (desc or "").lower()
117
+ if any(k in desc for k in ["uber", "ola", "cab", "taxi"]):
118
+ return "transport"
119
+ if any(k in desc for k in ["grocery", "walmart", "bigbasket", "grocer"]):
120
+ return "groceries"
121
+ if any(k in desc for k in ["rent", "apartment", "house"]):
122
+ return "rent"
123
+ if any(k in desc for k in ["netflix", "spotify", "prime", "hulu"]):
124
+ return "entertainment"
125
+ if any(k in desc for k in ["salary", "pay", "deposit"]):
126
+ return "income"
127
+ return "other"
128
 
129
+ def budget_summary(df: pd.DataFrame) -> Dict[str, Any]:
130
+ """
131
+ Generate a simple summary: total income, total expenses, top categories.
132
+ """
133
+ income = df[df["amount"] > 0]["amount"].sum()
134
+ expenses = -df[df["amount"] < 0]["amount"].sum() # amounts might be negative for expenses
 
 
 
135
  by_cat = df.groupby("category")["amount"].sum().sort_values()
136
+ top_exp = by_cat[by_cat < 0].sort_values().head(5) * -1
137
  return {
138
+ "total_income": float(income),
139
+ "total_expenses": float(expenses),
140
+ "net_savings": float(income - expenses),
141
+ "top_expense_categories": top_exp.to_dict()
 
142
  }
143
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
144
  # -------------------------
145
+ # Prompt engineering helpers
146
  # -------------------------
147
+ def build_prompt(user_question: str, demographic: str, budget_summary_text: str = "") -> str:
148
+ """
149
+ Build a watsonx prompt which adjusts tone and complexity based on demographic.
150
+ demographic: "student" or "professional"
151
+ """
152
+ tone = "friendly, simple, and educational" if demographic.lower() == "student" else "concise, professional, actionable"
153
+ complexity = "use short, clear sentences and examples" if demographic.lower() == "student" else "use precise financial language; include bullet recommendations"
154
+ prompt = (
155
+ f"You are a helpful personal finance assistant. Adopt a {tone} tone and {complexity}.\n\n"
156
+ f"Context (if any):\n{budget_summary_text}\n\n"
157
+ f"User question: {user_question}\n\n"
158
+ f"Provide:\n1) Short answer to the user's question.\n2) A 3-point actionable plan or suggestion.\n3) If the question is budget-related, give 1 quick saving tip.\n\nAnswer:"
159
  )
160
+ return prompt
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
161
 
162
  # -------------------------
163
+ # Streamlit UI
164
  # -------------------------
165
+ st.set_page_config(page_title="Personal Finance Chatbot", layout="wide")
166
+ st.title("💬 Personal Finance Chatbot — Savings, Taxes, Investments (IBM watsonx)")
167
+
168
+ # Sidebar: upload transactions and settings
169
+ st.sidebar.header("Data & Settings")
170
+ uploaded = st.sidebar.file_uploader("Upload transactions CSV (date,description,amount,category optional)", type=["csv"])
171
+ demographic = st.sidebar.selectbox("User type (affects tone & complexity)", ["student", "professional"])
172
+ model_choice = st.sidebar.text_input("watsonx model id", value=WATSONX_MODEL_ID)
173
+
174
+ st.sidebar.markdown("**API status**")
175
+ watsonx_client = make_watsonx_client()
176
+ assistant_client = make_assistant_client()
177
+ st.sidebar.write("watsonx configured:", bool(watsonx_client))
178
+ st.sidebar.write("Assistant configured:", bool(assistant_client))
179
+
180
+ # Load transactions if provided
181
+ tx_df = None
182
+ budget_text = ""
183
+ if uploaded:
184
+ try:
185
+ tx_df = parse_transactions_csv(uploaded)
186
+ st.sidebar.success(f"Loaded {len(tx_df)} transactions")
187
+ summary = budget_summary(tx_df)
188
+ # create a short textual budget summary for context to the model
189
+ budget_text = (
190
+ f"Total income: {summary['total_income']:.2f}. "
191
+ f"Total expenses: {summary['total_expenses']:.2f}. "
192
+ f"Net savings: {summary['net_savings']:.2f}. "
193
+ f"Top expense categories: {', '.join([f'{k}: {v:.2f}' for k,v in summary['top_expense_categories'].items()])}."
194
+ )
195
+ except Exception as e:
196
+ st.sidebar.error(f"Failed to parse CSV: {e}")
197
+
198
+ # Main: chat area
199
+ if "chat_messages" not in st.session_state:
200
+ st.session_state.chat_messages = []
201
+
202
+ chat_col, info_col = st.columns([3,1])
203
+ with chat_col:
204
+ st.subheader("Chat")
205
+ # display history
206
+ for msg in st.session_state.chat_messages:
207
+ if msg["role"] == "user":
208
+ st.chat_message("user").write(msg["content"])
209
+ else:
210
+ st.chat_message("assistant").write(msg["content"])
211
+
212
+ user_input = st.chat_input("Ask about savings, taxes, budgets, or investments...")
213
+ if user_input:
214
+ # show the user's message immediately
215
+ st.session_state.chat_messages.append({"role":"user", "content": user_input})
216
+ # Build prompt for watsonx
217
+ prompt = build_prompt(user_input, demographic, budget_summary_text=budget_text)
218
+ with st.spinner("Generating response from watsonx..."):
219
+ if watsonx_client:
220
+ response_text = watsonx_generate(watsonx_client, prompt=prompt, model_id=model_choice, max_tokens=512)
221
+ else:
222
+ # fallback heuristic answer if watsonx not configured
223
+ response_text = (
224
+ "Watsonx not configured. (Set WATSONX_API_KEY and WATSONX_URL.)\n\n"
225
+ "Quick tips: 1) Track monthly expenses; 2) Build 3-6 months emergency fund; 3) Automate savings."
226
+ )
227
+ # Append assistant response
228
+ st.session_state.chat_messages.append({"role":"assistant", "content": response_text})
229
+ st.experimental_rerun()
230
+
231
+ with info_col:
232
+ st.subheader("Quick Tools")
233
+ if tx_df is not None:
234
+ st.markdown("**Budget snapshot**")
235
+ st.write(budget_text)
236
+ if st.button("Show top expenses table"):
237
+ top = tx_df.groupby("category")["amount"].sum().abs().sort_values(ascending=False).reset_index()
238
+ top.columns = ["category","total_spent"]
239
+ st.table(top.head(10))
240
+ else:
241
+ st.info("Upload transactions to enable budget summaries & insights")
242
+
243
+ # Footer: example prompts & environment instructions
244
+ st.markdown("---")
245
+ st.markdown("**Example prompts**")
246
+ st.markdown("- `How much should I save monthly given my income is ₹50,000?`")
247
+ st.markdown("- `Suggest tax-saving instruments suitable for a young professional in India.`")
248
+ st.markdown("- `I spent too much on food. How can I cut dining expenses by 20%?`")
249
+
250
+ st.markdown("---")
251
+ st.markdown("**Setup / Requirements**")
252
+ st.code("""
253
+ pip install streamlit pandas python-dotenv ibm-watsonx-ai ibm-watson
254
+ # Environment variables (example)
255
+ export WATSONX_API_KEY='your_watsonx_api_key'
256
+ export WATSONX_URL='https://<region>.ml.cloud.ibm.com'
257
+ export WATSONX_MODEL_ID='ibm/granite-13b-instruct-v2'
258
+ # Optional (Assistant)
259
+ export ASSISTANT_APIKEY='your_assistant_apikey'
260
+ export ASSISTANT_URL='https://api.<region>.assistant.watson.cloud.ibm.com'
261
+ export ASSISTANT_ID='your_assistant_id'
262
+ # Run
263
+ streamlit run app.py
264
+ """, language="bash")
265
+
266
+ st.caption("This is a prototype demo — for production, add secure secret handling, input validation, rate-limiting, logging, and robust error handling.")