LogicGoInfotechSpaces commited on
Commit
0f4ddd7
·
verified ·
1 Parent(s): 68e64de

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +116 -88
app.py CHANGED
@@ -1,8 +1,13 @@
1
  # main.py
2
  """
3
  Financial Health Score Service (FastAPI)
4
- Now using GPT-4.1 with strict JSON mode.
5
- This guarantees the model ALWAYS returns valid JSON.
 
 
 
 
 
6
  """
7
 
8
  import json
@@ -16,7 +21,9 @@ from openai import OpenAI
16
  from pydantic import BaseModel
17
  from pymongo import MongoClient
18
 
19
- # Load environment
 
 
20
  load_dotenv()
21
 
22
  MONGO_URI = os.getenv("MONGO_URI")
@@ -28,18 +35,17 @@ if not MONGO_URI:
28
  if not OPENAI_API_KEY:
29
  raise RuntimeError("❌ OPENAI_API_KEY missing in environment variables")
30
 
31
- # MongoDB setup
32
  mongo_client = MongoClient(MONGO_URI)
33
  default_db = mongo_client.get_default_database()
 
 
34
 
35
  budget_collection = default_db["budget"]
36
  transaction_collection = default_db["transactions"]
37
  currencies_collection = default_db["currencies"]
38
 
39
- # OpenAI client
40
  openai = OpenAI(api_key=OPENAI_API_KEY)
41
 
42
- # FastAPI app
43
  app = FastAPI(title="Financial Health Score Service")
44
 
45
 
@@ -53,100 +59,98 @@ class ScoreRequest(BaseModel):
53
  # ===========================
54
  # HELPERS
55
  # ===========================
56
- def safe_number(value):
57
  try:
58
- return float(value)
59
- except Exception:
60
  return None
61
 
62
 
63
  def normalize_budgets(budgets):
64
- normalized = []
65
- for budget in budgets:
66
- head_categories = []
67
- heads = budget.get("headCategories") or []
68
-
69
- for head in heads:
70
- head_categories.append({
71
- "spendLimitType": head.get("spendLimitType"),
72
- "spendAmount": safe_number(head.get("spendAmount")),
73
- "maxAmount": safe_number(head.get("maxAmount")),
74
- "remainingAmount": safe_number(head.get("remainingAmount")),
75
- "notifications": head.get("notifications") or [],
76
  })
77
 
78
- normalized.append({
79
- "name": budget.get("name"),
80
- "status": budget.get("status"),
81
- "period": budget.get("period"),
82
- "maxAmount": safe_number(budget.get("maxAmount")),
83
- "spendAmount": safe_number(budget.get("spendAmount")),
84
- "remainingAmount": safe_number(budget.get("remainingAmount")),
85
- "rollover": budget.get("rollover"),
86
- "notifications": budget.get("notifications") or [],
87
- "headCategories": head_categories,
88
  })
89
-
90
- return normalized
91
 
92
 
93
- def normalize_transactions(transactions):
94
- trimmed = []
95
- for txn in transactions:
96
- date_value = txn.get("date")
97
- date_str = date_value.date().isoformat() if isinstance(date_value, datetime) else None
 
 
 
98
 
 
99
  currency_code = None
100
  currency_id = txn.get("currency")
101
 
102
  try:
103
- # Handle ObjectId or string forms
104
  if isinstance(currency_id, ObjectId):
105
- currency_doc = currencies_collection.find_one({"_id": currency_id})
106
- elif isinstance(currency_id, str):
107
- currency_doc = currencies_collection.find_one({"_id": ObjectId(currency_id)})
108
  elif isinstance(currency_id, dict) and "$oid" in currency_id:
109
- currency_doc = currencies_collection.find_one({"_id": ObjectId(currency_id["$oid"])})
 
 
 
 
 
 
110
  else:
111
- currency_doc = None
112
 
