dineshb commited on
Commit
489825c
·
verified ·
1 Parent(s): f26fd6b

Run Gemini outside Streamlit session thread

Browse files

Use a bounded cached worker and responsive polling so hosted WebSocket sessions remain alive while Gemini processes evidence.

Files changed (1) hide show
  1. app.py +486 -459
app.py CHANGED
@@ -1,461 +1,488 @@
1
- from __future__ import annotations
2
-
3
- import io
4
- import os
5
- from pathlib import Path
6
-
7
- import pandas as pd
8
- import plotly.express as px
9
- import streamlit as st
10
-
11
- from datapilot.analyst import dataframe_csv, gemini_dataset_summary, inspect_dataset
12
- from datapilot.config import get_settings
13
- from datapilot.data import SAMPLE_DATASETS, load_sample
14
- from datapilot.workflow import run_analysis
15
-
16
- st.set_page_config(
17
- page_title="DataPilot · Autonomous Data Analyst",
18
- page_icon="✦",
19
- layout="wide",
20
- initial_sidebar_state="expanded",
21
- )
22
-
23
- st.markdown(
24
- """
25
- <style>
26
- @import url('https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;600;700&family=Manrope:wght@600;700;800&display=swap');
27
- :root{--navy:#07111f;--panel:#0e1b2c;--line:#203149;--cyan:#49d7c5;--blue:#6d8dff;--text:#edf4ff;--muted:#91a1b7}
28
- .stApp{background:radial-gradient(circle at 75% -10%,#17355c 0,transparent 35%),#07111f;color:var(--text)}
29
- html,body,[class*="css"]{font-family:"DM Sans",sans-serif}
30
- h1,h2,h3{font-family:"Manrope",sans-serif;letter-spacing:-.03em}
31
- header[data-testid="stHeader"]{background:transparent}
32
- div[data-testid="stSidebar"]{background:#091522;border-right:1px solid var(--line)}
33
- .block-container{max-width:1480px;padding-top:1.1rem;padding-bottom:4rem}
34
- .brand{display:flex;gap:.75rem;align-items:center;font:800 1.2rem Manrope;color:white;margin:.2rem 0 1.3rem}
35
- .brand-mark{display:grid;place-items:center;width:34px;height:34px;border-radius:10px;background:linear-gradient(135deg,var(--cyan),var(--blue));color:#07111f}
36
- .hero{border:1px solid #29405d;background:linear-gradient(125deg,rgba(17,35,57,.96),rgba(9,22,38,.88));border-radius:24px;padding:2rem 2.2rem;margin-bottom:1rem;overflow:hidden;position:relative}
37
- .hero:after{content:"";position:absolute;width:340px;height:340px;border-radius:50%;right:-100px;top:-190px;background:rgba(73,215,197,.10)}
38
- .eyebrow{color:var(--cyan);font-size:.73rem;font-weight:700;letter-spacing:.18em;text-transform:uppercase}
39
- .hero h1{font-size:clamp(2.1rem,4vw,4rem);line-height:1.02;margin:.45rem 0 .7rem;color:white}
40
- .hero p{max-width:790px;color:#aebdd0;font-size:1.02rem;line-height:1.65;margin:0}
41
- .stepbar{display:flex;gap:.5rem;flex-wrap:wrap;margin-top:1.4rem}.step{border:1px solid #2d4664;border-radius:999px;padding:.4rem .72rem;color:#9eafc4;font-size:.75rem}.step.on{color:#07111f;background:var(--cyan);border-color:var(--cyan);font-weight:700}
42
- .panel{background:rgba(14,27,44,.92);border:1px solid var(--line);border-radius:18px;padding:1.15rem 1.25rem;height:100%}
43
- .kicker{color:var(--cyan);font-size:.72rem;font-weight:700;text-transform:uppercase;letter-spacing:.12em}.muted{color:var(--muted);font-size:.87rem;line-height:1.55}
44
- .signature{background:linear-gradient(145deg,#11263b,#0b1828);border:1px solid #29435f;border-radius:17px;padding:1rem;margin-top:1rem}.signature strong{color:white}.signature a{color:var(--cyan);text-decoration:none;font-size:.83rem}
45
- div[data-testid="stMetric"]{background:#0d1b2c;border:1px solid var(--line);padding:15px 17px;border-radius:15px}div[data-testid="stMetric"] label{color:#91a1b7}div[data-testid="stMetricValue"]{color:white}
46
- .stButton>button,.stDownloadButton>button{border:0;border-radius:11px;background:linear-gradient(135deg,#49d7c5,#6d8dff);color:#07111f;font-weight:800}
47
- .stButton>button:hover,.stDownloadButton>button:hover{color:#07111f;filter:brightness(1.08)}
48
- div[data-testid="stFileUploaderDropzone"]{background:#0c1a2b;border:1.5px dashed #3b617c;border-radius:16px;padding:1.3rem}
49
- div[data-baseweb="tab-list"]{gap:.3rem;background:#0b1828;border:1px solid var(--line);border-radius:13px;padding:.3rem}
50
- button[data-baseweb="tab"]{border-radius:9px;color:#9caec3}button[data-baseweb="tab"][aria-selected="true"]{background:#172b41;color:white}
51
- .stDataFrame{border:1px solid var(--line);border-radius:13px;overflow:hidden}
52
- [data-testid="stAlert"]{border-radius:13px}
53
- </style>
54
- """,
55
- unsafe_allow_html=True,
56
- )
57
-
58
- settings = get_settings()
59
- for key, default in {
60
- "frame": None,
61
- "dataset_name": "",
62
- "profile": None,
63
- "result": None,
64
- "ai_summary": "",
65
- "chat": [],
66
- "target": None,
67
- }.items():
68
- if key not in st.session_state:
69
- st.session_state[key] = default
70
-
71
-
72
- def read_upload(uploaded) -> pd.DataFrame:
73
- suffix = Path(uploaded.name).suffix.lower()
74
- raw = uploaded.getvalue()
75
- if len(raw) > settings.max_upload_mb * 1_048_576:
76
- raise ValueError(f"File exceeds the {settings.max_upload_mb} MB limit.")
77
- stream = io.BytesIO(raw)
78
- if suffix in {".csv", ".tsv", ".txt"}:
79
- return pd.read_csv(stream, sep="\t" if suffix == ".tsv" else None, engine="python")
80
- if suffix in {".xlsx", ".xls"}:
81
- return pd.read_excel(stream)
82
- if suffix == ".parquet":
83
- return pd.read_parquet(stream)
84
- if suffix == ".json":
85
- try:
86
- return pd.read_json(stream)
87
- except ValueError:
88
- stream.seek(0)
89
- return pd.read_json(stream, lines=True)
90
- raise ValueError("Use CSV, TSV, Excel, JSON, or Parquet.")
91
-
92
-
93
- with st.sidebar:
94
- st.markdown(
95
- '<div class="brand"><span class="brand-mark">✦</span>DataPilot</div>',
96
- unsafe_allow_html=True,
97
- )
98
- st.caption("AUTONOMOUS ANALYSIS WORKSPACE")
99
- st.markdown("##### Gemini intelligence")
100
- server_api_key = os.getenv("GEMINI_API_KEY", os.getenv("GOOGLE_API_KEY", ""))
101
- user_api_key = st.text_input(
102
- "Personal Gemini API key (optional)",
103
- value="",
104
- type="password",
105
- help="Leave blank to use the secured server-side key. Never stored or logged.",
106
- )
107
- api_key = user_api_key.strip() or server_api_key
108
- model = st.selectbox("Model", ["gemini-2.5-flash", "gemini-2.5-pro", "gemini-2.0-flash"])
109
- st.caption(
110
- "● AI ready · secured server key"
111
- if server_api_key
112
- else ("● AI ready" if api_key else "○ Local analysis mode")
113
- )
114
- st.divider()
115
- st.markdown("##### Privacy controls")
116
- metadata_only = st.toggle(
117
- "Metadata-first AI",
118
- value=True,
119
- help="Send schema, aggregate statistics, and three redacted examples—not the full dataset.",
120
- )
121
- excluded = st.multiselect(
122
- "Exclude columns from AI",
123
- list(st.session_state.frame.columns) if st.session_state.frame is not None else [],
124
- )
125
- st.divider()
126
- if st.button("Reset workspace", width="stretch"):
127
- for key in ("frame", "profile", "result", "ai_summary", "chat", "target"):
128
- st.session_state[key] = (
129
- None
130
- if key in {"frame", "profile", "result", "target"}
131
- else ([] if key == "chat" else "")
132
- )
133
- st.rerun()
134
- st.markdown(
135
- """
136
- <div class="signature">
137
- <div class="kicker">Built & designed by</div>
138
- <strong>Dinesh Barri</strong><br>
139
- <span class="muted">AI Engineer · Data Scientist</span><br><br>
140
- <a href="https://github.com/dineshbarri">GitHub ↗</a>&nbsp;&nbsp;
141
- <a href="https://www.linkedin.com/in/dinesh-barri-7654b010b">LinkedIn ↗</a>
142
- </div>""",
143
- unsafe_allow_html=True,
144
- )
145
-
146
- loaded = st.session_state.frame is not None
147
- st.markdown(
148
- f"""
149
- <section class="hero">
150
- <div class="eyebrow">Evidence-first autonomous data science</div>
151
- <h1>Your data. Explained.<br>Decisions, accelerated.</h1>
152
- <p>Upload a dataset and DataPilot immediately inspects its structure, surfaces quality risks,
153
- recommends analytical targets, creates interactive evidence, and prepares a leakage-safe
154
- machine-learning study—with Gemini available for grounded interpretation.</p>
155
- <div class="stepbar">
156
- <span class="step {"on" if loaded else ""}">01 · Connect</span>
157
- <span class="step {"on" if loaded else ""}">02 · Inspect</span>
158
- <span class="step {"on" if st.session_state.ai_summary else ""}">03 · Interpret</span>
159
- <span class="step {"on" if st.session_state.result else ""}">04 · Model</span>
160
- <span class="step {"on" if st.session_state.result else ""}">05 · Deliver</span>
161
- </div>
162
- </section>""",
163
- unsafe_allow_html=True,
164
- )
165
-
166
- if not loaded:
167
- left, right = st.columns([1.35, 0.65], gap="large")
168
- with left:
169
- st.markdown('<div class="kicker">Start a new analysis</div>', unsafe_allow_html=True)
170
- st.subheader("Drop in your dataset")
171
- uploaded = st.file_uploader(
172
- "Upload dataset",
173
- type=["csv", "tsv", "txt", "xlsx", "xls", "json", "parquet"],
174
- label_visibility="collapsed",
175
- )
176
- st.caption("CSV · TSV · Excel · JSON · Parquet | Raw data remains in this session.")
177
- if uploaded:
178
- try:
179
- with st.status("DataPilot is inspecting your dataset…", expanded=True) as status:
180
- st.write("Validating file structure")
181
- frame = read_upload(uploaded)
182
- st.write("Profiling columns, missingness, cardinality, and target candidates")
183
- profile = inspect_dataset(frame)
184
- st.session_state.frame = frame
185
- st.session_state.profile = profile
186
- st.session_state.dataset_name = uploaded.name
187
- status.update(label="Dataset ready", state="complete")
188
- st.rerun()
189
- except Exception as exc:
190
- st.error(f"Upload could not be processed: {exc}")
191
- with right:
192
- st.markdown(
193
- '<div class="panel"><div class="kicker">Try it instantly</div><h3>Explore a trusted demo</h3><p class="muted">Load a complete classification or regression dataset and see the full analyst workflow.</p></div>',
194
- unsafe_allow_html=True,
195
- )
196
- demo = st.selectbox("Demo dataset", list(SAMPLE_DATASETS))
197
- if st.button("Load demo workspace", width="stretch"):
198
- frame, target, name = load_sample(SAMPLE_DATASETS[demo])
199
- st.session_state.frame, st.session_state.target = frame, target
200
- st.session_state.dataset_name = name
201
- st.session_state.profile = inspect_dataset(frame)
202
- st.rerun()
203
- st.stop()
204
-
205
- frame: pd.DataFrame = st.session_state.frame
206
- profile = st.session_state.profile or inspect_dataset(frame)
207
- brief = profile["brief"]
208
-
209
- metrics = st.columns(6)
210
- metrics[0].metric("Rows", f"{brief.rows:,}")
211
- metrics[1].metric("Columns", f"{brief.columns:,}")
212
- metrics[2].metric("Numeric", brief.numeric)
213
- metrics[3].metric("Categorical", brief.categorical)
214
- metrics[4].metric("Missing cells", f"{brief.missing_cells:,}")
215
- metrics[5].metric("Quality score", f"{profile['quality_score']}/100")
216
-
217
- overview, quality, explore, ai_tab, model_tab, deliver = st.tabs(
218
- ["Overview", "Data quality", "Explore", "AI insights", "Model lab", "Deliver"]
219
- )
220
-
221
- with overview:
222
- st.subheader(st.session_state.dataset_name)
223
- st.caption(f"Dataset fingerprint {brief.fingerprint} · {brief.memory_mb:.2f} MB in memory")
224
- first, last, sample = st.tabs(["First 5 rows", "Last 5 rows", "Random sample"])
225
- first.dataframe(frame.head(), width="stretch", hide_index=True)
226
- last.dataframe(frame.tail(), width="stretch", hide_index=True)
227
- sample.dataframe(
228
- frame.sample(min(5, len(frame)), random_state=42), width="stretch", hide_index=True
229
- )
230
- st.markdown("#### Data dictionary")
231
- st.dataframe(
232
- profile["dictionary"].drop(columns=["issue_count"]), width="stretch", hide_index=True
233
- )
234
-
235
- with quality:
236
- a, b = st.columns([0.75, 1.25])
237
- with a:
238
- st.markdown("#### Quality signals")
239
- st.metric("Duplicate rows", f"{brief.duplicate_rows:,}")
240
- st.metric("Completeness", f"{100 - brief.missing_cells / max(1, frame.size) * 100:.1f}%")
241
- flagged = profile["dictionary"].query("issue_count > 0")
242
- st.metric("Flagged columns", len(flagged))
243
- st.info("DataPilot reports evidence first. No rows or values are changed without approval.")
244
- with b:
245
- missing = profile["missing"][profile["missing"] > 0].sort_values()
246
- if len(missing):
247
- fig = px.bar(
248
- x=missing.values,
249
- y=missing.index,
250
- orientation="h",
251
- labels={"x": "Missing values", "y": "Column"},
252
- title="Missing values by column",
253
- color=missing.values,
254
- color_continuous_scale=["#49d7c5", "#6d8dff"],
255
- )
256
- fig.update_layout(
257
- template="plotly_dark",
258
- paper_bgcolor="#0e1b2c",
259
- plot_bgcolor="#0e1b2c",
260
- coloraxis_showscale=False,
261
- )
262
- st.plotly_chart(fig, width="stretch")
263
- else:
264
- st.success("No missing values detected.")
265
- if len(flagged):
266
- st.dataframe(flagged.drop(columns=["issue_count"]), width="stretch", hide_index=True)
267
-
268
- with explore:
269
- numeric = profile["numeric"]
270
- if numeric:
271
- selected = st.selectbox("Explore a numerical feature", numeric)
272
- c1, c2 = st.columns(2)
273
- fig = px.histogram(
274
- frame,
275
- x=selected,
276
- marginal="box",
277
- title=f"Distribution of {selected}",
278
- color_discrete_sequence=["#49d7c5"],
279
- )
280
- fig.update_layout(template="plotly_dark", paper_bgcolor="#0e1b2c", plot_bgcolor="#0e1b2c")
281
- c1.plotly_chart(fig, width="stretch")
282
- if not profile["correlation"].empty:
283
- heat = px.imshow(
284
- profile["correlation"],
285
- text_auto=".2f",
286
- aspect="auto",
287
- color_continuous_scale=["#1a2940", "#49d7c5", "#f4b860"],
288
- title="Numeric correlation map",
289
- )
290
- heat.update_layout(template="plotly_dark", paper_bgcolor="#0e1b2c")
291
- c2.plotly_chart(heat, width="stretch")
292
- else:
293
- c2.info("Add another numerical column to calculate correlations.")
294
- st.dataframe(frame[numeric].describe().T, width="stretch")
295
- else:
296
- st.info("This dataset has no numerical columns. Use the categorical overview below.")
297
- categories = profile["categorical"]
298
- if categories:
299
- selected_cat = st.selectbox("Explore a categorical feature", categories)
300
- counts = frame[selected_cat].astype(str).value_counts().head(20).reset_index()
301
- fig = px.bar(
302
- counts,
303
- x="count",
304
- y=selected_cat,
305
- orientation="h",
306
- title=f"Top values · {selected_cat}",
307
- color="count",
308
- color_continuous_scale=["#49d7c5", "#6d8dff"],
309
- )
310
- fig.update_layout(
311
- template="plotly_dark",
312
- paper_bgcolor="#0e1b2c",
313
- plot_bgcolor="#0e1b2c",
314
- coloraxis_showscale=False,
315
- )
316
- st.plotly_chart(fig, width="stretch")
317
-
318
- with ai_tab:
319
- st.markdown("#### Ask Gemini to interpret the computed evidence")
320
- st.caption(
321
- "AI interpretation based on dataset metadata and limited redacted samples. Verify against source documentation."
322
- )
323
- if not api_key:
324
- st.warning(
325
- "Enter a Gemini API key in the sidebar. Deterministic profiling remains fully available without AI."
326
- )
327
- if st.button("Generate AI analyst brief", disabled=not bool(api_key)):
328
  try:
329
- # Keep the widget rerun free of transient status elements. Some hosted
330
- # reverse proxies can drop the Streamlit session while a synchronous
331
- # external request is paired with an actively updating spinner.
332
- summary = gemini_dataset_summary(frame, profile, api_key, model, excluded)
333
- st.session_state.ai_summary = summary
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
334
  st.success("AI analyst brief ready")
335
- except ValueError as exc:
336
- st.warning(str(exc))
337
- except Exception:
338
- st.error(
339
- "AI Insights encountered an unexpected problem. "
340
- "Your dataset and deterministic analysis remain available."
341
- )
342
- if st.session_state.ai_summary:
343
- st.markdown(st.session_state.ai_summary)
344
- with st.expander("What may be sent to Gemini"):
345
- st.write(
346
- "Column metadata, aggregate statistics, target candidates, quality score, and up to three redacted example rows."
347
- )
348
- st.write(
349
- "Automatically excluded potential PII:",
350
- [
351
- c
352
- for c in frame.columns
353
- if any(
354
- k in str(c).lower() for k in ("email", "phone", "address", "name", "account")
355
- )
356
- ]
357
- or "None detected",
358
- )
359
-
360
- with model_tab:
361
- st.markdown("#### Confirm the analytical target")
362
- candidates = pd.DataFrame(profile["targets"])
363
- st.dataframe(candidates, width="stretch", hide_index=True)
364
- default_target = st.session_state.target or (
365
- profile["targets"][0]["column"] if profile["targets"] else frame.columns[-1]
366
- )
367
- target = st.selectbox(
368
- "Target column", list(frame.columns), index=list(frame.columns).index(default_target)
369
- )
370
- st.caption("DataPilot will not train supervised models until you confirm this selection.")
371
- if frame[target].nunique(dropna=True) < 2:
372
- st.error("The selected target has fewer than two observed values.")
373
- run = st.button(
374
- "Run autonomous model study",
375
- type="primary",
376
- disabled=frame[target].nunique(dropna=True) < 2,
377
- )
378
- if run:
379
- try:
380
- progress = st.progress(0, text="Preparing agent graph")
381
- progress.progress(12, text="Data Quality Agent · auditing risks")
382
- with st.spinner(
383
- "LangGraph agents are profiling, planning, training, evaluating, and explaining…"
384
- ):
385
- result = run_analysis(frame, target, st.session_state.dataset_name, settings)
386
- progress.progress(100, text="Analysis complete")
387
- st.session_state.result = result.model_dump(mode="json")
388
- st.success(
389
- "Model study completed with leakage-safe preprocessing and cross-validation."
390
- )
391
- except Exception as exc:
392
- st.error(f"Model study failed: {exc}")
393
- result = st.session_state.result
394
- if result:
395
- best = result["model_results"][0]
396
- c1, c2, c3 = st.columns(3)
397
- c1.metric("Selected model", result["best_model"])
398
- c2.metric(
399
- "One-time test " + best["primary_metric"].replace("_", " ").title(),
400
- f"{best['final_test_score']:.3f}",
401
- )
402
- c3.metric("CV mean", f"{best['cross_validation_mean']:.3f}")
403
- results = pd.DataFrame(result["model_results"])
404
- fig = px.bar(
405
- results.sort_values("selection_score"),
406
- x="selection_score",
407
- y="name",
408
- orientation="h",
409
- color="selection_score",
410
- title="Training-CV model selection",
411
- color_continuous_scale=["#344b69", "#49d7c5"],
412
- )
413
- fig.update_layout(
414
- template="plotly_dark",
415
- paper_bgcolor="#0e1b2c",
416
- plot_bgcolor="#0e1b2c",
417
- coloraxis_showscale=False,
418
- )
419
- st.plotly_chart(fig, width="stretch")
420
- st.dataframe(results, width="stretch", hide_index=True)
421
- st.markdown("#### Agent execution trace")
422
- st.dataframe(pd.DataFrame(result["trace"]), width="stretch", hide_index=True)
423
-
424
- with deliver:
425
- st.markdown("#### Export your evidence")
426
- c1, c2 = st.columns(2)
427
- c1.download_button(
428
- "Download original dataset · CSV",
429
- dataframe_csv(frame),
430
- file_name=f"{Path(st.session_state.dataset_name).stem}_datapilot.csv",
431
- mime="text/csv",
432
- width="stretch",
433
- )
434
- c2.download_button(
435
- "Download data dictionary · CSV",
436
- dataframe_csv(profile["dictionary"].drop(columns=["issue_count"])),
437
- file_name="datapilot_data_dictionary.csv",
438
- mime="text/csv",
439
- width="stretch",
440
- )
441
- result = st.session_state.result
442
- if result:
443
- st.markdown("#### Model and report artifacts")
444
- columns = st.columns(min(4, len(result["artifacts"])))
445
- for column, (name, raw_path) in zip(columns, result["artifacts"].items(), strict=False):
446
- path = Path(raw_path)
447
- if path.exists():
448
- column.download_button(
449
- name.replace("_", " ").title(),
450
- path.read_bytes(),
451
- file_name=path.name,
452
- width="stretch",
453
- )
454
- else:
455
- st.info(
456
- "Run a model study to unlock the fitted pipeline, model card, metrics, and HTML report."
457
- )
458
-
459
- st.caption(
460
- "DataPilot provides exploratory decision support. Predictive associations do not establish causality."
461
- )
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import io
4
+ import os
5
+ import time
6
+ from concurrent.futures import ThreadPoolExecutor
7
+ from pathlib import Path
8
+
9
+ import pandas as pd
10
+ import plotly.express as px
11
+ import streamlit as st
12
+
13
+ from datapilot.analyst import dataframe_csv, gemini_dataset_summary, inspect_dataset
14
+ from datapilot.config import get_settings
15
+ from datapilot.data import SAMPLE_DATASETS, load_sample
16
+ from datapilot.workflow import run_analysis
17
+
18
+ st.set_page_config(
19
+ page_title="DataPilot · Autonomous Data Analyst",
20
+ page_icon="",
21
+ layout="wide",
22
+ initial_sidebar_state="expanded",
23
+ )
24
+
25
+ st.markdown(
26
+ """
27
+ <style>
28
+ @import url('https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;600;700&family=Manrope:wght@600;700;800&display=swap');
29
+ :root{--navy:#07111f;--panel:#0e1b2c;--line:#203149;--cyan:#49d7c5;--blue:#6d8dff;--text:#edf4ff;--muted:#91a1b7}
30
+ .stApp{background:radial-gradient(circle at 75% -10%,#17355c 0,transparent 35%),#07111f;color:var(--text)}
31
+ html,body,[class*="css"]{font-family:"DM Sans",sans-serif}
32
+ h1,h2,h3{font-family:"Manrope",sans-serif;letter-spacing:-.03em}
33
+ header[data-testid="stHeader"]{background:transparent}
34
+ div[data-testid="stSidebar"]{background:#091522;border-right:1px solid var(--line)}
35
+ .block-container{max-width:1480px;padding-top:1.1rem;padding-bottom:4rem}
36
+ .brand{display:flex;gap:.75rem;align-items:center;font:800 1.2rem Manrope;color:white;margin:.2rem 0 1.3rem}
37
+ .brand-mark{display:grid;place-items:center;width:34px;height:34px;border-radius:10px;background:linear-gradient(135deg,var(--cyan),var(--blue));color:#07111f}
38
+ .hero{border:1px solid #29405d;background:linear-gradient(125deg,rgba(17,35,57,.96),rgba(9,22,38,.88));border-radius:24px;padding:2rem 2.2rem;margin-bottom:1rem;overflow:hidden;position:relative}
39
+ .hero:after{content:"";position:absolute;width:340px;height:340px;border-radius:50%;right:-100px;top:-190px;background:rgba(73,215,197,.10)}
40
+ .eyebrow{color:var(--cyan);font-size:.73rem;font-weight:700;letter-spacing:.18em;text-transform:uppercase}
41
+ .hero h1{font-size:clamp(2.1rem,4vw,4rem);line-height:1.02;margin:.45rem 0 .7rem;color:white}
42
+ .hero p{max-width:790px;color:#aebdd0;font-size:1.02rem;line-height:1.65;margin:0}
43
+ .stepbar{display:flex;gap:.5rem;flex-wrap:wrap;margin-top:1.4rem}.step{border:1px solid #2d4664;border-radius:999px;padding:.4rem .72rem;color:#9eafc4;font-size:.75rem}.step.on{color:#07111f;background:var(--cyan);border-color:var(--cyan);font-weight:700}
44
+ .panel{background:rgba(14,27,44,.92);border:1px solid var(--line);border-radius:18px;padding:1.15rem 1.25rem;height:100%}
45
+ .kicker{color:var(--cyan);font-size:.72rem;font-weight:700;text-transform:uppercase;letter-spacing:.12em}.muted{color:var(--muted);font-size:.87rem;line-height:1.55}
46
+ .signature{background:linear-gradient(145deg,#11263b,#0b1828);border:1px solid #29435f;border-radius:17px;padding:1rem;margin-top:1rem}.signature strong{color:white}.signature a{color:var(--cyan);text-decoration:none;font-size:.83rem}
47
+ div[data-testid="stMetric"]{background:#0d1b2c;border:1px solid var(--line);padding:15px 17px;border-radius:15px}div[data-testid="stMetric"] label{color:#91a1b7}div[data-testid="stMetricValue"]{color:white}
48
+ .stButton>button,.stDownloadButton>button{border:0;border-radius:11px;background:linear-gradient(135deg,#49d7c5,#6d8dff);color:#07111f;font-weight:800}
49
+ .stButton>button:hover,.stDownloadButton>button:hover{color:#07111f;filter:brightness(1.08)}
50
+ div[data-testid="stFileUploaderDropzone"]{background:#0c1a2b;border:1.5px dashed #3b617c;border-radius:16px;padding:1.3rem}
51
+ div[data-baseweb="tab-list"]{gap:.3rem;background:#0b1828;border:1px solid var(--line);border-radius:13px;padding:.3rem}
52
+ button[data-baseweb="tab"]{border-radius:9px;color:#9caec3}button[data-baseweb="tab"][aria-selected="true"]{background:#172b41;color:white}
53
+ .stDataFrame{border:1px solid var(--line);border-radius:13px;overflow:hidden}
54
+ [data-testid="stAlert"]{border-radius:13px}
55
+ </style>
56
+ """,
57
+ unsafe_allow_html=True,
58
+ )
59
+
60
+
61
+ @st.cache_resource
62
+ def ai_executor() -> ThreadPoolExecutor:
63
+ """Keep slow provider I/O off Streamlit's session-handling thread."""
64
+ return ThreadPoolExecutor(max_workers=2, thread_name_prefix="datapilot-ai")
65
+
66
+ settings = get_settings()
67
+ for key, default in {
68
+ "frame": None,
69
+ "dataset_name": "",
70
+ "profile": None,
71
+ "result": None,
72
+ "ai_summary": "",
73
+ "ai_future": None,
74
+ "chat": [],
75
+ "target": None,
76
+ }.items():
77
+ if key not in st.session_state:
78
+ st.session_state[key] = default
79
+
80
+
81
+ def read_upload(uploaded) -> pd.DataFrame:
82
+ suffix = Path(uploaded.name).suffix.lower()
83
+ raw = uploaded.getvalue()
84
+ if len(raw) > settings.max_upload_mb * 1_048_576:
85
+ raise ValueError(f"File exceeds the {settings.max_upload_mb} MB limit.")
86
+ stream = io.BytesIO(raw)
87
+ if suffix in {".csv", ".tsv", ".txt"}:
88
+ return pd.read_csv(stream, sep="\t" if suffix == ".tsv" else None, engine="python")
89
+ if suffix in {".xlsx", ".xls"}:
90
+ return pd.read_excel(stream)
91
+ if suffix == ".parquet":
92
+ return pd.read_parquet(stream)
93
+ if suffix == ".json":
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
94
  try:
95
+ return pd.read_json(stream)
96
+ except ValueError:
97
+ stream.seek(0)
98
+ return pd.read_json(stream, lines=True)
99
+ raise ValueError("Use CSV, TSV, Excel, JSON, or Parquet.")
100
+
101
+
102
+ with st.sidebar:
103
+ st.markdown(
104
+ '<div class="brand"><span class="brand-mark">✦</span>DataPilot</div>',
105
+ unsafe_allow_html=True,
106
+ )
107
+ st.caption("AUTONOMOUS ANALYSIS WORKSPACE")
108
+ st.markdown("##### Gemini intelligence")
109
+ server_api_key = os.getenv("GEMINI_API_KEY", os.getenv("GOOGLE_API_KEY", ""))
110
+ user_api_key = st.text_input(
111
+ "Personal Gemini API key (optional)",
112
+ value="",
113
+ type="password",
114
+ help="Leave blank to use the secured server-side key. Never stored or logged.",
115
+ )
116
+ api_key = user_api_key.strip() or server_api_key
117
+ model = st.selectbox("Model", ["gemini-2.5-flash", "gemini-2.5-pro", "gemini-2.0-flash"])
118
+ st.caption(
119
+ "● AI ready · secured server key"
120
+ if server_api_key
121
+ else ("● AI ready" if api_key else "○ Local analysis mode")
122
+ )
123
+ st.divider()
124
+ st.markdown("##### Privacy controls")
125
+ metadata_only = st.toggle(
126
+ "Metadata-first AI",
127
+ value=True,
128
+ help="Send schema, aggregate statistics, and three redacted examples—not the full dataset.",
129
+ )
130
+ excluded = st.multiselect(
131
+ "Exclude columns from AI",
132
+ list(st.session_state.frame.columns) if st.session_state.frame is not None else [],
133
+ )
134
+ st.divider()
135
+ if st.button("Reset workspace", width="stretch"):
136
+ for key in ("frame", "profile", "result", "ai_summary", "chat", "target"):
137
+ st.session_state[key] = (
138
+ None
139
+ if key in {"frame", "profile", "result", "target"}
140
+ else ([] if key == "chat" else "")
141
+ )
142
+ st.rerun()
143
+ st.markdown(
144
+ """
145
+ <div class="signature">
146
+ <div class="kicker">Built & designed by</div>
147
+ <strong>Dinesh Barri</strong><br>
148
+ <span class="muted">AI Engineer · Data Scientist</span><br><br>
149
+ <a href="https://github.com/dineshbarri">GitHub ↗</a>&nbsp;&nbsp;
150
+ <a href="https://www.linkedin.com/in/dinesh-barri-7654b010b">LinkedIn ↗</a>
151
+ </div>""",
152
+ unsafe_allow_html=True,
153
+ )
154
+
155
+ loaded = st.session_state.frame is not None
156
+ st.markdown(
157
+ f"""
158
+ <section class="hero">
159
+ <div class="eyebrow">Evidence-first autonomous data science</div>
160
+ <h1>Your data. Explained.<br>Decisions, accelerated.</h1>
161
+ <p>Upload a dataset and DataPilot immediately inspects its structure, surfaces quality risks,
162
+ recommends analytical targets, creates interactive evidence, and prepares a leakage-safe
163
+ machine-learning study—with Gemini available for grounded interpretation.</p>
164
+ <div class="stepbar">
165
+ <span class="step {"on" if loaded else ""}">01 · Connect</span>
166
+ <span class="step {"on" if loaded else ""}">02 · Inspect</span>
167
+ <span class="step {"on" if st.session_state.ai_summary else ""}">03 · Interpret</span>
168
+ <span class="step {"on" if st.session_state.result else ""}">04 · Model</span>
169
+ <span class="step {"on" if st.session_state.result else ""}">05 · Deliver</span>
170
+ </div>
171
+ </section>""",
172
+ unsafe_allow_html=True,
173
+ )
174
+
175
+ if not loaded:
176
+ left, right = st.columns([1.35, 0.65], gap="large")
177
+ with left:
178
+ st.markdown('<div class="kicker">Start a new analysis</div>', unsafe_allow_html=True)
179
+ st.subheader("Drop in your dataset")
180
+ uploaded = st.file_uploader(
181
+ "Upload dataset",
182
+ type=["csv", "tsv", "txt", "xlsx", "xls", "json", "parquet"],
183
+ label_visibility="collapsed",
184
+ )
185
+ st.caption("CSV · TSV · Excel · JSON · Parquet | Raw data remains in this session.")
186
+ if uploaded:
187
+ try:
188
+ with st.status("DataPilot is inspecting your dataset…", expanded=True) as status:
189
+ st.write("Validating file structure")
190
+ frame = read_upload(uploaded)
191
+ st.write("Profiling columns, missingness, cardinality, and target candidates")
192
+ profile = inspect_dataset(frame)
193
+ st.session_state.frame = frame
194
+ st.session_state.profile = profile
195
+ st.session_state.dataset_name = uploaded.name
196
+ status.update(label="Dataset ready", state="complete")
197
+ st.rerun()
198
+ except Exception as exc:
199
+ st.error(f"Upload could not be processed: {exc}")
200
+ with right:
201
+ st.markdown(
202
+ '<div class="panel"><div class="kicker">Try it instantly</div><h3>Explore a trusted demo</h3><p class="muted">Load a complete classification or regression dataset and see the full analyst workflow.</p></div>',
203
+ unsafe_allow_html=True,
204
+ )
205
+ demo = st.selectbox("Demo dataset", list(SAMPLE_DATASETS))
206
+ if st.button("Load demo workspace", width="stretch"):
207
+ frame, target, name = load_sample(SAMPLE_DATASETS[demo])
208
+ st.session_state.frame, st.session_state.target = frame, target
209
+ st.session_state.dataset_name = name
210
+ st.session_state.profile = inspect_dataset(frame)
211
+ st.rerun()
212
+ st.stop()
213
+
214
+ frame: pd.DataFrame = st.session_state.frame
215
+ profile = st.session_state.profile or inspect_dataset(frame)
216
+ brief = profile["brief"]
217
+
218
+ metrics = st.columns(6)
219
+ metrics[0].metric("Rows", f"{brief.rows:,}")
220
+ metrics[1].metric("Columns", f"{brief.columns:,}")
221
+ metrics[2].metric("Numeric", brief.numeric)
222
+ metrics[3].metric("Categorical", brief.categorical)
223
+ metrics[4].metric("Missing cells", f"{brief.missing_cells:,}")
224
+ metrics[5].metric("Quality score", f"{profile['quality_score']}/100")
225
+
226
+ overview, quality, explore, ai_tab, model_tab, deliver = st.tabs(
227
+ ["Overview", "Data quality", "Explore", "AI insights", "Model lab", "Deliver"]
228
+ )
229
+
230
+ with overview:
231
+ st.subheader(st.session_state.dataset_name)
232
+ st.caption(f"Dataset fingerprint {brief.fingerprint} · {brief.memory_mb:.2f} MB in memory")
233
+ first, last, sample = st.tabs(["First 5 rows", "Last 5 rows", "Random sample"])
234
+ first.dataframe(frame.head(), width="stretch", hide_index=True)
235
+ last.dataframe(frame.tail(), width="stretch", hide_index=True)
236
+ sample.dataframe(
237
+ frame.sample(min(5, len(frame)), random_state=42), width="stretch", hide_index=True
238
+ )
239
+ st.markdown("#### Data dictionary")
240
+ st.dataframe(
241
+ profile["dictionary"].drop(columns=["issue_count"]), width="stretch", hide_index=True
242
+ )
243
+
244
+ with quality:
245
+ a, b = st.columns([0.75, 1.25])
246
+ with a:
247
+ st.markdown("#### Quality signals")
248
+ st.metric("Duplicate rows", f"{brief.duplicate_rows:,}")
249
+ st.metric("Completeness", f"{100 - brief.missing_cells / max(1, frame.size) * 100:.1f}%")
250
+ flagged = profile["dictionary"].query("issue_count > 0")
251
+ st.metric("Flagged columns", len(flagged))
252
+ st.info("DataPilot reports evidence first. No rows or values are changed without approval.")
253
+ with b:
254
+ missing = profile["missing"][profile["missing"] > 0].sort_values()
255
+ if len(missing):
256
+ fig = px.bar(
257
+ x=missing.values,
258
+ y=missing.index,
259
+ orientation="h",
260
+ labels={"x": "Missing values", "y": "Column"},
261
+ title="Missing values by column",
262
+ color=missing.values,
263
+ color_continuous_scale=["#49d7c5", "#6d8dff"],
264
+ )
265
+ fig.update_layout(
266
+ template="plotly_dark",
267
+ paper_bgcolor="#0e1b2c",
268
+ plot_bgcolor="#0e1b2c",
269
+ coloraxis_showscale=False,
270
+ )
271
+ st.plotly_chart(fig, width="stretch")
272
+ else:
273
+ st.success("No missing values detected.")
274
+ if len(flagged):
275
+ st.dataframe(flagged.drop(columns=["issue_count"]), width="stretch", hide_index=True)
276
+
277
+ with explore:
278
+ numeric = profile["numeric"]
279
+ if numeric:
280
+ selected = st.selectbox("Explore a numerical feature", numeric)
281
+ c1, c2 = st.columns(2)
282
+ fig = px.histogram(
283
+ frame,
284
+ x=selected,
285
+ marginal="box",
286
+ title=f"Distribution of {selected}",
287
+ color_discrete_sequence=["#49d7c5"],
288
+ )
289
+ fig.update_layout(template="plotly_dark", paper_bgcolor="#0e1b2c", plot_bgcolor="#0e1b2c")
290
+ c1.plotly_chart(fig, width="stretch")
291
+ if not profile["correlation"].empty:
292
+ heat = px.imshow(
293
+ profile["correlation"],
294
+ text_auto=".2f",
295
+ aspect="auto",
296
+ color_continuous_scale=["#1a2940", "#49d7c5", "#f4b860"],
297
+ title="Numeric correlation map",
298
+ )
299
+ heat.update_layout(template="plotly_dark", paper_bgcolor="#0e1b2c")
300
+ c2.plotly_chart(heat, width="stretch")
301
+ else:
302
+ c2.info("Add another numerical column to calculate correlations.")
303
+ st.dataframe(frame[numeric].describe().T, width="stretch")
304
+ else:
305
+ st.info("This dataset has no numerical columns. Use the categorical overview below.")
306
+ categories = profile["categorical"]
307
+ if categories:
308
+ selected_cat = st.selectbox("Explore a categorical feature", categories)
309
+ counts = frame[selected_cat].astype(str).value_counts().head(20).reset_index()
310
+ fig = px.bar(
311
+ counts,
312
+ x="count",
313
+ y=selected_cat,
314
+ orientation="h",
315
+ title=f"Top values · {selected_cat}",
316
+ color="count",
317
+ color_continuous_scale=["#49d7c5", "#6d8dff"],
318
+ )
319
+ fig.update_layout(
320
+ template="plotly_dark",
321
+ paper_bgcolor="#0e1b2c",
322
+ plot_bgcolor="#0e1b2c",
323
+ coloraxis_showscale=False,
324
+ )
325
+ st.plotly_chart(fig, width="stretch")
326
+
327
+ with ai_tab:
328
+ st.markdown("#### Ask Gemini to interpret the computed evidence")
329
+ st.caption(
330
+ "AI interpretation based on dataset metadata and limited redacted samples. Verify against source documentation."
331
+ )
332
+ if not api_key:
333
+ st.warning(
334
+ "Enter a Gemini API key in the sidebar. Deterministic profiling remains fully available without AI."
335
+ )
336
+ ai_future = st.session_state.ai_future
337
+ if st.button(
338
+ "Generate AI analyst brief",
339
+ disabled=not bool(api_key) or ai_future is not None,
340
+ ):
341
+ st.session_state.ai_future = ai_executor().submit(
342
+ gemini_dataset_summary,
343
+ frame.copy(deep=True),
344
+ profile,
345
+ api_key,
346
+ model,
347
+ list(excluded),
348
+ )
349
+ st.rerun()
350
+
351
+ ai_future = st.session_state.ai_future
352
+ if ai_future is not None and ai_future.done():
353
+ try:
354
+ st.session_state.ai_summary = ai_future.result()
355
  st.success("AI analyst brief ready")
