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

Update src/streamlit_app.py

Browse files
Files changed (1) hide show
  1. src/streamlit_app.py +49 -265
src/streamlit_app.py CHANGED
@@ -1,266 +1,50 @@
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.")
 
1
+ # Add a FINANCE_KEYWORDS list at the top
2
+ FINANCE_KEYWORDS = [
3
+ "finance", "money", "budget", "spend", "expense", "investment", "invest",
4
+ "mutual fund", "savings", "loan", "credit", "debit", "stock", "tax",
5
+ "insurance", "emi", "pay", "salary", "income", "expense", "roi",
6
+ "interest", "dividend", "bond", "rate", "portfolio", "wealth", "goal",
7
+ "sip", "fd", "rd", "fixed deposit", "asset", "liability", "capital"
8
+ ]
9
+
10
+ def is_finance_related(text):
11
+ text_l = text.lower()
12
+ # Keywords
13
+ if any(word in text_l for word in FINANCE_KEYWORDS):
14
+ return True
15
+ # Numbers (strip commas, dots, %)
16
+ if any(char.isdigit() for char in text):
17
+ return True
18
+ return False
19
+
20
+ # In your chat input section, filter before sending to the AI
21
+ with col_chat:
22
+ st.subheader("🗣️ Ask about finance or numbers only")
23
+ for turn in st.session_state.chat_history:
24
+ with st.chat_message(turn["role"]):
25
+ st.markdown(turn["content"])
26
+ user_msg = st.chat_input("Type your finance/numbers-related question…")
27
+ if user_msg:
28
+ if not is_finance_related(user_msg):
29
+ assistant_message = "Sorry, I can only answer questions related to finance or numbers. Please rephrase your query."
30
+ st.session_state.chat_history.append({"role": "assistant", "content": assistant_message})
31
+ with st.chat_message("assistant"):
32
+ st.markdown(assistant_message)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
33
  else:
34
+ st.session_state.chat_history.append({"role": "user", "content": user_msg})
35
+ intent = detect_intent(user_msg)
36
+ sys_prompt = (
37
+ "You are a finance and numbers-only AI assistant. "
38
+ "If asked anything outside of finance or numbers, politely refuse to answer."
39
+ "\n\n" + build_system_prompt(profile)
40
+ )
41
+ usr_prompt = craft_user_prompt(user_msg, intent, summary)
42
+ final_prompt = sys_prompt + "\n\n" + usr_prompt
43
+ with st.chat_message("assistant"):
44
+ with st.spinner(f"Thinking with {provider.name}…"):
45
+ try:
46
+ ai = provider.generate(final_prompt, max_tokens=768)
47
+ except Exception as e:
48
+ ai = f"Provider error: {e}\nFalling back to heuristic guidance.\n" + usr_prompt
49
+ st.markdown(ai)
50
+ st.session_state.chat_history.append({"role": "assistant", "content": ai})