113
- if currency_doc:
114
- currency_code = currency_doc.get("code") or currency_doc.get("currency")
115
-
116
- except Exception:
117
  currency_code = None
118
 
119
- trimmed.append({
120
  "type": txn.get("type"),
121
  "amount": safe_number(txn.get("amount")),
122
  "currency": currency_code,
123
  "date": date_str,
124
  })
125
- return trimmed
126
 
127
 
128
- def score_prompt(budgets, transactions):
129
- return {
130
- "role": "user",
131
- "content": f"""
132
- You are a financial wellness expert. Using the budgets and last 30 days of transactions below,
133
- rate the user’s financial health from 0 to 100 (higher = better).
134
 
135
  Rules:
136
- - Always prefix amounts with currency code when available (e.g., INR 5000).
137
- - Keep the explanation short (1–2 sentences).
138
- - Consider income, expenses, spending consistency, and remaining budgets.
 
139
 
140
  Budgets:
141
- {json.dumps(normalize_budgets(budgets), indent=2)}
142
 
143
- Transactions (last 30 days):
144
- {json.dumps(normalize_transactions(transactions), indent=2)}
145
-
146
- Respond ONLY using this exact JSON structure:
147
- {{ "score": number, "explanation": "string" }}
148
  """
149
- }
150
 
151
 
152
  # ===========================
@@ -154,7 +158,7 @@ Respond ONLY using this exact JSON structure:
154
  # ===========================
155
  @app.get("/")
156
  def root():
157
- return {"status": "Financial Health Score service is running"}
158
 
159
 
160
  @app.post("/financial-score")
@@ -162,21 +166,23 @@ def financial_score(payload: ScoreRequest):
162
  # Validate ObjectId
163
  try:
164
  user_id = ObjectId(payload.userId)
165
- except Exception:
166
  raise HTTPException(status_code=400, detail="Invalid userId")
167
 
168
  # Fetch budgets
169
- budgets = list(budget_collection.find({"createdBy": user_id}))
 
170
 
171
  # Fetch last 30 days transactions
172
  thirty_days_ago = datetime.utcnow() - timedelta(days=30)
173
- transactions = list(
174
- transaction_collection.find({
175
- "user": user_id,
176
- "date": {"$gte": thirty_days_ago}
177
- }).sort("date", -1).limit(120)
178
  )
 
179
 
 
180
  if not budgets and not transactions:
181
  return {
182
  "userId": payload.userId,
@@ -184,35 +190,57 @@ def financial_score(payload: ScoreRequest):
184
  "explanation": "No budgets or recent transactions found."
185
  }
186
 
187
- # Build messages
188
- messages = [score_prompt(budgets, transactions)]
189
 
190
- # -- Strict JSON Mode using GPT-4.1 --
 
 
191
  try:
192
  response = openai.chat.completions.create(
193
  model="gpt-4.1",
194
- response_format={"type": "json_object"}, # 🔥 Guarantees valid JSON
195
- messages=messages,
196
- temperature=0.4,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
197
  )
198
  except Exception as exc:
199
  raise HTTPException(status_code=502, detail=f"OpenAI request failed: {exc}")
200
 
201
- # Direct JSON no more parsing issues
202
- parsed = response.choices[0].message.parsed
 
 
 
 
 
 
203
 
204
- # Validate shapes
205
  score_val = parsed.get("score", 0)
206
- explanation = parsed.get("explanation", "")
207
-
208
  try:
209
  score_val = int(float(score_val))
210
- score_val = max(0, min(100, score_val))
211
- except Exception:
212
  score_val = 0
 
213
 
214
  return {
215
  "userId": payload.userId,
216
  "score": score_val,
217
- "explanation": explanation
218
  }
 
1
  # main.py
