bpmredacademy commited on
Commit
23d6322
·
verified ·
1 Parent(s): bf80e55

Upload app.py

Browse files
Files changed (1) hide show
  1. app.py +718 -0
app.py ADDED
@@ -0,0 +1,718 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import time
3
+ import json
4
+ import csv
5
+ import io
6
+ import textwrap
7
+ from dataclasses import dataclass, field
8
+ from typing import Dict, Tuple, List, Any
9
+
10
+ import gradio as gr
11
+
12
+
13
+ # ============================================================
14
+ # Family Financial Intelligence (FFI)
15
+ # BPM RED Academy — HumAI MightHub
16
+ # Research Preview · Family Budgeting · Scenario Planning
17
+ # ============================================================
18
+
19
+ TITLE = "Family Financial Intelligence (FFI)"
20
+ SUBTITLE = "AI-assisted household financial planning · Strategic budgeting · Scenario stress testing"
21
+
22
+ # Set in Hugging Face Space -> Settings -> Variables and secrets
23
+ # Example:
24
+ # FFI_ACCESS_CODES = "FFI-EDIN-001,FFI-CLIENT-002"
25
+ # FFI_DAILY_QUOTA = "10"
26
+ # FFI_BRAND_CONTACT = "engagement@bpm.ba"
27
+ # FFI_MIN_CHARS = "30"
28
+
29
+ ACCESS_CODES_RAW = os.getenv("FFI_ACCESS_CODES", os.getenv("PFI_ACCESS_CODES", "")).strip()
30
+ DAILY_QUOTA = int(os.getenv("FFI_DAILY_QUOTA", os.getenv("PFI_DAILY_QUOTA", "10")).strip() or "10")
31
+ BRAND_CONTACT = os.getenv("FFI_BRAND_CONTACT", os.getenv("PFI_BRAND_CONTACT", "engagement@bpm.ba")).strip()
32
+ MIN_CHARS = int(os.getenv("FFI_MIN_CHARS", os.getenv("PFI_MIN_CHARS", "30")).strip() or "30")
33
+ VALID_CODES = {c.strip() for c in ACCESS_CODES_RAW.split(",") if c.strip()}
34
+ WINDOW_SECONDS = 24 * 60 * 60
35
+
36
+
37
+ @dataclass
38
+ class UsageEntry:
39
+ ts: list = field(default_factory=list)
40
+
41
+
42
+ @dataclass
43
+ class AccessState:
44
+ usage: Dict[str, UsageEntry] = field(default_factory=dict)
45
+ audit: list = field(default_factory=list)
46
+
47
+
48
+ def now_ts() -> float:
49
+ return time.time()
50
+
51
+
52
+ def utc_now_iso() -> str:
53
+ return time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
54
+
55
+
56
+ def clean_old(ts_list: list, now: float) -> list:
57
+ cutoff = now - WINDOW_SECONDS
58
+ return [t for t in ts_list if t >= cutoff]
59
+
60
+
61
+ def validate_code(code: str) -> Tuple[bool, str]:
62
+ code = (code or "").strip()
63
+ if not code:
64
+ return False, "Access Code is required."
65
+ if not VALID_CODES:
66
+ return False, "FFI is not configured for access codes yet. Add FFI_ACCESS_CODES in Space secrets."
67
+ if code not in VALID_CODES:
68
+ return False, "Invalid Access Code."
69
+ return True, "Access granted."
70
+
71
+
72
+ def money(value: Any) -> float:
73
+ try:
74
+ return round(float(value or 0), 2)
75
+ except Exception:
76
+ return 0.0
77
+
78
+
79
+ def pct(value: Any) -> float:
80
+ try:
81
+ return round(float(value or 0), 2)
82
+ except Exception:
83
+ return 0.0
84
+
85
+
86
+ def safe_div(a: float, b: float) -> float:
87
+ if not b:
88
+ return 0.0
89
+ return round(a / b, 4)
90
+
91
+
92
+ def parse_expenses_csv(raw: str) -> List[Dict[str, Any]]:
93
+ """
94
+ Expected rows:
95
+ category, amount, owner, type, note
96
+
97
+ Example:
98
+ Housing, 900, Joint, fixed, Rent or loan
99
+ Food, 650, Joint, variable, Groceries
100
+ AI Tools, 250, Edin, project, HF/OpenAI/Cloud
101
+ """
102
+ raw = (raw or "").strip()
103
+ if not raw:
104
+ return []
105
+
106
+ rows = []
107
+ f = io.StringIO(raw)
108
+ reader = csv.reader(f)
109
+ for row in reader:
110
+ if not row or all(not str(x).strip() for x in row):
111
+ continue
112
+
113
+ # Skip header if present
114
+ first = str(row[0]).strip().lower()
115
+ if first in {"category", "kategorija"}:
116
+ continue
117
+
118
+ category = str(row[0]).strip() if len(row) > 0 else "Uncategorized"
119
+ amount = money(row[1]) if len(row) > 1 else 0.0
120
+ owner = str(row[2]).strip() if len(row) > 2 else "Joint"
121
+ expense_type = str(row[3]).strip() if len(row) > 3 else "variable"
122
+ note = str(row[4]).strip() if len(row) > 4 else ""
123
+
124
+ rows.append({
125
+ "category": category or "Uncategorized",
126
+ "amount": amount,
127
+ "owner": owner or "Joint",
128
+ "type": expense_type or "variable",
129
+ "note": note,
130
+ })
131
+ return rows
132
+
133
+
134
+ def parse_goals_csv(raw: str) -> List[Dict[str, Any]]:
135
+ """
136
+ Expected rows:
137
+ goal, target_amount, current_amount, deadline_months, priority
138
+ """
139
+ raw = (raw or "").strip()
140
+ if not raw:
141
+ return []
142
+
143
+ rows = []
144
+ f = io.StringIO(raw)
145
+ reader = csv.reader(f)
146
+ for row in reader:
147
+ if not row or all(not str(x).strip() for x in row):
148
+ continue
149
+
150
+ first = str(row[0]).strip().lower()
151
+ if first in {"goal", "cilj"}:
152
+ continue
153
+
154
+ goal = str(row[0]).strip() if len(row) > 0 else "Goal"
155
+ target_amount = money(row[1]) if len(row) > 1 else 0.0
156
+ current_amount = money(row[2]) if len(row) > 2 else 0.0
157
+ deadline_months = int(money(row[3])) if len(row) > 3 else 12
158
+ priority = str(row[4]).strip() if len(row) > 4 else "Medium"
159
+
160
+ rows.append({
161
+ "goal": goal or "Goal",
162
+ "target_amount": target_amount,
163
+ "current_amount": current_amount,
164
+ "deadline_months": max(1, deadline_months),
165
+ "priority": priority or "Medium",
166
+ })
167
+ return rows
168
+
169
+
170
+ def category_totals(expenses: List[Dict[str, Any]]) -> Dict[str, float]:
171
+ totals = {}
172
+ for item in expenses:
173
+ cat = item.get("category", "Uncategorized")
174
+ totals[cat] = round(totals.get(cat, 0.0) + money(item.get("amount")), 2)
175
+ return totals
176
+
177
+
178
+ def owner_totals(expenses: List[Dict[str, Any]]) -> Dict[str, float]:
179
+ totals = {}
180
+ for item in expenses:
181
+ owner = item.get("owner", "Joint")
182
+ totals[owner] = round(totals.get(owner, 0.0) + money(item.get("amount")), 2)
183
+ return totals
184
+
185
+
186
+ def type_totals(expenses: List[Dict[str, Any]]) -> Dict[str, float]:
187
+ totals = {}
188
+ for item in expenses:
189
+ typ = item.get("type", "variable")
190
+ totals[typ] = round(totals.get(typ, 0.0) + money(item.get("amount")), 2)
191
+ return totals
192
+
193
+
194
+ def build_financial_snapshot(
195
+ household_name: str,
196
+ partner_a: str,
197
+ partner_b: str,
198
+ monthly_income_a: float,
199
+ monthly_income_b: float,
200
+ other_income: float,
201
+ cash_reserve: float,
202
+ debt_total: float,
203
+ debt_monthly_payment: float,
204
+ expenses_raw: str,
205
+ goals_raw: str,
206
+ ai_budget: float,
207
+ bpm_projected_income: float,
208
+ expected_one_time_income: float,
209
+ horizon_months: int,
210
+ emergency_target_months: int,
211
+ stress_income_drop_pct: float,
212
+ stress_expense_increase_pct: float,
213
+ notes: str,
214
+ ) -> Dict[str, Any]:
215
+
216
+ expenses = parse_expenses_csv(expenses_raw)
217
+ goals = parse_goals_csv(goals_raw)
218
+
219
+ income_a = money(monthly_income_a)
220
+ income_b = money(monthly_income_b)
221
+ other = money(other_income)
222
+ ai = money(ai_budget)
223
+ bpm_income = money(bpm_projected_income)
224
+ one_time = money(expected_one_time_income)
225
+
226
+ base_monthly_income = round(income_a + income_b + other, 2)
227
+ strategic_monthly_income = round(base_monthly_income + bpm_income, 2)
228
+
229
+ monthly_expenses = round(sum(money(x.get("amount")) for x in expenses) + ai, 2)
230
+ monthly_debt = money(debt_monthly_payment)
231
+ total_monthly_obligations = round(monthly_expenses + monthly_debt, 2)
232
+
233
+ base_cashflow = round(base_monthly_income - total_monthly_obligations, 2)
234
+ strategic_cashflow = round(strategic_monthly_income - total_monthly_obligations, 2)
235
+
236
+ reserve = money(cash_reserve)
237
+ emergency_target = round(total_monthly_obligations * max(1, int(emergency_target_months or 3)), 2)
238
+ reserve_gap = round(max(0.0, emergency_target - reserve), 2)
239
+
240
+ debt = money(debt_total)
241
+ debt_to_income = safe_div(monthly_debt, base_monthly_income)
242
+ expense_to_income = safe_div(total_monthly_obligations, base_monthly_income)
243
+ reserve_months = safe_div(reserve, total_monthly_obligations)
244
+
245
+ stress_income = round(base_monthly_income * (1 - pct(stress_income_drop_pct) / 100.0), 2)
246
+ stress_expenses = round(total_monthly_obligations * (1 + pct(stress_expense_increase_pct) / 100.0), 2)
247
+ stress_cashflow = round(stress_income - stress_expenses, 2)
248
+
249
+ goal_requirements = []
250
+ total_goal_monthly_need = 0.0
251
+ for g in goals:
252
+ remaining = round(max(0.0, money(g["target_amount"]) - money(g["current_amount"])), 2)
253
+ monthly_need = round(remaining / max(1, int(g["deadline_months"])), 2)
254
+ total_goal_monthly_need += monthly_need
255
+ goal_requirements.append({
256
+ **g,
257
+ "remaining_amount": remaining,
258
+ "monthly_required_saving": monthly_need,
259
+ })
260
+
261
+ total_goal_monthly_need = round(total_goal_monthly_need, 2)
262
+ cashflow_after_goals = round(base_cashflow - total_goal_monthly_need, 2)
263
+ strategic_cashflow_after_goals = round(strategic_cashflow - total_goal_monthly_need, 2)
264
+
265
+ if base_cashflow < 0:
266
+ posture = "Defensive"
267
+ elif reserve_months < emergency_target_months * 0.5:
268
+ posture = "Stabilization"
269
+ elif cashflow_after_goals < 0:
270
+ posture = "Goal-Constrained"
271
+ else:
272
+ posture = "Strategic"
273
+
274
+ risk_flags = []
275
+ if base_cashflow < 0:
276
+ risk_flags.append("Negative base monthly cash-flow")
277
+ if stress_cashflow < 0:
278
+ risk_flags.append("Stress scenario produces negative cash-flow")
279
+ if reserve_months < 1:
280
+ risk_flags.append("Cash reserve covers less than one month of obligations")
281
+ if debt_to_income > 0.35:
282
+ risk_flags.append("Debt payment burden is elevated relative to monthly income")
283
+ if expense_to_income > 0.85:
284
+ risk_flags.append("Obligations consume a very high share of monthly income")
285
+ if ai > 0 and safe_div(ai, base_monthly_income) > 0.10:
286
+ risk_flags.append("AI/cloud/tooling budget exceeds 10% of base monthly income")
287
+ if not risk_flags:
288
+ risk_flags.append("No immediate structural red flag identified from provided inputs")
289
+
290
+ snapshot = {
291
+ "document_type": "family_financial_intelligence_snapshot",
292
+ "generated_at": utc_now_iso(),
293
+ "household": {
294
+ "household_name": household_name.strip() or "Family Household",
295
+ "partner_a": partner_a.strip() or "Partner A",
296
+ "partner_b": partner_b.strip() or "Partner B",
297
+ },
298
+ "income": {
299
+ "partner_a_monthly_income": income_a,
300
+ "partner_b_monthly_income": income_b,
301
+ "other_monthly_income": other,
302
+ "bpm_or_projected_monthly_income": bpm_income,
303
+ "expected_one_time_income": one_time,
304
+ "base_monthly_income": base_monthly_income,
305
+ "strategic_monthly_income": strategic_monthly_income,
306
+ },
307
+ "expenses": {
308
+ "items": expenses,
309
+ "category_totals": category_totals(expenses),
310
+ "owner_totals": owner_totals(expenses),
311
+ "type_totals": type_totals(expenses),
312
+ "ai_development_budget": ai,
313
+ "monthly_expenses_excluding_debt": monthly_expenses,
314
+ "monthly_debt_payment": monthly_debt,
315
+ "total_monthly_obligations": total_monthly_obligations,
316
+ },
317
+ "assets_and_debt": {
318
+ "cash_reserve": reserve,
319
+ "total_debt": debt,
320
+ "reserve_months": reserve_months,
321
+ "debt_payment_to_income_ratio": debt_to_income,
322
+ "obligation_to_income_ratio": expense_to_income,
323
+ },
324
+ "goals": {
325
+ "items": goal_requirements,
326
+ "total_monthly_goal_saving_required": total_goal_monthly_need,
327
+ },
328
+ "cashflow": {
329
+ "base_monthly_cashflow": base_cashflow,
330
+ "strategic_monthly_cashflow": strategic_cashflow,
331
+ "cashflow_after_goals": cashflow_after_goals,
332
+ "strategic_cashflow_after_goals": strategic_cashflow_after_goals,
333
+ },
334
+ "emergency_reserve": {
335
+ "target_months": int(emergency_target_months or 3),
336
+ "target_amount": emergency_target,
337
+ "reserve_gap": reserve_gap,
338
+ },
339
+ "stress_test": {
340
+ "horizon_months": int(horizon_months or 12),
341
+ "income_drop_pct": pct(stress_income_drop_pct),
342
+ "expense_increase_pct": pct(stress_expense_increase_pct),
343
+ "stressed_income": stress_income,
344
+ "stressed_obligations": stress_expenses,
345
+ "stressed_cashflow": stress_cashflow,
346
+ },
347
+ "structural_posture": posture,
348
+ "risk_flags": risk_flags,
349
+ "notes": notes.strip(),
350
+ "boundary": {
351
+ "financial_advice": False,
352
+ "trading_signals": False,
353
+ "execution": False,
354
+ "purpose": "household planning, budgeting, scenario framing, and decision preparation",
355
+ }
356
+ }
357
+
358
+ return snapshot
359
+
360
+
361
+ def build_human_report(snapshot: Dict[str, Any]) -> str:
362
+ h = snapshot["household"]
363
+ income = snapshot["income"]
364
+ exp = snapshot["expenses"]
365
+ cf = snapshot["cashflow"]
366
+ reserve = snapshot["emergency_reserve"]
367
+ stress = snapshot["stress_test"]
368
+ goals = snapshot["goals"]
369
+ assets = snapshot["assets_and_debt"]
370
+
371
+ goals_text = ""
372
+ for g in goals["items"]:
373
+ goals_text += (
374
+ f"- {g['goal']}: target {g['target_amount']:.2f}, current {g['current_amount']:.2f}, "
375
+ f"remaining {g['remaining_amount']:.2f}, required/month {g['monthly_required_saving']:.2f}, "
376
+ f"priority {g['priority']}\n"
377
+ )
378
+ if not goals_text:
379
+ goals_text = "- No structured goals entered.\n"
380
+
381
+ category_text = "\n".join([f"- {k}: {v:.2f}" for k, v in exp["category_totals"].items()]) or "- No expense categories entered."
382
+
383
+ risk_text = "\n".join([f"- {x}" for x in snapshot["risk_flags"]])
384
+
385
+ if snapshot["structural_posture"] == "Defensive":
386
+ next_step = "First stabilize monthly cash-flow before adding new long-term commitments."
387
+ elif snapshot["structural_posture"] == "Stabilization":
388
+ next_step = "Prioritize emergency reserve build-up before aggressive project or lifestyle expansion."
389
+ elif snapshot["structural_posture"] == "Goal-Constrained":
390
+ next_step = "Re-rank goals and decide which goals are priority, delayed, reduced, or funded by new income."
391
+ else:
392
+ next_step = "Maintain disciplined budgeting and use surplus for priority goals, reserve growth, and strategic projects."
393
+
394
+ report = f"""
395
+ FAMILY FINANCIAL INTELLIGENCE REPORT
396
+ Research Preview · Informational · Non-executable
397
+
398
+ Household:
399
+ - {h['household_name']}
400
+ - Partners: {h['partner_a']} + {h['partner_b']}
401
+
402
+ 1) Monthly Position
403
+ - Base monthly income: {income['base_monthly_income']:.2f}
404
+ - Strategic monthly income including projected BPM/project income: {income['strategic_monthly_income']:.2f}
405
+ - Monthly expenses excluding debt: {exp['monthly_expenses_excluding_debt']:.2f}
406
+ - Monthly debt payment: {exp['monthly_debt_payment']:.2f}
407
+ - Total monthly obligations: {exp['total_monthly_obligations']:.2f}
408
+
409
+ 2) Cash-flow
410
+ - Base monthly cash-flow: {cf['base_monthly_cashflow']:.2f}
411
+ - Strategic monthly cash-flow: {cf['strategic_monthly_cashflow']:.2f}
412
+ - Monthly saving required for listed goals: {goals['total_monthly_goal_saving_required']:.2f}
413
+ - Base cash-flow after goals: {cf['cashflow_after_goals']:.2f}
414
+ - Strategic cash-flow after goals: {cf['strategic_cashflow_after_goals']:.2f}
415
+
416
+ 3) Reserve and Debt
417
+ - Cash reserve: {assets['cash_reserve']:.2f}
418
+ - Reserve coverage in months: {assets['reserve_months']:.2f}
419
+ - Emergency reserve target: {reserve['target_amount']:.2f}
420
+ - Reserve gap: {reserve['reserve_gap']:.2f}
421
+ - Debt payment / income ratio: {assets['debt_payment_to_income_ratio']:.2%}
422
+ - Obligations / income ratio: {assets['obligation_to_income_ratio']:.2%}
423
+
424
+ 4) Expense Structure
425
+ {category_text}
426
+
427
+ 5) Goals
428
+ {goals_text}
429
+ 6) Stress Test
430
+ - Horizon: {stress['horizon_months']} months
431
+ - Income drop: {stress['income_drop_pct']:.2f}%
432
+ - Expense increase: {stress['expense_increase_pct']:.2f}%
433
+ - Stressed monthly income: {stress['stressed_income']:.2f}
434
+ - Stressed monthly obligations: {stress['stressed_obligations']:.2f}
435
+ - Stressed monthly cash-flow: {stress['stressed_cashflow']:.2f}
436
+
437
+ 7) Structural Posture
438
+ - {snapshot['structural_posture']}
439
+
440
+ 8) Risk Flags
441
+ {risk_text}
442
+
443
+ 9) Suggested Next Planning Step
444
+ - {next_step}
445
+
446
+ Important boundary:
447
+ This output is for household planning, budgeting, and scenario framing only.
448
+ It is not financial advice, investment advice, trading guidance, tax advice, legal advice, or execution authority.
449
+ """
450
+ return textwrap.dedent(report).strip()
451
+
452
+
453
+ def gated_generate_report(
454
+ access_code: str,
455
+ household_name: str,
456
+ partner_a: str,
457
+ partner_b: str,
458
+ monthly_income_a: float,
459
+ monthly_income_b: float,
460
+ other_income: float,
461
+ cash_reserve: float,
462
+ debt_total: float,
463
+ debt_monthly_payment: float,
464
+ expenses_raw: str,
465
+ goals_raw: str,
466
+ ai_budget: float,
467
+ bpm_projected_income: float,
468
+ expected_one_time_income: float,
469
+ horizon_months: int,
470
+ emergency_target_months: int,
471
+ stress_income_drop_pct: float,
472
+ stress_expense_increase_pct: float,
473
+ notes: str,
474
+ state: AccessState,
475
+ ):
476
+ state = state or AccessState()
477
+ now = now_ts()
478
+
479
+ ok, msg = validate_code(access_code)
480
+ code = (access_code or "").strip()
481
+ state.audit.append((int(now), "validate", code, ok))
482
+
483
+ if not ok:
484
+ locked = f"""
485
+ 🔒 FFI Licensed Access Required
486
+
487
+ Reason: {msg}
488
+
489
+ To request a licensed Access Code, contact: {BRAND_CONTACT}
490
+ """
491
+ return textwrap.dedent(locked).strip(), "{}", state
492
+
493
+ if code not in state.usage:
494
+ state.usage[code] = UsageEntry(ts=[])
495
+
496
+ state.usage[code].ts = clean_old(state.usage[code].ts, now)
497
+ used = len(state.usage[code].ts)
498
+
499
+ if used >= DAILY_QUOTA:
500
+ remaining_time = int((state.usage[code].ts[0] + WINDOW_SECONDS) - now)
501
+ hours = max(0, remaining_time // 3600)
502
+ minutes = max(0, (remaining_time % 3600) // 60)
503
+ quota_msg = f"""
504
+ ⛔ Daily quota reached for this Access Code.
505
+
506
+ Quota: {DAILY_QUOTA} requests / 24h
507
+ Next reset in: ~{hours}h {minutes}m
508
+
509
+ If you need expanded access, contact: {BRAND_CONTACT}
510
+ """
511
+ state.audit.append((int(now), "quota_block", code, used))
512
+ return textwrap.dedent(quota_msg).strip(), "{}", state
513
+
514
+ minimum_context = f"{household_name} {partner_a} {partner_b} {expenses_raw} {goals_raw} {notes}".strip()
515
+ if len(minimum_context) < MIN_CHARS:
516
+ return (
517
+ f"Input rejected. Add more household context, expense rows, goals, or notes. Minimum context length: {MIN_CHARS} characters.",
518
+ "{}",
519
+ state,
520
+ )
521
+
522
+ state.usage[code].ts.append(now)
523
+ state.audit.append((int(now), "quota_consume", code, used + 1))
524
+
525
+ snapshot = build_financial_snapshot(
526
+ household_name,
527
+ partner_a,
528
+ partner_b,
529
+ monthly_income_a,
530
+ monthly_income_b,
531
+ other_income,
532
+ cash_reserve,
533
+ debt_total,
534
+ debt_monthly_payment,
535
+ expenses_raw,
536
+ goals_raw,
537
+ ai_budget,
538
+ bpm_projected_income,
539
+ expected_one_time_income,
540
+ horizon_months,
541
+ emergency_target_months,
542
+ stress_income_drop_pct,
543
+ stress_expense_increase_pct,
544
+ notes,
545
+ )
546
+
547
+ header = f"✅ Access Code: {code} | Usage: {len(state.usage[code].ts)}/{DAILY_QUOTA} rolling 24h\n\n"
548
+ report = header + build_human_report(snapshot)
549
+ return report, json.dumps(snapshot, ensure_ascii=False, indent=2), state
550
+
551
+
552
+ EXAMPLE_EXPENSES = """Housing,900,Joint,fixed,Loan or rent
553
+ Utilities,250,Joint,fixed,Electricity water internet
554
+ Food,650,Joint,variable,Groceries and household
555
+ Transport,300,Joint,variable,Fuel parking maintenance
556
+ Health,250,Joint,variable,Medical pharmacy checkups
557
+ Wedding/Family,400,Joint,project,Family and marriage costs
558
+ AI Tools,250,Edin,project,Hugging Face OpenAI cloud domains
559
+ BPM Studio,350,Edin,project,Space equipment hosting materials
560
+ Personal Elvira,250,Elvira,personal,Personal spending
561
+ Personal Edin,250,Edin,personal,Personal spending
562
+ Reserve Saving,500,Joint,saving,Emergency reserve
563
+ """
564
+
565
+ EXAMPLE_GOALS = """Emergency Reserve,12000,3000,12,Critical
566
+ AI Factory Fund,80000,0,60,High
567
+ Family Travel,4000,500,10,Medium
568
+ BPM Growth Fund,10000,1000,18,High
569
+ """
570
+
571
+
572
+ with gr.Blocks(theme=gr.themes.Soft()) as demo:
573
+ gr.Markdown(
574
+ f"""
575
+ # 🧠 {TITLE}
576
+ **{SUBTITLE}**
577
+
578
+ ⚠️ Research preview only.
579
+ FFI supports household planning, budgeting, scenario framing, and family decision preparation.
580
+ It does **not** provide financial advice, investment advice, tax advice, legal advice, trading signals, or execution.
581
+ """
582
+ )
583
+
584
+ state = gr.State(AccessState())
585
+
586
+ with gr.Row():
587
+ access_code = gr.Textbox(
588
+ label="FFI Access Code",
589
+ placeholder="e.g., FFI-EDIN-001",
590
+ type="password",
591
+ )
592
+
593
+ gr.Markdown("## 1. Household Profile")
594
+
595
+ with gr.Row():
596
+ household_name = gr.Textbox(label="Household Name", value="Edin & Elvira Household")
597
+ partner_a = gr.Textbox(label="Partner A", value="Edin")
598
+ partner_b = gr.Textbox(label="Partner B", value="Elvira")
599
+
600
+ gr.Markdown("## 2. Monthly Income")
601
+
602
+ with gr.Row():
603
+ monthly_income_a = gr.Number(label="Partner A Monthly Income", value=0)
604
+ monthly_income_b = gr.Number(label="Partner B Monthly Income", value=0)
605
+ other_income = gr.Number(label="Other Monthly Income", value=0)
606
+
607
+ with gr.Row():
608
+ bpm_projected_income = gr.Number(label="Projected BPM / Project Monthly Income", value=0)
609
+ expected_one_time_income = gr.Number(label="Expected One-Time Income", value=0)
610
+
611
+ gr.Markdown("## 3. Assets, Debt, Expenses")
612
+
613
+ with gr.Row():
614
+ cash_reserve = gr.Number(label="Current Cash Reserve", value=0)
615
+ debt_total = gr.Number(label="Total Debt", value=0)
616
+ debt_monthly_payment = gr.Number(label="Monthly Debt Payment", value=0)
617
+
618
+ with gr.Row():
619
+ ai_budget = gr.Number(label="Monthly AI / Cloud / Tools Budget", value=0)
620
+ emergency_target_months = gr.Slider(1, 12, value=6, step=1, label="Emergency Reserve Target Months")
621
+
622
+ expenses_raw = gr.Textbox(
623
+ label="Monthly Expenses CSV",
624
+ value=EXAMPLE_EXPENSES,
625
+ lines=10,
626
+ info="Format: category, amount, owner, type, note",
627
+ )
628
+
629
+ goals_raw = gr.Textbox(
630
+ label="Family Goals CSV",
631
+ value=EXAMPLE_GOALS,
632
+ lines=6,
633
+ info="Format: goal, target_amount, current_amount, deadline_months, priority",
634
+ )
635
+
636
+ gr.Markdown("## 4. Scenario Stress Test")
637
+
638
+ with gr.Row():
639
+ horizon_months = gr.Slider(3, 120, value=24, step=1, label="Planning Horizon Months")
640
+ stress_income_drop_pct = gr.Slider(0, 80, value=25, step=1, label="Stress: Income Drop %")
641
+ stress_expense_increase_pct = gr.Slider(0, 80, value=20, step=1, label="Stress: Expense Increase %")
642
+
643
+ notes = gr.Textbox(
644
+ label="Household Notes / Strategic Context",
645
+ lines=5,
646
+ placeholder=(
647
+ "Example: We want to jointly manage marriage finances, keep a reserve, fund BPM growth, "
648
+ "control AI/cloud costs, and prepare for larger strategic projects."
649
+ ),
650
+ )
651
+
652
+ btn = gr.Button("Generate Family Financial Intelligence Report")
653
+
654
+ gr.Markdown("## 5. Output")
655
+
656
+ report_output = gr.Textbox(
657
+ label="FFI Report",
658
+ lines=24,
659
+ )
660
+
661
+ json_output = gr.Code(
662
+ label="Structured JSON Snapshot",
663
+ language="json",
664
+ )
665
+
666
+ btn.click(
667
+ fn=gated_generate_report,
668
+ inputs=[
669
+ access_code,
670
+ household_name,
671
+ partner_a,
672
+ partner_b,
673
+ monthly_income_a,
674
+ monthly_income_b,
675
+ other_income,
676
+ cash_reserve,
677
+ debt_total,
678
+ debt_monthly_payment,
679
+ expenses_raw,
680
+ goals_raw,
681
+ ai_budget,
682
+ bpm_projected_income,
683
+ expected_one_time_income,
684
+ horizon_months,
685
+ emergency_target_months,
686
+ stress_income_drop_pct,
687
+ stress_expense_increase_pct,
688
+ notes,
689
+ state,
690
+ ],
691
+ outputs=[report_output, json_output, state],
692
+ )
693
+
694
+ gr.Markdown(
695
+ f"""
696
+ ---
697
+ ### Recommended Space Secrets
698
+
699
+ ```txt
700
+ FFI_ACCESS_CODES=FFI-EDIN-001,FFI-CLIENT-002
701
+ FFI_DAILY_QUOTA=10
702
+ FFI_BRAND_CONTACT={BRAND_CONTACT}
703
+ FFI_MIN_CHARS=30
704
+ ```
705
+
706
+ ### Governance Boundary
707
+
708
+ FFI is a household planning and scenario-framing system.
709
+ It does not provide financial, investment, legal, tax, trading, or execution advice.
710
+ All decisions remain fully human-owned.
711
+
712
+ © 2026 BPM RED Academy · HumAI MightHub
713
+ """
714
+ )
715
+
716
+
717
+ if __name__ == "__main__":
718
+ demo.launch()