LogicGoInfotechSpaces commited on
Commit
e88ec4d
·
verified ·
1 Parent(s): 9588afe

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +105 -41
app.py CHANGED
@@ -1,3 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import json
2
  import os
3
  import re
@@ -31,6 +48,7 @@ if default_db is None:
31
 
32
  budget_collection = default_db["budget"]
33
  transaction_collection = default_db["transactions"]
 
34
 
35
  # OpenAI client
36
  openai = OpenAI(api_key=OPENAI_API_KEY)
@@ -49,7 +67,6 @@ class ScoreRequest(BaseModel):
49
  # ===========================
50
  # HELPERS
51
  # ===========================
52
-
53
  def safe_number(value):
54
  try:
55
  return float(value)
@@ -89,6 +106,9 @@ def normalize_budgets(budgets):
89
 
90
 
91
  def normalize_transactions(transactions):
 
 
 
92
  trimmed = []
93
  for txn in transactions:
94
  date_value = txn.get("date")
@@ -98,18 +118,55 @@ def normalize_transactions(transactions):
98
  else:
99
  date_str = None
100
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
101
  trimmed.append({
102
  "type": txn.get("type"),
103
  "amount": safe_number(txn.get("amount")),
 
104
  "date": date_str,
105
  })
106
  return trimmed
107
 
108
 
109
  def score_prompt(budgets, transactions):
 
110
  return f"""
111
- You are a financial wellness expert. Using any available budgets and recent transactions below,
112
- rate the user's financial health on a scale of 0100.
 
 
 
 
 
 
113
 
114
  Budgets:
115
  {json.dumps(normalize_budgets(budgets), indent=2)}
@@ -117,63 +174,69 @@ Budgets:
117
  Transactions (last 30 days):
118
  {json.dumps(normalize_transactions(transactions), indent=2)}
119
 
120
- Respond ONLY with:
121
- {{"score": number, "explanation": "short explanation"}}
122
  """
123
-
124
 
125
  def extract_json_payload(text):
126
  """Extract JSON from plain text or fenced code blocks."""
127
  trimmed = (text or "").strip()
128
 
 
129
  fenced = re.search(r"```(?:json)?\s*([\s\S]*?)```", trimmed)
130
  if fenced:
131
  return json.loads(fenced.group(1).strip())
132
 
 
 
 
 
 
 
133
  return json.loads(trimmed)
134
 
135
 
136
  # ===========================
137
- # NEW BULLETPROOF OPENAI EXTRACTOR
138
  # ===========================
139
-
140
  def extract_openai_text(response):
141
  """
142
- Bulletproof extractor for OpenAI SDK v1.x ChatCompletion responses.
143
- Handles:
144
- - message.content (real JSON string)
145
- - ChatCompletionMessage(content='...') object repr (your case)
146
- - strings
147
- - lists
148
  """
149
  try:
150
- msg = response.choices[0].message
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
151
  except Exception:
152
- raise HTTPException(status_code=502, detail="OpenAI returned no valid choices")
153
-
154
- # Case 1: content exists normally
155
- if hasattr(msg, "content"):
156
- return msg.content.strip()
157
-
158
- # Case 2: msg is dict containing 'content'
159
- if isinstance(msg, dict) and "content" in msg:
160
- return str(msg["content"]).strip()
161
-
162
- # Case 3: msg is a Python repr:
163
- # ChatCompletionMessage(content='{"score":...}', role='assistant')
164
- msg_str = str(msg)
165
- match = re.search(r"content='([\s\S]*?)'", msg_str)
166
- if match:
167
- return match.group(1).strip()
168
-
169
- # Fallback
170
- return msg_str.strip()
171
 
172
 
173
  # ===========================
174
  # ROUTES
175
  # ===========================
176
-
177
  @app.get("/")
178
  def root():
179
  return {"status": "Financial Health Score service is running"}
@@ -187,7 +250,7 @@ def financial_score(payload: ScoreRequest):
187
  except Exception:
188
  raise HTTPException(status_code=400, detail="Invalid userId")
189
 
190
- # Fetch budgets
191
  budgets = list(budget_collection.find({"createdBy": user_id}))
192
 
193
  # Fetch last 30 days transactions
@@ -199,11 +262,12 @@ def financial_score(payload: ScoreRequest):
199
  }).sort("date", -1).limit(100)
200
  )
201
 
 
202
  if not budgets and not transactions:
203
  return {
204
  "userId": payload.userId,
205
  "score": 0,
206
- "explanation": "No budgets or transactions found."
207
  }
208
 
209
  prompt = score_prompt(budgets, transactions)
