#!/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 = """ CSV → Insights · GritAI
GritAI · CSV → Insights
Upload data · charts + plain-English findings
#12 · 30-in-15

Turn a spreadsheet into a briefing.

Every statistic and chart is computed directly from your file. The AI only writes the story — it never invents a number.

Analyze a CSV
{{ body|safe }}

Backend: {{ backend }} · Local GPU fleet by default · GritAI Solutions

""" 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"\1", line) line = re.sub(r"`(.+?)`", r"\1", 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(""); in_ul = False out.append(f"

{html.escape(m_h.group(1))}

") elif m_li: if not in_ul: out.append(""); in_ul = False out.append(f"

{line}

") if in_ul: out.append("") 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'
{html.escape(str(k))}' f'{html.escape(str(v))}
' 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'
{html.escape(c[
' for c in charts) flags_html = "" if prof.get("flags"): items = "".join(f"
  • ⚠ {html.escape(f)}
  • " for f in prof["flags"]) flags_html = f'
    Data-quality flags
    ' return f"""
    01Overview — {html.escape(name)}
    Shape
    {_stat_rows(prof)}
    {flags_html}
    02Findings
    {md_to_html(findings)}
    03Charts
    {chart_html}
    """ @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='
    Please choose a .csv file first.
    ') if not f.filename.lower().endswith(".csv"): return render_template_string(PAGE, backend=BACKEND, body='
    That doesn\'t look like a .csv file.
    ') 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='
    That file has no rows/columns to analyze.
    ') 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'
    Could not read that file as a CSV ' f'({type(e).__name__}). Try a standard comma-separated file.
    ') if __name__ == "__main__": port = int(os.environ.get("PORT", "7860")) app.run(host="0.0.0.0", port=port, debug=False)