File size: 8,205 Bytes
e2aa97d
 
f063f4d
a53d475
e2aa97d
 
a53d475
e2aa97d
 
 
8983830
e2aa97d
 
 
 
 
 
a53d475
e2aa97d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8983830
e2aa97d
8983830
 
e2aa97d
8983830
 
e2aa97d
 
 
 
 
 
8983830
 
a53d475
 
e2aa97d
8983830
 
e2aa97d
8983830
e2aa97d
 
 
 
 
 
8983830
 
e2aa97d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8983830
f063f4d
a53d475
f063f4d
 
e2aa97d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f063f4d
a53d475
 
 
8983830
 
c5cdb34
e2aa97d
 
8983830
a53d475
f063f4d
e2aa97d
 
 
 
 
 
 
 
 
 
a53d475
 
 
 
e2aa97d
 
a53d475
8983830
a53d475
 
e2aa97d
a53d475
 
 
e2aa97d
a53d475
f063f4d
e2aa97d
 
 
 
a53d475
f063f4d
a53d475
e2aa97d
8983830
e2aa97d
f063f4d
e2aa97d
8983830
f063f4d
e2aa97d
f063f4d
8983830
 
 
 
 
 
 
 
a53d475
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
import os
import time
import gradio as gr
import textwrap
from dataclasses import dataclass, field
from typing import Dict, Tuple

# ============================================================
# PFI Elite Access Control (per-user access codes)
# ============================================================

# Set in Hugging Face Space -> Settings -> Variables and secrets
# Example:
#   PFI_ACCESS_CODES = "PFI-EDIN-001,PFI-CLIENT-002,PFI-CLIENT-003"
#   PFI_DAILY_QUOTA = "3"
#   PFI_BRAND_CONTACT = "pfi@bpmred.academy"
#   PFI_MIN_CHARS = "40"

ACCESS_CODES_RAW = os.getenv("PFI_ACCESS_CODES", "").strip()
DAILY_QUOTA = int(os.getenv("PFI_DAILY_QUOTA", "3").strip() or "3")
BRAND_CONTACT = os.getenv("PFI_BRAND_CONTACT", "pfi@bpmred.academy").strip()
MIN_CHARS = int(os.getenv("PFI_MIN_CHARS", "40").strip() or "40")

# Normalized set of valid codes
VALID_CODES = {c.strip() for c in ACCESS_CODES_RAW.split(",") if c.strip()}

# 24h window (seconds)
WINDOW_SECONDS = 24 * 60 * 60


@dataclass
class UsageEntry:
    # timestamps of successful requests
    ts: list = field(default_factory=list)


@dataclass
class AccessState:
    # code -> usage
    usage: Dict[str, UsageEntry] = field(default_factory=dict)
    # tiny audit log (session-local)
    audit: list = field(default_factory=list)


def _now() -> float:
    return time.time()


def _clean_old(ts_list: list, now: float) -> list:
    """Keep only timestamps inside rolling 24h window."""
    cutoff = now - WINDOW_SECONDS
    return [t for t in ts_list if t >= cutoff]


def validate_code(code: str) -> Tuple[bool, str]:
    code = (code or "").strip()
    if not code:
        return False, "Access Code is required."
    if not VALID_CODES:
        # If admin forgot to set PFI_ACCESS_CODES, fail closed (safest)
        return False, "PFI is not configured for access codes yet. Please contact support."
    if code not in VALID_CODES:
        return False, "Invalid Access Code."
    return True, "Access granted."


# ============================================================
# Core PFI reasoning stub (preview-only, non-executive)
# Replace this later with your real model/API call.
# ============================================================

def pfi_reasoning_preview(question: str) -> str:
    q = (question or "").strip()
    if len(q) < MIN_CHARS:
        return textwrap.dedent(
            f"""
            [Input rejected]

            PFI requires a precise, high-density financial question (min {MIN_CHARS} characters).
            Ambiguous or underspecified inputs reduce analytical value.

            Use this template:
            - Objective:
            - Horizon:
            - Constraints (max drawdown / liquidity / taxes / jurisdiction):
            - Current exposures (concentration risk):
            - What "irreversible downside" means to you:
            """
        ).strip()

    response = f"""
    PFI STRUCTURAL ANALYSIS (PREVIEW · NON-EXECUTABLE)

    Question (received):
    - {q}

    Structural Map:
    1) Decision domain: capital allocation / risk architecture
    2) Time structure: near-term liquidity vs long-horizon convexity
    3) Constraint set: drawdown tolerance, cash-flow needs, optionality preservation
    4) Failure modes: forced selling, duration mismatch, correlation spikes, policy shock
    5) Control levers: rebalancing rules, hedges, reserve sizing, exposure caps

    Cognitive Decomposition:
    - Primary variables:
      • income stability / career risk
      • liquidity needs and timing
      • inflation/regime risk
      • concentration risk (single asset / geography / employer)
      • tax / jurisdiction constraints
    - Secondary dependencies:
      • correlation behavior under stress
      • funding liquidity vs market liquidity
      • refinancing risk / rate sensitivity
    - Irreversible downside candidates:
      • ruin risk (capital impairment that changes future opportunity set)
      • forced liquidation triggers
      • leverage or short-vol exposure under volatility expansion

    Next Questions (to deepen analysis):
    - Define max acceptable drawdown and time-to-recover.
    - Specify liquidity schedule (must-pay obligations by month/quarter).
    - List top 3 concentrated exposures and whether they can be reduced.
    - Clarify jurisdiction + tax constraints (capital gains, withholding).
    """

    return textwrap.dedent(response).strip()