@@ -218,17 +282,16 @@ def financial_score(payload: ScoreRequest):
218
  except Exception as exc:
219
  raise HTTPException(status_code=502, detail=f"OpenAI request failed: {exc}")
220
 
221
- # Extract text safely
222
  model_output = extract_openai_text(response)
223
 
224
- # Parse JSON
225
  try:
226
  parsed = extract_json_payload(model_output)
227
  except Exception:
228
  raise HTTPException(
229
  status_code=502,
230
  detail={
231
- "error": "Unable to parse OpenAI response",
232
  "rawResponse": model_output
233
  },
234
  )
@@ -243,7 +306,7 @@ def financial_score(payload: ScoreRequest):
243
  }
244
  )
245
 
246
- # Score limits
247
  try:
248
  score_val = int(float(parsed["score"]))
249
  score_val = max(0, min(100, score_val))
@@ -255,3 +318,4 @@ def financial_score(payload: ScoreRequest):
255
  "score": score_val,
256
  "explanation": parsed["explanation"]
257
  }
 
 
1
+ # main.py
2
+ """
3
+ Financial Health Score Service (FastAPI)
4
+
5
+ Features:
6
+ - Fetches user budgets and last-30-day transactions from MongoDB
7
+ - Looks up transaction currency (from `currencies` collection) and embeds currency code
8
+ - Builds a careful prompt for OpenAI (gpt-4o-mini) that instructs usage of currency code
9
+ - Calls OpenAI and reliably extracts JSON output
10
+ - Returns {"userId", "score", "explanation"}
11
+
12
+ IMPORTANT:
13
+ - Ensure MONGO_URI and OPENAI_API_KEY are set in your environment.
14
+ - The `currencies` collection name is assumed to be "currencies".
15
+ - This file uses the OpenAI Python client (OpenAI(api_key=...)) per your earlier setup.
16
+ """
17
+
18
  import json
19
  import os
20
  import re
 
48
 
49
  budget_collection = default_db["budget"]
50
  transaction_collection = default_db["transactions"]
51
+ currencies_collection = default_db["currencies"] # <-- currencies collection
52
 
53
  # OpenAI client
54
  openai = OpenAI(api_key=OPENAI_API_KEY)
 
67
  # ===========================
68
  # HELPERS
69
  # ===========================
 
70
  def safe_number(value):
71
  try:
72
  return float(value)
 
106
 
107
 
108
  def normalize_transactions(transactions):
109
+ """
110
+ Trim transactions and attach currency code (e.g., 'EUR', 'INR', 'USD') when possible.
111
+ """
112
  trimmed = []
113
  for txn in transactions:
114
  date_value = txn.get("date")
 
118
  else:
119
  date_str = None
120
 
121
+ # ---- Currency lookup ----
122
+ currency_code = None
123
+ currency_id = txn.get("currency")
124
+
125
+ try:
126
+ # currency may be stored as an ObjectId already; handle strings too
127
+ if isinstance(currency_id, ObjectId):
128
+ currency_doc = currencies_collection.find_one({"_id": currency_id})
129
+ elif isinstance(currency_id, dict) and "$oid" in currency_id:
130
+ # sometimes the raw export contains {"$oid": "..."}
131
+ try:
132
+ currency_doc = currencies_collection.find_one({"_id": ObjectId(currency_id["$oid"])})
133
+ except Exception:
134
+ currency_doc = None
135
+ elif isinstance(currency_id, str):
136
+ try:
137
+ currency_doc = currencies_collection.find_one({"_id": ObjectId(currency_id)})
138
+ except Exception:
139
+ currency_doc = currencies_collection.find_one({"code": currency_id}) or currencies_collection.find_one({"currency": currency_id})
140
+ else:
141
+ currency_doc = None
142
+
143
+ if currency_doc:
144
+ # Option A: use currency code only (e.g., "EUR")
145
+ currency_code = currency_doc.get("code") or currency_doc.get("currency")
146
+
147
+ except Exception:
148
+ currency_code = None
149
+
150
  trimmed.append({
151
  "type": txn.get("type"),
152
  "amount": safe_number(txn.get("amount")),
153
+ "currency": currency_code, # <-- added
154
  "date": date_str,
155
  })
156
  return trimmed
157
 
158
 
159
  def score_prompt(budgets, transactions):
