ninarg commited on
Commit
2d6c179
Β·
verified Β·
1 Parent(s): b3a8cd7

Add app.py

Browse files
Files changed (1) hide show
  1. app.py +159 -0
app.py ADDED
@@ -0,0 +1,159 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ VynFi Streamlit Template β€” Generate, Explore, Visualize
3
+
4
+ A ready-to-deploy Streamlit app that connects to the VynFi API,
5
+ generates synthetic financial data, and renders interactive dashboards.
6
+ Clone and customize for your use case.
7
+
8
+ Usage:
9
+ export VYNFI_API_KEY=vf_live_...
10
+ streamlit run app.py
11
+ """
12
+
13
+ import os
14
+ import streamlit as st
15
+ import pandas as pd
16
+ import vynfi
17
+
18
+ st.set_page_config(page_title="VynFi Explorer", page_icon="πŸ“Š", layout="wide")
19
+
20
+ st.title("πŸ“Š VynFi Data Explorer")
21
+
22
+ api_key = os.environ.get("VYNFI_API_KEY", "")
23
+ if not api_key:
24
+ api_key = st.sidebar.text_input("VynFi API Key", type="password", placeholder="vf_live_...")
25
+
26
+ if not api_key:
27
+ st.info("Enter your VynFi API key in the sidebar to get started. [Get a free key β†’](https://vynfi.com/signup)")
28
+ st.stop()
29
+
30
+ client = vynfi.VynFi(api_key=api_key)
31
+
32
+ # ── Sidebar: generation config ──────────────────────────────────────────────
33
+
34
+ st.sidebar.header("Generate")
35
+
36
+ sector = st.sidebar.selectbox(
37
+ "Sector",
38
+ ["retail", "manufacturing", "financial_services", "banking_aml", "healthcare", "technology", "energy"],
39
+ index=1,
40
+ )
41
+ rows = st.sidebar.slider("Rows", min_value=100, max_value=100_000, value=1000, step=100)
42
+ companies = st.sidebar.slider("Companies", min_value=1, max_value=20, value=3)
43
+ fraud_rate = st.sidebar.slider("Fraud rate", min_value=0.0, max_value=0.20, value=0.03, step=0.01)
44
+
45
+ # NL config option
46
+ st.sidebar.divider()
47
+ nl_description = st.sidebar.text_area(
48
+ "Or describe what you want (Scale+)",
49
+ placeholder="e.g. 6 months of P2P for a German manufacturer with IFRS",
50
+ height=80,
51
+ )
52
+
53
+ generate = st.sidebar.button("Generate", type="primary", use_container_width=True)
54
+
55
+ # ── Main area ────────────────────────────────────────────────────────────────
56
+
57
+ if generate:
58
+ with st.spinner("Generating..."):
59
+ try:
60
+ if nl_description.strip():
61
+ resp = client._request("POST", "/v1/configs/from-description", json={
62
+ "description": nl_description.strip()
63
+ })
64
+ config = resp.get("config", {})
65
+ st.sidebar.success(f"AI config: {config.get('sector')} / {config.get('rows')} rows")
66
+ else:
67
+ config = {
68
+ "sector": sector,
69
+ "rows": rows,
70
+ "companies": companies,
71
+ "fraudRate": fraud_rate,
72
+ "complexity": "medium",
73
+ "exportFormat": "json",
74
+ "output": {"numericMode": "native"},
75
+ }
76
+
77
+ job = client.jobs.generate_config(config=config)
78
+ completed = client.jobs.wait(job.id, poll_interval=3.0, timeout=300.0)
79
+
80
+ if completed.status != "completed":
81
+ st.error(f"Job failed: {completed.error_detail}")
82
+ st.stop()
83
+
84
+ st.session_state["job_id"] = completed.id
85
+ st.session_state["archive"] = client.jobs.download_archive(completed.id)
86
+ st.success(f"Job {completed.id} completed")
87
+ except Exception as e:
88
+ st.error(f"Error: {e}")
89
+
90
+ # ── Display results ──────────────────────────────────────────────────────────
91
+
92
+ if "archive" in st.session_state:
93
+ archive = st.session_state["archive"]
94
+
95
+ tab1, tab2, tab3, tab4 = st.tabs(["Journal Entries", "Documents", "Quality", "Files"])
96
+
97
+ with tab1:
98
+ st.subheader("Journal Entries")
99
+ try:
100
+ entries = archive.json("journal_entries.json")
101
+ # Flatten
102
+ rows_flat = []
103
+ for entry in entries[:500]: # Cap for display
104
+ header = entry.get("header", entry)
105
+ lines = entry.get("lines", [entry])
106
+ for line in lines:
107
+ rows_flat.append({
108
+ "document_id": header.get("document_id", ""),
109
+ "company_code": header.get("company_code", ""),
110
+ "posting_date": header.get("posting_date", ""),
111
+ "document_type": header.get("document_type", ""),
112
+ "is_fraud": header.get("is_fraud", False),
113
+ "gl_account": line.get("gl_account", ""),
114
+ "debit_amount": line.get("debit_amount", 0),
115
+ "credit_amount": line.get("credit_amount", 0),
116
+ })
117
+ df = pd.DataFrame(rows_flat)
118
+ st.metric("Line items", f"{len(df):,}")
119
+
120
+ col1, col2 = st.columns(2)
121
+ with col1:
122
+ st.metric("Total debits", f"${df['debit_amount'].sum():,.2f}")
123
+ with col2:
124
+ fraud_count = df["is_fraud"].sum()
125
+ st.metric("Fraud entries", f"{fraud_count} ({fraud_count/len(df)*100:.1f}%)")
126
+
127
+ st.dataframe(df, use_container_width=True, hide_index=True)
128
+ except Exception as e:
129
+ st.warning(f"Could not load journal entries: {e}")
130
+
131
+ with tab2:
132
+ st.subheader("Document Flows")
133
+ for doc_type in ["purchase_orders", "goods_receipts", "vendor_invoices", "payments"]:
134
+ try:
135
+ docs = archive.json(f"document_flows/{doc_type}.json")
136
+ st.write(f"**{doc_type.replace('_', ' ').title()}**: {len(docs)} records")
137
+ except Exception:
138
+ pass
139
+
140
+ with tab3:
141
+ st.subheader("Quality Metrics")
142
+ try:
143
+ analytics = client.jobs.analytics(st.session_state["job_id"])
144
+ if hasattr(analytics, "benford_analysis") and analytics.benford_analysis:
145
+ b = analytics.benford_analysis
146
+ col1, col2, col3 = st.columns(3)
147
+ col1.metric("Benford MAD", f"{b.mad:.4f}")
148
+ col2.metric("Chi-squared", f"{b.chi_squared:.2f}")
149
+ col3.metric("Conforms", "βœ…" if b.passes else "❌")
150
+ except Exception:
151
+ st.info("Quality analytics not available for this job.")
152
+
153
+ with tab4:
154
+ st.subheader("Archive Files")
155
+ for f in archive.files():
156
+ st.text(f)
157
+
158
+ else:
159
+ st.info("Click **Generate** in the sidebar to create a dataset.")