2
  """
3
  Financial Health Score Service (FastAPI)
4
+
5
+ Rewrite includes:
6
+ - Uses GPT-4.1 with strict JSON schema (response_format)
7
+ - OpenAI JSON is guaranteed valid -> uses message.parsed
8
+ - Bulletproof Mongo fetch for budgets, transactions, currencies
9
+ - Fully rewritten OpenAI call (Option A)
10
+ - No messy regex parsing
11
  """
12
 
13
  import json
 
21
  from pydantic import BaseModel
22
  from pymongo import MongoClient
23
 
24
+ # ===========================
25
+ # ENV & CLIENT SETUP
26
+ # ===========================
27
  load_dotenv()
28
 
29
  MONGO_URI = os.getenv("MONGO_URI")
 
35
  if not OPENAI_API_KEY:
36
  raise RuntimeError("❌ OPENAI_API_KEY missing in environment variables")
37
 
 
38
  mongo_client = MongoClient(MONGO_URI)
39
  default_db = mongo_client.get_default_database()
40
+ if default_db is None:
41
+ raise RuntimeError("Unable to determine default database from MONGO_URI")
42
 
43
  budget_collection = default_db["budget"]
44
  transaction_collection = default_db["transactions"]
45
  currencies_collection = default_db["currencies"]
46
 
 
47
  openai = OpenAI(api_key=OPENAI_API_KEY)
48
 
 
49
  app = FastAPI(title="Financial Health Score Service")
50
 
51
 
 
59
  # ===========================
60
  # HELPERS
61
  # ===========================
62
+ def safe_number(v):
63
  try:
64
+ return float(v)
65
+ except:
66
  return None
67
 
68
 
69
  def normalize_budgets(budgets):
70
+ out = []
71
+ for b in budgets:
72
+ heads = []
73
+ for h in b.get("headCategories", []) or []:
74
+ heads.append({
75
+ "spendLimitType": h.get("spendLimitType"),
76
+ "spendAmount": safe_number(h.get("spendAmount")),
77
+ "maxAmount": safe_number(h.get("maxAmount")),
78
+ "remainingAmount": safe_number(h.get("remainingAmount")),
79
+ "notifications": h.get("notifications") or []
 
 
80
  })
81
 
82
+ out.append({
83
+ "name": b.get("name"),
84
+ "status": b.get("status"),
85
+ "period": b.get("period"),
86
+ "maxAmount": safe_number(b.get("maxAmount")),
87
+ "spendAmount": safe_number(b.get("spendAmount")),
88
+ "remainingAmount": safe_number(b.get("remainingAmount")),
89
+ "rollover": b.get("rollover"),
90
+ "notifications": b.get("notifications") or [],
91
+ "headCategories": heads,
92
  })
93
+ return out
 
94
 
95
 
96
+ def normalize_transactions(txns):
97
+ out = []
98
+ for txn in txns:
99
+ date_val = txn.get("date")
100
+ if isinstance(date_val, datetime):
101
+ date_str = date_val.date().isoformat()
102
+ else:
103
+ date_str = None
104
 
105
+ # Resolve currency
106
  currency_code = None
107
  currency_id = txn.get("currency")
108
 
109
  try:
 
110
  if isinstance(currency_id, ObjectId):
111
+ doc = currencies_collection.find_one({"_id": currency_id})
 
 
112
  elif isinstance(currency_id, dict) and "$oid" in currency_id:
113
+ doc = currencies_collection.find_one({"_id": ObjectId(currency_id["$oid"])})
114
+ elif isinstance(currency_id, str):
115
+ try:
116
+ doc = currencies_collection.find_one({"_id": ObjectId(currency_id)})
117
+ except:
118
+ doc = currencies_collection.find_one({"code": currency_id}) or \
119
+ currencies_collection.find_one({"currency": currency_id})
120
  else:
121
+ doc = None
122
 
123
+ if doc:
124
+ currency_code = doc.get("code") or doc.get("currency")
125
+ except:
 
126
  currency_code = None
127
 