160
+ # We instruct the model to use currency code when mentioning amounts (Option A)
161
  return f"""
162
+ You are a succinct financial wellness expert. Using the budgets and last 30 days of transactions below,
163
+ rate the user's financial health on a scale from 0 to 100 (higher is better).
164
+
165
+ IMPORTANT:
166
+ - When referring to monetary amounts, ALWAYS prefix with the currency code if available.
167
+ Example: "EUR 10,000", "INR 5,000", "USD 200".
168
+ - If a transaction has no currency code, you may use the number only (e.g., 1000).
169
+ - Keep the explanation short (one or two sentences) and directly related to budgets and transactions.
170
 
171
  Budgets:
172
  {json.dumps(normalize_budgets(budgets), indent=2)}
 
174
  Transactions (last 30 days):
175
  {json.dumps(normalize_transactions(transactions), indent=2)}
176
 
177
+ Respond only with valid JSON, nothing else, using this exact shape:
178
+ {{ "score": number, "explanation": "short explanation" }}
179
  """
180
+
181
 
182
  def extract_json_payload(text):
183
  """Extract JSON from plain text or fenced code blocks."""
184
  trimmed = (text or "").strip()
185
 
186
+ # try fenced json block first
187
  fenced = re.search(r"```(?:json)?\s*([\s\S]*?)```", trimmed)
188
  if fenced:
189
  return json.loads(fenced.group(1).strip())
190
 
191
+ # try to find first { ... } substring
192
+ first_obj = re.search(r"(\{[\s\S]*\})", trimmed)
193
+ if first_obj:
194
+ return json.loads(first_obj.group(1))
195
+
196
+ # last resort: direct JSON load
197
  return json.loads(trimmed)
198
 
199
 
200
  # ===========================
201
+ # BULLETPROOF OPENAI EXTRACTOR
202
  # ===========================
 
203
  def extract_openai_text(response):
204
  """
205
+ Robust extractor for OpenAI SDK responses.
206
+ Handles several possible message wrappers and returns the assistant text.
 
 
 
 
207
  """
208
  try:
209
+ # Best-effort to access nested choice message content
210
+ choices = getattr(response, "choices", None) or response.get("choices") if isinstance(response, dict) else None
211
+ if not choices:
212
+ # fallback: maybe response is a dict-like structure
213
+ return str(response)
214
+
215
+ msg = choices[0].get("message") if isinstance(choices[0], dict) else getattr(choices[0], "message", None)
216
+ if not msg:
217
+ return str(choices[0])
218
+
219
+ # If message exposes 'content'
220
+ if isinstance(msg, dict) and "content" in msg:
221
+ return msg["content"].strip()
222
+ if hasattr(msg, "content"):
223
+ return msg.content.strip()
224
+
225
+ # If message is a repr like ChatCompletionMessage(content='...'), extract via regex
226
+ msg_str = str(msg)
227
+ match = re.search(r"content='([\s\S]*?)'", msg_str)
228
+ if match:
229
+ return match.group(1).strip()
230
+
231
+ # fallback
232
+ return msg_str.strip()
233
  except Exception:
234
+ return str(response)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
235
 
236
 
237
  # ===========================
238
  # ROUTES
239
  # ===========================
 
240
  @app.get("/")
241
  def root():
242
  return {"status": "Financial Health Score service is running"}
 
250
  except Exception:
251
  raise HTTPException(status_code=400, detail="Invalid userId")
252
 
253
+ # Fetch budgets (all budgets created by this user)
254
  budgets = list(budget_collection.find({"createdBy": user_id}))
255
 
256
  # Fetch last 30 days transactions
 
262
  }).sort("date", -1).limit(100)
263
  )
264
 
265
+ # If neither budgets nor recent transactions exist -> score 0
266
  if not budgets and not transactions:
267
  return {
268
  "userId": payload.userId,
269
  "score": 0,
270
+ "explanation": "No budgets or recent transactions found."
271
  }
272
 
273
  prompt = score_prompt(budgets, transactions)
 
282
  except Exception as exc:
283
  raise HTTPException(status_code=502, detail=f"OpenAI request failed: {exc}")
284
 
 
285
  model_output = extract_openai_text(response)
286
 
287
+ # Parse JSON payload from model output
288
  try:
289
  parsed = extract_json_payload(model_output)
290
  except Exception:
291
  raise HTTPException(
292
  status_code=502,
293
  detail={
294
+ "error": "Unable to parse OpenAI response as JSON",
295
  "rawResponse": model_output
296
  },
297
  )
 
306
  }
307
  )
308
 
309
+ # Clamp score to 0..100
310
  try:
311
  score_val = int(float(parsed["score"]))
312
  score_val = max(0, min(100, score_val))
 
318
  "score": score_val,
319
  "explanation": parsed["explanation"]
320
  }
321
+