Spaces:
Sleeping
Sleeping
File size: 8,461 Bytes
e2f9a1d | 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 | #!/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>
"""
@app.route("/")
def index():
return render_template_string(PAGE, body="", backend=BACKEND)
@app.route("/analyze", methods=["POST"])
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)
|