128
+ out.append({
129
  "type": txn.get("type"),
130
  "amount": safe_number(txn.get("amount")),
131
  "currency": currency_code,
132
  "date": date_str,
133
  })
134
+ return out
135
 
136
 
137
+ def build_prompt(budgets, transactions):
138
+ return f"""
139
+ You are a financial wellness expert. Use the user's budgets and last 30 days of transactions
140
+ to compute a financial health score from 0 to 100.
 
 
141
 
142
  Rules:
143
+ - When mentioning amounts, ALWAYS prefix with currency code when available (e.g., "INR 10,000").
144
+ - Keep explanation short (1–2 sentences).
145
+ - Score must reflect spending habits, budget usage, and financial balance.
146
+ - Respond ONLY using the JSON schema enforced below.
147
 
148
  Budgets:
149
+ {json.dumps(budgets, indent=2)}
150
 
151
+ Transactions:
152
+ {json.dumps(transactions, indent=2)}
 
 
 
153
  """
 
154
 
155
 
156
  # ===========================
 
158
  # ===========================
159
  @app.get("/")
160
  def root():
161
+ return {"status": "Financial Health Score service running"}
162
 
163
 
164
  @app.post("/financial-score")
 
166
  # Validate ObjectId
167
  try:
168
  user_id = ObjectId(payload.userId)
169
+ except:
170
  raise HTTPException(status_code=400, detail="Invalid userId")
171
 
172
  # Fetch budgets
173
+ budgets_raw = list(budget_collection.find({"createdBy": user_id}))
174
+ budgets = normalize_budgets(budgets_raw)
175
 
176
  # Fetch last 30 days transactions
177
  thirty_days_ago = datetime.utcnow() - timedelta(days=30)
178
+ txns_raw = list(
179
+ transaction_collection.find(
180
+ {"user": user_id, "date": {"$gte": thirty_days_ago}}
181
+ ).sort("date", -1).limit(150)
 
182
  )
183
+ transactions = normalize_transactions(txns_raw)
184
 
185
+ # If no data at all
186
  if not budgets and not transactions:
187
  return {
188
  "userId": payload.userId,
 
190
  "explanation": "No budgets or recent transactions found."
191
  }
192
 
193
+ prompt = build_prompt(budgets, transactions)
 
194
 
195
+ # ===========================
196
+ # STRICT GPT-4.1 JSON (Option A)
197
+ # ===========================
198
  try:
199
  response = openai.chat.completions.create(
200
  model="gpt-4.1",
201
+ temperature=0.6,
202
+ response_format={
203
+ "type": "json_schema",
204
+ "json_schema": {
205
+ "name": "financial_score",
206
+ "schema": {
207
+ "type": "object",
208
+ "properties": {
209
+ "score": {"type": "number"},
210
+ "explanation": {"type": "string"}
211
+ },
212
+ "required": ["score", "explanation"],
213
+ "additionalProperties": False
214
+ }
215
+ }
216
+ },
217
+ messages=[
218
+ {"role": "system", "content": "You are a financial scoring engine."},
219
+ {"role": "user", "content": prompt}
220
+ ]
221
  )
222
  except Exception as exc:
223
  raise HTTPException(status_code=502, detail=f"OpenAI request failed: {exc}")
224
 
225
+ # `.parsed` contains GUARANTEED VALID JSON
226
+ try:
227
+ parsed = response.choices[0].message.parsed
228
+ except Exception:
229
+ raise HTTPException(
230
+ status_code=502,
231
+ detail="OpenAI response did not include parsed JSON (unexpected)"
232
+ )
233
 
234
+ # Score
235
  score_val = parsed.get("score", 0)
 
 
236
  try:
237
  score_val = int(float(score_val))
238
+ except:
 
239
  score_val = 0
240
+ score_val = max(0, min(100, score_val))
241
 
242
  return {
243
  "userId": payload.userId,
244
  "score": score_val,
245
+ "explanation": parsed.get("explanation")
246
  }