356
+ except ValueError as exc:
357
+ st.warning(str(exc))
358
+ except Exception:
359
+ st.error(
360
+ "AI Insights encountered an unexpected problem. "
361
+ "Your dataset and deterministic analysis remain available."
362
+ )
363
+ finally:
364
+ st.session_state.ai_future = None
365
+ elif ai_future is not None:
366
+ st.info("Gemini is reviewing the bounded evidence package…")
367
+ time.sleep(0.5)
368
+ st.rerun()
369
+ if st.session_state.ai_summary:
370
+ st.markdown(st.session_state.ai_summary)
371
+ with st.expander("What may be sent to Gemini"):
372
+ st.write(
373
+ "Column metadata, aggregate statistics, target candidates, quality score, and up to three redacted example rows."
374
+ )
375
+ st.write(
376
+ "Automatically excluded potential PII:",
377
+ [
378
+ c
379
+ for c in frame.columns
380
+ if any(
381
+ k in str(c).lower() for k in ("email", "phone", "address", "name", "account")
382
+ )
383
+ ]
384
+ or "None detected",
385
+ )
386
+
387
+ with model_tab:
388
+ st.markdown("#### Confirm the analytical target")
389
+ candidates = pd.DataFrame(profile["targets"])
390
+ st.dataframe(candidates, width="stretch", hide_index=True)
391
+ default_target = st.session_state.target or (
392
+ profile["targets"][0]["column"] if profile["targets"] else frame.columns[-1]
393
+ )
394
+ target = st.selectbox(
395
+ "Target column", list(frame.columns), index=list(frame.columns).index(default_target)
396
+ )
397
+ st.caption("DataPilot will not train supervised models until you confirm this selection.")
398
+ if frame[target].nunique(dropna=True) < 2:
399
+ st.error("The selected target has fewer than two observed values.")
400
+ run = st.button(
401
+ "Run autonomous model study",
402
+ type="primary",
403
+ disabled=frame[target].nunique(dropna=True) < 2,
404
+ )
405
+ if run:
406
+ try:
407
+ progress = st.progress(0, text="Preparing agent graph")
408
+ progress.progress(12, text="Data Quality Agent · auditing risks")
409
+ with st.spinner(
410
+ "LangGraph agents are profiling, planning, training, evaluating, and explaining…"
411
+ ):
412
+ result = run_analysis(frame, target, st.session_state.dataset_name, settings)
413
+ progress.progress(100, text="Analysis complete")
414
+ st.session_state.result = result.model_dump(mode="json")
415
+ st.success(
416
+ "Model study completed with leakage-safe preprocessing and cross-validation."
417
+ )
418
+ except Exception as exc:
419
+ st.error(f"Model study failed: {exc}")
420
+ result = st.session_state.result
421
+ if result:
422
+ best = result["model_results"][0]
423
+ c1, c2, c3 = st.columns(3)
424
+ c1.metric("Selected model", result["best_model"])
425
+ c2.metric(
426
+ "One-time test " + best["primary_metric"].replace("_", " ").title(),
427
+ f"{best['final_test_score']:.3f}",
428
+ )
429
+ c3.metric("CV mean", f"{best['cross_validation_mean']:.3f}")
430
+ results = pd.DataFrame(result["model_results"])
431
+ fig = px.bar(
432
+ results.sort_values("selection_score"),
433
+ x="selection_score",
434
+ y="name",
435
+ orientation="h",
436
+ color="selection_score",
437
+ title="Training-CV model selection",
438
+ color_continuous_scale=["#344b69", "#49d7c5"],
439
+ )
440
+ fig.update_layout(
441
+ template="plotly_dark",
442
+ paper_bgcolor="#0e1b2c",
443
+ plot_bgcolor="#0e1b2c",
444
+ coloraxis_showscale=False,
445
+ )
446
+ st.plotly_chart(fig, width="stretch")
447
+ st.dataframe(results, width="stretch", hide_index=True)
448
+ st.markdown("#### Agent execution trace")
449
+ st.dataframe(pd.DataFrame(result["trace"]), width="stretch", hide_index=True)
450
+
451
+ with deliver:
452
+ st.markdown("#### Export your evidence")
453
+ c1, c2 = st.columns(2)
454
+ c1.download_button(
455
+ "Download original dataset · CSV",
456
+ dataframe_csv(frame),
457
+ file_name=f"{Path(st.session_state.dataset_name).stem}_datapilot.csv",
458
+ mime="text/csv",
459
+ width="stretch",
460
+ )
461
+ c2.download_button(
462
+ "Download data dictionary · CSV",
463
+ dataframe_csv(profile["dictionary"].drop(columns=["issue_count"])),
464
+ file_name="datapilot_data_dictionary.csv",
465
+ mime="text/csv",
466
+ width="stretch",
467
+ )
468
+ result = st.session_state.result
469
+ if result:
470
+ st.markdown("#### Model and report artifacts")
471
+ columns = st.columns(min(4, len(result["artifacts"])))
472
+ for column, (name, raw_path) in zip(columns, result["artifacts"].items(), strict=False):
473
+ path = Path(raw_path)
474
+ if path.exists():
475
+ column.download_button(
476
+ name.replace("_", " ").title(),
477
+ path.read_bytes(),
478
+ file_name=path.name,
479
+ width="stretch",
480
+ )
481
+ else:
482
+ st.info(
483
+ "Run a model study to unlock the fitted pipeline, model card, metrics, and HTML report."
484
+ )
485
+
486
+ st.caption(
487
+ "DataPilot provides exploratory decision support. Predictive associations do not establish causality."
488
+ )