LogicGoInfotechSpaces commited on
Commit
b68e9e4
·
verified ·
1 Parent(s): bcb8d98

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +76 -33
app.py CHANGED
@@ -10,17 +10,17 @@ from openai import OpenAI
10
  from pydantic import BaseModel
11
  from pymongo import MongoClient
12
 
13
- # Load .env variables
14
  load_dotenv()
15
 
16
  MONGO_URI = os.getenv("MONGO_URI")
17
  OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
18
 
19
  if not MONGO_URI:
20
- raise RuntimeError("MONGO_URI missing in environment variables")
21
 
22
  if not OPENAI_API_KEY:
23
- raise RuntimeError("OPENAI_API_KEY missing in environment variables")
24
 
25
  # MongoDB Setup
26
  mongo_client = MongoClient(MONGO_URI)
@@ -31,19 +31,19 @@ if default_db is None:
31
  budget_collection = default_db["budget"]
32
  transaction_collection = default_db["transactions"]
33
 
34
- # OpenAI Client
35
  openai = OpenAI(api_key=OPENAI_API_KEY)
36
 
37
- # FastAPI App
38
  app = FastAPI(title="Financial Health Score Service")
39
 
40
 
41
- # Request Model
42
  class ScoreRequest(BaseModel):
43
  userId: str
44
 
45
 
46
- # Helpers
 
47
  def safe_number(value):
48
  try:
49
  return float(value)
@@ -78,22 +78,25 @@ def normalize_budgets(budgets):
78
  "notifications": budget.get("notifications") or [],
79
  "headCategories": head_categories,
80
  })
81
-
82
  return normalized
83
 
84
 
85
  def normalize_transactions(transactions):
86
  trimmed = []
87
  for txn in transactions:
88
- date_str = None
89
- if isinstance(txn.get("date"), datetime):
90
- date_str = txn["date"].date().isoformat()
 
 
 
91
 
92
  trimmed.append({
93
  "type": txn.get("type"),
94
  "amount": safe_number(txn.get("amount")),
95
  "date": date_str,
96
  })
 
97
  return trimmed
98
 
99
 
@@ -114,15 +117,58 @@ Respond ONLY with:
114
 
115
 
116
  def extract_json_payload(text):
117
- """
118
- Removes markdown fences and extracts pure JSON.
119
- """
120
  trimmed = (text or "").strip()
121
  match = re.search(r"```(?:json)?\s*([\s\S]*?)```", trimmed)
 
122
  payload = match.group(1).strip() if match else trimmed
123
  return json.loads(payload)
124
 
125
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
126
  @app.get("/")
127
  def root():
128
  return {"status": "Financial Health Score service is running"}
@@ -130,7 +176,7 @@ def root():
130
 
131
  @app.post("/financial-score")
132
  def financial_score(payload: ScoreRequest):
133
- # Validate userId
134
  try:
135
  user_id = ObjectId(payload.userId)
136
  except Exception:
@@ -139,7 +185,7 @@ def financial_score(payload: ScoreRequest):
139
  # Fetch budgets
140
  budgets = list(budget_collection.find({"createdBy": user_id}))
141
 
142
- # Fetch transactions (last 30 days)
143
  thirty_days_ago = datetime.utcnow() - timedelta(days=30)
144
  transactions = list(
145
  transaction_collection.find({
@@ -148,7 +194,7 @@ def financial_score(payload: ScoreRequest):
148
  }).sort("date", -1).limit(100)
149
  )
150
 
151
- # No data fallback
152
  if not budgets and not transactions:
153
  return {
154
  "userId": payload.userId,
@@ -156,10 +202,10 @@ def financial_score(payload: ScoreRequest):
156
  "explanation": "No budgets or transactions found."
157
  }
158
 
159
- # Build LLM Prompt
160
  prompt = score_prompt(budgets, transactions)
161
 
162
- # Call OpenAI (new SDK)
163
  try:
164
  response = openai.chat.completions.create(
165
  model="gpt-4o-mini",
@@ -169,16 +215,10 @@ def financial_score(payload: ScoreRequest):
169
  except Exception as exc:
170
  raise HTTPException(status_code=502, detail=f"OpenAI request failed: {exc}")
171
 
172
- # Extract content text
173
- try:
174
- model_output = response.choices[0].message["content"]
175
- except Exception:
176
- raise HTTPException(
177
- status_code=502,
178
- detail="Invalid response format from OpenAI"
179
- )
180
 
181
- # Parse JSON response
182
  try:
183
  parsed = extract_json_payload(model_output)
184
  except Exception:
@@ -187,18 +227,21 @@ def financial_score(payload: ScoreRequest):
187
  detail={"error": "Unable to parse OpenAI response", "rawResponse": model_output}
188
  )
189
 
190
- # Validate fields
191
  if "score" not in parsed or "explanation" not in parsed:
192
  raise HTTPException(
193
  status_code=502,
194
- detail={"error": "Invalid OpenAI response", "rawResponse": model_output}
195
  )
196
 
197
- # Clamp score 0–100
198
- score_val = max(0, min(100, int(parsed["score"])))
 
 
 
199
 
200
  return {
201
  "userId": payload.userId,
202
  "score": score_val,
203
  "explanation": parsed["explanation"]
204
- }
 
10
  from pydantic import BaseModel
11
  from pymongo import MongoClient
12
 
13
+ # Load environment
14
  load_dotenv()
15
 
16
  MONGO_URI = os.getenv("MONGO_URI")
17
  OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
18
 
19
  if not MONGO_URI:
