File size: 7,632 Bytes
9392c26
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6158ab4
 
 
 
 
 
 
 
 
 
 
 
 
 
9392c26
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6158ab4
9392c26
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""FinIR-Intent -- minimal Hugging Face Space.

Flow: natural-language instruction -> FinIR-Intent baseline -> canonical envelope
(validated against the core `finir` package's JSON Schema) -> real FinIR runtime
execution -> result. This file contains no financial computation and no execution
logic of its own -- it only calls `finir.intent` / `finir.FinancialModel`.

    python space/app.py
"""

from __future__ import annotations

import json
import os
import sys
from pathlib import Path

os.environ.setdefault("GRADIO_ANALYTICS_ENABLED", "False")
os.environ.setdefault("HF_HUB_OFFLINE", "1")

import gradio as gr
from finir import __version__ as FINIR_RUNTIME_VERSION
from finir.intent import (
    SCHEMA_VERSION,
    FinIRIntent,
    IntentValidationError,
    execute_intent,
    json_schema,
)

try:
    # Normal case once `finir-intent` is pip-installed (e.g. from requirements.txt
    # in a real Space, or `pip install -e .` locally).
    from finir_intent import build_reference_model, compile_intent
    from finir_intent._version import FINIR_RUNTIME_COMPATIBLE
    from finir_intent._version import __version__ as FINIR_INTENT_VERSION
except ImportError:
    # Fallback for running straight out of this repo layout before the package is
    # published -- see README.md "Packaging note". Only touches sys.path when the
    # normal import actually fails, so an installed package is never shadowed.
    sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "src"))
    from finir_intent import build_reference_model, compile_intent
    from finir_intent._version import FINIR_RUNTIME_COMPATIBLE
    from finir_intent._version import __version__ as FINIR_INTENT_VERSION

# Hugging Face ZeroGPU (this Space's free hardware tier) requires at least one
# `@spaces.GPU`-decorated function to be registered at startup. This demo's compute is
# CPU-only (finir's default NumPy backend) and `run()` takes/returns plain strings, so
# wrapping it is safe and cheap. The `spaces` package only exists on ZeroGPU Spaces, so
# the import is guarded; locally (and on CPU hardware) `_gpu` is a transparent no-op and
# no GPU is ever requested.
try:
    from spaces import GPU as _gpu  # type: ignore[import-not-found]
except Exception:  # not on a ZeroGPU Space (local dev / CI / CPU hardware)

    def _gpu(fn=None, **_):
        return fn if callable(fn) else (lambda f: f)


_SCHEMA_VALIDATOR = None  # lazily built (jsonschema is optional at Space runtime)


def _schema_errors(envelope: dict) -> list[str]:
    global _SCHEMA_VALIDATOR
    try:
        import jsonschema
    except ImportError:
        return []
    if _SCHEMA_VALIDATOR is None:
        _SCHEMA_VALIDATOR = jsonschema.Draft202012Validator(json_schema())
    return [e.message for e in _SCHEMA_VALIDATOR.iter_errors(envelope)]


_MODEL = build_reference_model()
_MODEL.evaluate()  # warm the runtime's incremental cache; result is not used here

_VERSION_BANNER = (
    f"**FinIR-Intent** `{FINIR_INTENT_VERSION}`  |  "
    f"**Intent schema** `{SCHEMA_VERSION}`  |  "
    f"**Compatible FinIR runtime** `{FINIR_RUNTIME_COMPATIBLE}`  |  "
    f"**Installed finir** `{FINIR_RUNTIME_VERSION}`"
)

_REFERENCE_MODEL_NOTE = """
Reference model (fixture data, not a real company): revenue, cogs, opex,
payment_terms, accounts_payable, inventory, capex, debt, interest_rate, cash, price,
volume -- all ZAR except where noted. See `src/finir_intent/reference_model.py`.
"""


@_gpu
def run(text: str) -> tuple[str, str, str]:
    if not text or not text.strip():
        return "", "", "Enter a natural-language financial instruction above."

    envelope = compile_intent(text)  # AI interpretation layer -- no financial math here
    envelope_json = json.dumps(envelope, indent=2)

    errors = _schema_errors(envelope)
    if errors:
        # Should never happen for this baseline; surfaced rather than hidden.
        return envelope_json, "", "Schema validation failed:\n" + "\n".join(errors)

    status = envelope["status"]
    if status != "valid":
        reason = envelope.get("reason", "(no reason given)")
        label = {"ambiguous": "Ambiguous", "unsupported": "Unsupported"}.get(status, status)
        return envelope_json, "", f"**{label}** -- not executed.\n\n{reason}"

    try:
        FinIRIntent.from_obj(envelope)  # structural validation (core package)
        execution = execute_intent(_MODEL, envelope)  # real FinIR runtime execution
    except IntentValidationError as exc:
        return envelope_json, "", f"Rejected at execution (semantic validation failed):\n\n{exc}"

    if execution.scenario_results is not None:
        lines = ["| scenario | ebitda | gross_margin |", "|---|---|---|"]
        for name, result in execution.scenario_results.items():
            lines.append(
                f"| {name} | R{float(result['ebitda']):,.0f} | {float(result['gross_margin']):.3f} |"
            )
        result_md = "\n".join(lines)
    else:
        r = execution.result
        is_batch = any(op.get("operation") == "range" for op in envelope.get("operations", []))
        if is_batch:
            ebitda = r["ebitda"]
            lines = [
                f"- **ebitda range**: R{float(ebitda.min()):,.0f} .. R{float(ebitda.max()):,.0f}",
                f"- **batch size**: {ebitda.size:,}",
            ]
        else:
            lines = [
                f"- **ebitda**: R{float(r['ebitda']):,.0f}",
                f"- **gross_margin**: {float(r['gross_margin']):.3f}",
            ]
            if "interest_expense" in r:
                lines.append(f"- **interest_expense**: R{float(r['interest_expense']):,.0f}")
            if "net_cash_position" in r:
                lines.append(f"- **net_cash_position**: R{float(r['net_cash_position']):,.0f}")
        lines.append(f"- recomputed: `{sorted(r.recomputed)}`")
        lines.append(f"- reused: `{sorted(r.reused)}`")
        result_md = "\n".join(lines)

    return envelope_json, result_md, "Executed against the real FinIR runtime."


_EXAMPLES = [
    "Increase COGS by 4%.",
    "Increase opex by R5,000,000.",
    "Extend payment terms to 60 days.",
    "Revenue falls 8%, COGS rises 3%, and extend payment terms to 60 days.",
    "Improve margins next year.",
    "Acquire our largest competitor.",
    "Increase revenue by USD 2,000,000.",
    "Sweep COGS from 300,000,000 to 400,000,000 in 100 steps.",
    "Base scenario: no changes. Upside scenario: revenue grows 10%. Downside scenario: revenue falls 8% and COGS rises 5%.",
]

with gr.Blocks(title="FinIR-Intent", analytics_enabled=False) as demo:
    gr.Markdown("# FinIR-Intent")
    gr.Markdown(_VERSION_BANNER)
    gr.Markdown(
        "Natural language -> structured **FinIR Intent Contract** envelope -> validated "
        "-> executed by the real FinIR runtime. Ambiguous or unsupported requests are "
        "refused rather than guessed."
    )
    with gr.Row():
        inp = gr.Textbox(
            label="Financial instruction", placeholder="e.g. Increase COGS by 4%.", lines=2
        )
    run_btn = gr.Button("Compile & execute", variant="primary")
    gr.Examples(examples=_EXAMPLES, inputs=inp)
    with gr.Row():
        envelope_out = gr.Code(label="FinIR Intent envelope (schema v1.0)", language="json")
        result_out = gr.Markdown(label="Execution result")
    status_out = gr.Markdown()
    gr.Markdown(_REFERENCE_MODEL_NOTE)

    run_btn.click(run, inputs=inp, outputs=[envelope_out, result_out, status_out])
    inp.submit(run, inputs=inp, outputs=[envelope_out, result_out, status_out])

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