# ============================================================
# Gated handler (enforces per-code quota)
# ============================================================

def gated_request(access_code: str, question: str, state: AccessState) -> Tuple[str, AccessState]:
    state = state or AccessState()
    now = _now()

    ok, msg = validate_code(access_code)
    code = (access_code or "").strip()

    # audit
    state.audit.append((int(now), "validate", code, ok))

    if not ok:
        locked = f"""
🔒 **PFI Licensed Access Required**

Reason: **{msg}**

To request a licensed Access Code, contact: **{BRAND_CONTACT}**
"""
        return textwrap.dedent(locked).strip(), state

    # init usage
    if code not in state.usage:
        state.usage[code] = UsageEntry(ts=[])

    # rolling window cleanup
    state.usage[code].ts = _clean_old(state.usage[code].ts, now)
    used = len(state.usage[code].ts)

    if used >= DAILY_QUOTA:
        remaining_time = int((state.usage[code].ts[0] + WINDOW_SECONDS) - now)
        hours = max(0, remaining_time // 3600)
        minutes = max(0, (remaining_time % 3600) // 60)

        quota_msg = f"""
⛔ **Daily quota reached** for this Access Code.

- Quota: **{DAILY_QUOTA} requests / 24h**
- Next reset in: **~{hours}h {minutes}m**

If you need expanded access, contact: **{BRAND_CONTACT}**
"""
        state.audit.append((int(now), "quota_block", code, used))
        return textwrap.dedent(quota_msg).strip(), state

    # Consume 1 quota
    state.usage[code].ts.append(now)
    state.audit.append((int(now), "quota_consume", code, used + 1))

    # Run preview reasoning
    out = pfi_reasoning_preview(question)

    # Add header with usage
    used_after = len(state.usage[code].ts)
    header = f"✅ Access Code: {code}  |  Usage: {used_after}/{DAILY_QUOTA} (rolling 24h)\n\n"
    return header + out, state


# ============================================================
# UI
# ============================================================

with gr.Blocks(theme=gr.themes.Soft()) as demo:
    gr.Markdown(
        """
# 🧠 Personal Financial Intelligence (PFI)
**High-density financial reasoning · Research Preview**

⚠️ *PFI is a licensed research interface.  
It does NOT provide financial advice, trading signals, or execute decisions.*
"""
    )

    # Session state (in-memory per user session)
    state = gr.State(AccessState())

    with gr.Row():
        access_code = gr.Textbox(
            label="PFI Access Code (Licensed)",
            placeholder="e.g., PFI-CLIENT-002",
            type="password",
        )

    with gr.Row():
        question_input = gr.Textbox(
            label="Your Question",
            placeholder=(
                "Write ONE precise, high-impact financial question.\n"
                "Include horizon, constraints, and context. Ambiguity reduces output quality."
            ),
            lines=4,
        )

    btn = gr.Button("Request Structural Analysis (Preview)")

    output_box = gr.Textbox(
        label="PFI Output (Exploratory · Non-Executable)",
        lines=10,
    )

    btn.click(
        fn=gated_request,
        inputs=[access_code, question_input, state],
        outputs=[output_box, state],
    )

    gr.Markdown(
        f"""
---
🔒 **Deeper Structural Reasoning — Licensed Layer**

Personalized constraints, scenario stress tests, and capital structure reasoning  
are available only under **PFI Licensed Access**.

👉 Request access: **{BRAND_CONTACT}**

---
### Disclaimer
PFI outputs are exploratory and informational only.  
No financial advice, trading signals, or decision execution is provided.

© 2026 BPM RED Academy · All rights reserved.
"""
    )

if __name__ == "__main__":
    demo.launch()