20
+ raise RuntimeError("MONGO_URI missing in environment variables")
21
 
22
  if not OPENAI_API_KEY:
23
+ raise RuntimeError("OPENAI_API_KEY missing in environment variables")
24
 
25
  # MongoDB Setup
26
  mongo_client = MongoClient(MONGO_URI)
 
31
  budget_collection = default_db["budget"]
32
  transaction_collection = default_db["transactions"]
33
 
34
+ # OpenAI client
35
  openai = OpenAI(api_key=OPENAI_API_KEY)
36
 
37
+ # FastAPI app
38
  app = FastAPI(title="Financial Health Score Service")
39
 
40
 
 
41
  class ScoreRequest(BaseModel):
42
  userId: str
43
 
44
 
45
+ # ----------------------------- HELPERS ---------------------------------- #
46
+
47
  def safe_number(value):
48
  try:
49
  return float(value)
 
78
  "notifications": budget.get("notifications") or [],
79
  "headCategories": head_categories,
80
  })
 
81
  return normalized
82
 
83
 
84
  def normalize_transactions(transactions):
85
  trimmed = []
86
  for txn in transactions:
87
+ date_value = txn.get("date")
88
+
89
+ if isinstance(date_value, datetime):
90
+ date_str = date_value.date().isoformat()
91
+ else:
92
+ date_str = None
93
 
94
  trimmed.append({
95
  "type": txn.get("type"),
96
  "amount": safe_number(txn.get("amount")),
97
  "date": date_str,
98
  })
99
+
100
  return trimmed
101
 
102
 
 
117
 
118
 
119
  def extract_json_payload(text):
120
+ """Extract JSON from plain text or fenced code blocks."""
 
 
121
  trimmed = (text or "").strip()
122
  match = re.search(r"```(?:json)?\s*([\s\S]*?)```", trimmed)
123
+
124
  payload = match.group(1).strip() if match else trimmed
125
  return json.loads(payload)
126
 
127
 
128
+ def extract_openai_text(response):
129
+ """
130
+ Safely extracts text from ANY OpenAI Chat Completion output.
131
+ Handles:
132
+ - string output
133
+ - list of blocks
134
+ - dict with 'content'
135
+ - dict with 'text'
136
+ """
137
+ try:
138
+ message = response.choices[0].message
139
+ except Exception:
140
+ raise HTTPException(status_code=502, detail="OpenAI returned no valid choices")
141
+
142
+ # Case: dict with content
143
+ if isinstance(message, dict) and "content" in message:
144
+ content = message["content"]
145
+ else:
146
+ content = message
147
+
148
+ # Simple string
149
+ if isinstance(content, str):
150
+ return content.strip()
151
+
152
+ # List of blocks
153
+ if isinstance(content, list):
154
+ output_chunks = []
155
+ for block in content:
156
+ if isinstance(block, dict) and "text" in block:
157
+ output_chunks.append(block["text"])
158
+ elif isinstance(block, str):
159
+ output_chunks.append(block)
160
+ return "\n".join(output_chunks).strip()
161
+
162
+ # Dict with text
163
+ if isinstance(content, dict) and "text" in content:
164
+ return content["text"].strip()
165
+
166
+ # Fallback: stringify
167
+ return str(content).strip()
168
+
169
+
170
+ # ----------------------------- ROUTES ---------------------------------- #
171
+
172
  @app.get("/")
173
  def root():
174
  return {"status": "Financial Health Score service is running"}
 
176
 
177
  @app.post("/financial-score")
178
  def financial_score(payload: ScoreRequest):
179
+ # Validate ObjectId
180
  try:
181
  user_id = ObjectId(payload.userId)
182
  except Exception:
 
185
  # Fetch budgets
186
  budgets = list(budget_collection.find({"createdBy": user_id}))
187
 
188
+ # Fetch last 30 days transactions
189
  thirty_days_ago = datetime.utcnow() - timedelta(days=30)
190
  transactions = list(
191
  transaction_collection.find({
 
194
  }).sort("date", -1).limit(100)
195
  )
196
 
197
+ # If no data
198
  if not budgets and not transactions:
199
  return {
200
  "userId": payload.userId,
 
202
  "explanation": "No budgets or transactions found."
203
  }
204
 
205
+ # Build LLM prompt
206
  prompt = score_prompt(budgets, transactions)
207
 
208
+ # Call OpenAI (new SDK format)
209
  try:
210
  response = openai.chat.completions.create(
211
  model="gpt-4o-mini",
 
215
  except Exception as exc:
216
  raise HTTPException(status_code=502, detail=f"OpenAI request failed: {exc}")
217
 
218
+ # Extract text safely
219
+ model_output = extract_openai_text(response)
 
 
 
 
 
 
220
 
221
+ # Parse JSON
222
  try:
223
  parsed = extract_json_payload(model_output)
224
  except Exception:
 
227
  detail={"error": "Unable to parse OpenAI response", "rawResponse": model_output}
228
  )
229
 
230
+ # Validate format
231
  if "score" not in parsed or "explanation" not in parsed:
232
  raise HTTPException(
233
  status_code=502,
234
+ detail={"error": "OpenAI response missing required fields", "rawResponse": model_output}
235
  )
236
 
237
+ # Clamp score to 0–100
238
+ try:
239
+ score_val = max(0, min(100, int(float(parsed["score"]))))
240
+ except Exception:
241
+ score_val = 0
242
 
243
  return {
244
  "userId": payload.userId,
245
  "score": score_val,
246
  "explanation": parsed["explanation"]
247
+ }