Spaces:
Running
Running
Download app.py from BonusLockSMith/csv-insights: direct link, hf CLI and curl.
- Browser
- Download file 8.46 kB
-
https://huggingface.co/spaces/BonusLockSMith/csv-insights/resolve/main/app.py
- Command line
-
hf download hf://spaces/BonusLockSMith/csv-insights/app.py
-
curl -L -o app.py https://huggingface.co/spaces/BonusLockSMith/csv-insights/resolve/main/app.py
8.46 kB
| #!/usr/bin/env python | |
| """CSV -> Insights Analyst (#12) — Flask UI. | |
| Upload a CSV (or try the sample) -> deterministic pandas profile -> auto-charts -> a plain-English | |
| briefing grounded ONLY in the computed numbers. Local GPU-fleet LLM by default (free), Claude optional. | |
| pip install -r requirements.txt | |
| python app.py # http://127.0.0.1:7860 | |
| """ | |
| import html | |
| import io | |
| import os | |
| import re | |
| import traceback | |
| from flask import Flask, request, render_template_string | |
| import pandas as pd | |
| from profiler import profile_df | |
| from charts import build_charts | |
| from narrator import narrate, BACKEND | |
| app = Flask(__name__) | |
| app.config["MAX_CONTENT_LENGTH"] = 12 * 1024 * 1024 # 12 MB upload cap | |
| SAMPLE = os.path.join(os.path.dirname(__file__), "samples", "seattle-weather.csv") | |
| PAGE = """<!doctype html><html lang="en"><head> | |
| <meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"> | |
| <title>CSV → Insights · GritAI</title> | |
| <link rel="stylesheet" href="/static/gritai.css"> | |
| <style> | |
| .wrap{max-width:1040px;margin:0 auto;padding:34px 22px 80px} | |
| .up{display:flex;gap:14px;flex-wrap:wrap;align-items:center;margin-top:18px} | |
| input[type=file]{color:var(--dim);font-family:var(--mono);font-size:13px} | |
| .grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(300px,1fr));gap:16px;margin-top:18px} | |
| .chart img{width:100%;height:auto;border:1px solid var(--line);border-radius:3px;display:block} | |
| .stat{display:flex;justify-content:space-between;gap:12px;padding:7px 0;border-bottom:1px solid var(--line);font-size:13.5px} | |
| .stat .k{color:var(--dim)} .stat .v{font-family:var(--mono);color:var(--ink)} | |
| .findings{font-size:15px;line-height:1.7} | |
| .findings h3{font-family:var(--disp);color:var(--grit);font-size:15px;letter-spacing:.06em;text-transform:uppercase;margin:18px 0 8px} | |
| .findings li{margin:5px 0} | |
| .flag{color:var(--bad)} .chip{color:var(--grit)} | |
| .err{border:1px solid var(--bad);color:var(--bad);padding:14px 16px;border-radius:3px;background:rgba(255,107,107,.06)} | |
| .muted{color:var(--dim2);font-family:var(--mono);font-size:11px;letter-spacing:.16em;text-transform:uppercase} | |
| </style></head><body> | |
| <div class="wrap"> | |
| <header style="display:flex;align-items:center;gap:14px"> | |
| <svg width="38" height="38" viewBox="0 0 38 38" fill="none"><rect x="1" y="1" width="36" height="36" rx="2" stroke="#2b303c"/><path d="M27 12.5A9 9 0 1 0 28 22H19" stroke="#ff5a2a" stroke-width="2.4" stroke-linecap="square"/><rect x="10.5" y="10.5" width="3" height="3" fill="#ff5a2a"/></svg> | |
| <div class="brand"><div class="co">Grit<b>AI</b> · CSV → Insights</div> | |
| <div class="sub">Upload data · charts + plain-English findings</div></div> | |
| <div style="flex:1"></div><div class="tag hot">#12 · 30-in-15</div> | |
| </header> | |
| <div class="hero" style="margin-top:26px"> | |
| <h1>Turn a spreadsheet into a briefing.</h1> | |
| <p style="color:var(--dim);max-width:60ch;margin-top:12px">Every statistic and chart is computed | |
| directly from your file. The AI only writes the story — it never invents a number.</p> | |
| </div> | |
| <form class="panel" style="padding:20px" method="post" action="/analyze" enctype="multipart/form-data"> | |
| <div class="muted">Analyze a CSV</div> | |
| <div class="up"> | |
| <input type="file" name="file" accept=".csv"> | |
| <button class="btn" type="submit">Analyze ▸</button> | |
| <button class="btn ghost" type="submit" name="sample" value="1">Try the sample (Seattle weather)</button> | |
| </div> | |
| </form> | |
| {{ body|safe }} | |
| <p class="muted" style="margin-top:40px">Backend: {{ backend }} · Local GPU fleet by default · GritAI Solutions</p> | |
| </div></body></html>""" | |
| def md_to_html(text: str) -> str: | |
| """Tiny, safe markdown -> HTML (escape first, then a few inline/block rules). No deps.""" | |
| out, in_ul = [], False | |
| for raw in text.splitlines(): | |
| line = html.escape(raw.rstrip()) | |
| line = re.sub(r"\*\*(.+?)\*\*", r"<strong>\1</strong>", line) | |
| line = re.sub(r"`(.+?)`", r"<code>\1</code>", line) | |
| m_h = re.match(r"^\s*#{1,6}\s*(.+?):?\s*$", raw) | |
| m_li = re.match(r"^\s*[-*]\s+(.+)$", raw) | |
| if m_h: | |
| if in_ul: out.append("</ul>"); in_ul = False | |
| out.append(f"<h3>{html.escape(m_h.group(1))}</h3>") | |
| elif m_li: | |
| if not in_ul: out.append("<ul>"); in_ul = True | |
| item = re.sub(r"\*\*(.+?)\*\*", r"<strong>\1</strong>", html.escape(m_li.group(1))) | |
| out.append(f"<li>{item}</li>") | |
| elif line.strip(): | |
| if in_ul: out.append("</ul>"); in_ul = False | |
| out.append(f"<p>{line}</p>") | |
| if in_ul: out.append("</ul>") | |
| return "\n".join(out) | |
| def _stat_rows(prof): | |
| r = [("Rows", f"{prof['shape']['rows']:,}"), ("Columns", prof["shape"]["cols"])] | |
| if prof.get("datetime_cols"): | |
| d = prof["datetime_cols"][0] | |
| r.append(("Date range", f"{d['start']} → {d['end']}")) | |
| r.append(("Numeric cols", len(prof["numeric_cols"]))) | |
| r.append(("Categorical cols", len(prof["categorical_cols"]))) | |
| if prof.get("duplicate_rows"): | |
| r.append(("Duplicate rows", prof["duplicate_rows"])) | |
| return "".join(f'<div class="stat"><span class="k">{html.escape(str(k))}</span>' | |
| f'<span class="v">{html.escape(str(v))}</span></div>' for k, v in r) | |
| def render_results(df, name): | |
| prof = profile_df(df, name=name) | |
| charts = build_charts(df, prof) | |
| findings = narrate(prof) | |
| chart_html = "".join( | |
| f'<div class="chart card" style="padding:10px"><img src="{c["img"]}" alt="{html.escape(c["title"])}"></div>' | |
| for c in charts) | |
| flags_html = "" | |
| if prof.get("flags"): | |
| items = "".join(f"<li class='flag'>⚠ {html.escape(f)}</li>" for f in prof["flags"]) | |
| flags_html = f'<div class="card"><div class="muted">Data-quality flags</div><ul>{items}</ul></div>' | |
| return f""" | |
| <div class="sec" style="display:flex;align-items:center;gap:12px;margin:34px 0 6px"> | |
| <span class="num">01</span><span class="name">Overview — {html.escape(name)}</span> | |
| <span class="rule"></span></div> | |
| <div class="grid"> | |
| <div class="card"><div class="muted">Shape</div>{_stat_rows(prof)}</div> | |
| {flags_html} | |
| </div> | |
| <div class="sec" style="display:flex;align-items:center;gap:12px;margin:34px 0 6px"> | |
| <span class="num">02</span><span class="name">Findings</span><span class="rule"></span></div> | |
| <div class="panel findings" style="padding:22px 24px">{md_to_html(findings)}</div> | |
| <div class="sec" style="display:flex;align-items:center;gap:12px;margin:34px 0 6px"> | |
| <span class="num">03</span><span class="name">Charts</span><span class="rule"></span></div> | |
| <div class="grid">{chart_html}</div> | |
| """ | |
| def index(): | |
| return render_template_string(PAGE, body="", backend=BACKEND) | |
| def analyze(): | |
| try: | |
| if request.form.get("sample"): | |
| df = pd.read_csv(SAMPLE) | |
| name = "seattle-weather.csv (sample)" | |
| else: | |
| f = request.files.get("file") | |
| if not f or not f.filename: | |
| return render_template_string(PAGE, backend=BACKEND, | |
| body='<div class="err" style="margin-top:20px">Please choose a .csv file first.</div>') | |
| if not f.filename.lower().endswith(".csv"): | |
| return render_template_string(PAGE, backend=BACKEND, | |
| body='<div class="err" style="margin-top:20px">That doesn\'t look like a .csv file.</div>') | |
| df = pd.read_csv(io.BytesIO(f.read())) | |
| name = os.path.basename(f.filename) | |
| if df.empty or df.shape[1] == 0: | |
| return render_template_string(PAGE, backend=BACKEND, | |
| body='<div class="err" style="margin-top:20px">That file has no rows/columns to analyze.</div>') | |
| return render_template_string(PAGE, body=render_results(df, name), backend=BACKEND) | |
| except Exception as e: | |
| print(f"analyze error: {type(e).__name__}") # TYPE only — never repr/secrets or raw data | |
| return render_template_string(PAGE, backend=BACKEND, | |
| body=f'<div class="err" style="margin-top:20px">Could not read that file as a CSV ' | |
| f'({type(e).__name__}). Try a standard comma-separated file.</div>') | |
| if __name__ == "__main__": | |
| port = int(os.environ.get("PORT", "7860")) | |
| app.run(host="0.0.0.0", port=port, debug=False) | |