AaronTekle's picture
Update app.py
7c08aa4 verified
Raw History Blame Contribute Delete
44.7 kB
from __future__ import annotations
import spaces
import html, json, os, threading, uuid
from pathlib import Path
from typing import Any
import gradio as gr
import pandas as pd
from huggingface_hub import HfApi, hf_hub_download
from agent import DataEngineeringAgent
from config import APP_TITLE, DEFAULT_MAX_TOKENS, HF_MODEL_ID, HF_PROVIDER, HF_TOKEN, MAX_PROFILE_ROWS, MAX_UPLOAD_MB, SUPPORTED_DATA_EXTENSIONS
from data_engine import DataContext
SESSIONS: dict[str, DataContext] = {}
LOCK = threading.Lock()
# Public Hugging Face example dataset used to populate the workspace on startup
EXAMPLE_DATASET_REPO = "Saturia/e-commerce"
EXAMPLE_DATASET_SOURCE = f"hf://datasets/{EXAMPLE_DATASET_REPO}"
def _sid(sid): return sid or uuid.uuid4().hex
def _get(sid):
if not sid: return None
with LOCK: return SESSIONS.get(sid)
def _set(sid, ctx):
with LOCK: SESSIONS[sid] = ctx
def _status(ctx, message="Ready"):
details = "No dataset loaded" if ctx is None else f"{ctx.profile()['rows']:,} rows 路 {ctx.profile()['columns']:,} columns 路 {html.escape(ctx.source_name)}"
state_class = "ready" if ctx is None else "active"
return f'''
<div class="status-card {state_class}">
<div class="status-row">
<span class="status-dot"></span>
<span class="status-kicker">DATASET STATUS</span>
</div>
<strong>{html.escape(message)}</strong>
<span class="status-details">{details}</span>
</div>
'''
def _schema_table(ctx):
return pd.DataFrame([
{
"column": c["name"],
"logical_type": c["logical_type"],
"pandas_dtype": c["pandas_dtype"],
"null_pct": c["null_pct"],
"unique_count": c["unique_count"],
"examples": json.dumps(c["examples"]),
}
for c in ctx.profile()["column_schema"]
])
def load_dataset(uploaded: Any, sid: str | None):
sid = _sid(sid)
if uploaded is None:
return sid, _status(_get(sid), "No file selected"), pd.DataFrame(), pd.DataFrame(), "{}", {}
path = Path(uploaded.name if hasattr(uploaded, "name") else str(uploaded))
if path.suffix.lower() not in SUPPORTED_DATA_EXTENSIONS:
raise gr.Error("Unsupported file type.")
size = path.stat().st_size / 1024 / 1024
if size > MAX_UPLOAD_MB:
raise gr.Error(f"{path.name} is {size:.1f} MB. Limit is {MAX_UPLOAD_MB} MB.")
try:
ctx = DataContext.from_path(path, max_rows=MAX_PROFILE_ROWS)
_set(sid, ctx)
except Exception as exc:
raise gr.Error(f"Could not load {path.name}: {exc}") from exc
inferred = {
"columns": {
c["name"]: {"type": c["logical_type"], "nullable": c["nullable"]}
for c in ctx.profile()["column_schema"]
}
}
diag = {
"source": ctx.source_name,
"rows_loaded": ctx.profile()["rows"],
"columns": ctx.profile()["columns"],
"memory_mb": ctx.profile()["memory_mb"],
"duplicate_rows": ctx.profile()["duplicate_rows"],
"profiling_row_limit": MAX_PROFILE_ROWS,
}
return sid, _status(ctx, "Dataset loaded"), ctx.dataframe.head(20), _schema_table(ctx), json.dumps(inferred, indent=2), diag
def _dataset_outputs(ctx: DataContext, sid: str, message: str, extra_diagnostics: dict[str, Any] | None = None):
inferred = {
"columns": {
c["name"]: {"type": c["logical_type"], "nullable": c["nullable"]}
for c in ctx.profile()["column_schema"]
}
}
diag = {
"source": ctx.source_name,
"rows_loaded": ctx.profile()["rows"],
"columns": ctx.profile()["columns"],
"memory_mb": ctx.profile()["memory_mb"],
"duplicate_rows": ctx.profile()["duplicate_rows"],
"profiling_row_limit": MAX_PROFILE_ROWS,
}
if extra_diagnostics:
diag.update(extra_diagnostics)
return (
sid,
_status(ctx, message),
ctx.dataframe.head(20),
_schema_table(ctx),
json.dumps(inferred, indent=2),
diag,
)
def load_example_dataset(sid: str | None):
"""Load the public Hugging Face example dataset into the current workspace."""
sid = _sid(sid)
try:
# Resolve the public dataset's CSV file dynamically so the app is not coupled to a repository-specific filename
repo_files = HfApi().list_repo_files(
repo_id=EXAMPLE_DATASET_REPO,
repo_type="dataset",
)
csv_files = sorted(
(name for name in repo_files if name.lower().endswith(".csv")),
key=lambda name: (name.count("/"), len(name), name),
)
if not csv_files:
raise RuntimeError("No CSV file was found in the example dataset repository.")
example_file = csv_files[0]
cached_path = hf_hub_download(
repo_id=EXAMPLE_DATASET_REPO,
filename=example_file,
repo_type="dataset",
)
ctx = DataContext.from_path(cached_path, max_rows=MAX_PROFILE_ROWS)
ctx.source_name = f"{EXAMPLE_DATASET_SOURCE}/{example_file}"
_set(sid, ctx)
return _dataset_outputs(
ctx,
sid,
"HF example dataset loaded",
{
"dataset_repo": EXAMPLE_DATASET_REPO,
"dataset_file": example_file,
"dataset_origin": "Hugging Face Datasets",
"preloaded_example": True,
},
)
except Exception as exc:
# Keep the Space usable even if the Hub is temporarily unavailable
return (
sid,
_status(_get(sid), "HF example dataset unavailable"),
pd.DataFrame(),
pd.DataFrame(),
"{}",
{
"dataset_repo": EXAMPLE_DATASET_REPO,
"preloaded_example": False,
"error": str(exc),
},
)
def run_quality(sid):
ctx = _get(sid)
if ctx is None:
raise gr.Error("Load a dataset first.")
q = ctx.quality_report()
return ctx.quality_markdown(), pd.DataFrame(q["issues"]), {
"issue_count": q["issue_count"],
"high": q["high"],
"medium": q["medium"],
"low": q["low"],
}
def validate_schema(schema_json, sid):
ctx = _get(sid)
if ctx is None:
raise gr.Error("Load a dataset first.")
r = ctx.validate_schema(schema_json)
summary = "### Schema validation\n**PASS**\n\n" + r.get("message", "") if r.get("valid") else "### Schema validation\n**FAIL**\n\n" + "\n".join(f"- {x}" for x in r.get("errors", []))
if r.get("warnings"):
summary += "\n\n**Warnings**\n" + "\n".join(f"- {x}" for x in r["warnings"])
return summary, r
@spaces.GPU(duration=120)
def run_agent(task, schema, dialect, target, temperature, max_tokens, sid):
ctx = _get(sid)
if ctx is None:
raise gr.Error("Load a dataset first.")
if not (task or "").strip():
raise gr.Error("Enter an engineering task.")
agent = DataEngineeringAgent(ctx, schema, dialect, target, temperature, int(max_tokens))
answer, sql, py, trace = agent.run(task.strip())
diag = {
"model": HF_MODEL_ID,
"provider": HF_PROVIDER or "HF automatic routing",
"hf_token_configured": bool(HF_TOKEN),
"tool_calls": len(trace),
"tools_used": [x["tool"] for x in trace],
}
return answer, sql, py, trace, diag
def clear_session(sid):
if sid:
with LOCK:
SESSIONS.pop(sid, None)
return "", _status(None, "Session cleared"), pd.DataFrame(), pd.DataFrame(), "{}", "", pd.DataFrame(), "", "", "", [], {}
CSS = r'''
:root {
--bg-0: #05050a;
--bg-1: #080a12;
--bg-2: #0d101b;
--panel: rgba(12, 15, 27, .78);
--panel-strong: rgba(15, 19, 34, .94);
--panel-soft: rgba(255, 255, 255, .035);
--stroke: rgba(255, 255, 255, .09);
--stroke-strong: rgba(255, 255, 255, .16);
--text: #f7f9ff;
--muted: #98a2b6;
--muted-2: #6d778c;
--cyan: #31d7ff;
--cyan-2: #0ea5e9;
--violet: #8b5cf6;
--violet-2: #6d5dfc;
--green: #43e5a0;
--amber: #ffbd59;
--danger: #ff5d78;
--shadow: 0 30px 90px rgba(0, 0, 0, .46);
}
* { box-sizing: border-box; }
.gradio-container,
.gradio-container button,
.gradio-container input,
.gradio-container textarea,
.gradio-container select {
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif !important;
}
html { scroll-behavior: smooth; }
html, body { background: var(--bg-0) !important; }
body { color: var(--text) !important; }
.gradio-container {
max-width: 1580px !important;
margin: 0 auto !important;
min-height: 100vh;
color: var(--text) !important;
background:
radial-gradient(circle at 14% -4%, rgba(49, 215, 255, .16), transparent 32%),
radial-gradient(circle at 83% 6%, rgba(139, 92, 246, .18), transparent 30%),
radial-gradient(circle at 50% 105%, rgba(67, 229, 160, .07), transparent 35%),
linear-gradient(145deg, #05050a 0%, #080b13 46%, #090c16 100%) !important;
position: relative;
}
.gradio-container::before {
content: "";
position: fixed;
inset: 0;
pointer-events: none;
opacity: .18;
background-image:
linear-gradient(rgba(255,255,255,.025) 1px, transparent 1px),
linear-gradient(90deg, rgba(255,255,255,.025) 1px, transparent 1px);
background-size: 44px 44px;
mask-image: linear-gradient(to bottom, black, transparent 85%);
}
.main-shell { padding: 28px 26px 44px; position: relative; z-index: 1; }
.hero {
position: relative;
overflow: hidden;
border: 1px solid rgba(255,255,255,.11);
background:
linear-gradient(130deg, rgba(17,21,38,.96), rgba(8,10,18,.93) 62%, rgba(10,12,22,.96));
border-radius: 28px;
padding: 40px 42px 38px;
box-shadow: var(--shadow), inset 0 1px 0 rgba(255,255,255,.045);
margin-bottom: 18px;
}
.hero::before {
content: "";
position: absolute;
inset: 0 0 auto 0;
height: 1px;
background: linear-gradient(90deg, transparent, var(--cyan), var(--violet), transparent);
box-shadow: 0 0 34px rgba(49, 215, 255, .45);
}
.hero::after {
content: "";
position: absolute;
width: 580px;
height: 580px;
right: -225px;
top: -285px;
border-radius: 50%;
background:
radial-gradient(circle at 48% 48%, rgba(49,215,255,.09), transparent 34%),
conic-gradient(from 0deg, rgba(49,215,255,.18), rgba(139,92,246,.18), rgba(67,229,160,.08), rgba(49,215,255,.18));
filter: blur(2px);
opacity: .8;
}
.hero-grid {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
gap: 26px;
align-items: end;
position: relative;
z-index: 1;
}
.eyebrow {
display: inline-flex;
align-items: center;
gap: 8px;
color: #bdefff;
font-size: 11px;
font-weight: 900;
letter-spacing: .19em;
text-transform: uppercase;
}
.eyebrow-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--green);
box-shadow: 0 0 16px rgba(67,229,160,.75);
}
.hero h1 {
margin: 12px 0 12px;
max-width: 980px;
color: #ffffff;
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
font-size: clamp(42px, 5vw, 68px);
line-height: 1.02;
letter-spacing: -.035em;
font-weight: 950;
font-style: normal;
text-rendering: geometricPrecision;
}
.gradient-word {
color: #ffffff;
background: none;
-webkit-background-clip: initial;
background-clip: initial;
filter: none;
font-style: normal;
font-weight: 950;
}
.hero p {
max-width: 930px;
color: #aab4c9;
font-size: 15px;
line-height: 1.72;
margin: 0;
}
.hero-orbit {
width: 170px;
height: 170px;
border: 1px solid rgba(255,255,255,.08);
border-radius: 28px;
background: linear-gradient(145deg, rgba(49,215,255,.06), rgba(139,92,246,.05));
position: relative;
box-shadow: inset 0 0 45px rgba(49,215,255,.035);
}
.hero-orbit::before,
.hero-orbit::after {
content: "";
position: absolute;
border-radius: 50%;
border: 1px solid rgba(255,255,255,.10);
inset: 24px;
}
.hero-orbit::after { inset: 54px; border-color: rgba(49,215,255,.35); box-shadow: 0 0 26px rgba(49,215,255,.12); }
.hero-orbit-core {
position: absolute;
width: 16px;
height: 16px;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
border-radius: 5px;
background: linear-gradient(135deg, var(--cyan), var(--violet));
box-shadow: 0 0 28px rgba(49,215,255,.6);
}
.badges { display: flex; flex-wrap: wrap; gap: 9px; margin-top: 22px; }
.badge {
border: 1px solid rgba(255,255,255,.105);
background: rgba(255,255,255,.035);
padding: 8px 11px;
border-radius: 999px;
color: #aeb9ce;
font-size: 11px;
backdrop-filter: blur(12px);
}
.badge strong { color: #eefbff; margin-right: 4px; }
.badge-accent { border-color: rgba(49,215,255,.22); background: rgba(49,215,255,.055); color: #c6f3ff; }
.workflow-strip {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 10px;
margin: 0 0 18px;
}
.workflow-step {
position: relative;
min-height: 76px;
border: 1px solid var(--stroke);
border-radius: 17px;
padding: 14px 15px;
background: linear-gradient(145deg, rgba(255,255,255,.038), rgba(255,255,255,.018));
box-shadow: inset 0 1px 0 rgba(255,255,255,.025);
}
.workflow-step span { display: block; color: #68758d; font-size: 10px; font-weight: 900; letter-spacing: .12em; }
.workflow-step strong { display: block; color: #f1f5ff; font-size: 13px; margin-top: 5px; }
.workflow-step small { display: block; color: #7f8ba0; font-size: 11px; margin-top: 3px; }
.workflow-step:nth-child(1) { border-top-color: rgba(49,215,255,.55); }
.workflow-step:nth-child(2) { border-top-color: rgba(67,229,160,.55); }
.workflow-step:nth-child(3) { border-top-color: rgba(255,189,89,.55); }
.workflow-step:nth-child(4) { border-top-color: rgba(139,92,246,.62); }
.app-panel {
background: rgba(10, 13, 24, .78) !important;
border: 1px solid var(--stroke) !important;
border-radius: 24px !important;
box-shadow: 0 24px 65px rgba(0,0,0,.34), inset 0 1px 0 rgba(255,255,255,.028);
/* Gradio dropdown menus are position:fixed and use viewport coordinates.
Keep backdrop-filter off this ancestor so menus stay correctly anchored.
overflow:hidden restores the rounded panel/tab clipping without reintroducing
the dropdown positioning bug. */
overflow: hidden;
backdrop-filter: none;
-webkit-backdrop-filter: none;
padding-top: 0 !important;
}
.side-column { gap: 18px !important; }
.sidebar-card {
background: linear-gradient(145deg, rgba(17,21,37,.88), rgba(10,13,23,.88));
border: 1px solid var(--stroke);
border-radius: 18px;
padding: 22px;
margin-bottom: 0;
box-shadow: inset 0 1px 0 rgba(255,255,255,.025), 0 16px 45px rgba(0,0,0,.20);
}
.sidebar-card + .sidebar-card { margin-top: 20px; }
.sidebar-card h3 { margin: 0 0 18px; color: #fff; font-size: 13px; letter-spacing: -.01em; }
.sidebar-card p, .sidebar-card li { color: var(--muted); font-size: 12px; line-height: 1.82; }
.sidebar-card ol { margin: 16px 0 0; padding-left: 20px; }
.sidebar-card li + li { margin-top: 13px; }
.sidebar-card code { color: #bff4ff; background: rgba(49,215,255,.07); border: 1px solid rgba(49,215,255,.12); padding: 2px 5px; border-radius: 6px; }
.runtime-line { display: flex; align-items: center; justify-content: space-between; gap: 22px; padding: 18px 0; border-bottom: 1px solid rgba(255,255,255,.06); }
.runtime-line:last-child { border-bottom: 0; padding-bottom: 2px; }
.runtime-line span { color: #718099; font-size: 10px; font-weight: 800; letter-spacing: .08em; text-transform: uppercase; min-width: 88px; }
.runtime-line strong { color: #eaf0fb; font-size: 11px; text-align: right; max-width: 66%; overflow-wrap: anywhere; line-height: 1.55; }
.status-card {
border: 1px solid rgba(49,215,255,.16);
background: linear-gradient(145deg, rgba(49,215,255,.065), rgba(139,92,246,.035));
border-radius: 18px;
padding: 16px;
color: #acb8cc;
min-height: 98px;
box-shadow: inset 0 1px 0 rgba(255,255,255,.028), 0 16px 40px rgba(0,0,0,.18);
}
.status-row { display: flex; align-items: center; gap: 7px; margin-bottom: 9px; }
.status-dot { width: 8px; height: 8px; border-radius: 50%; background: #667085; box-shadow: 0 0 0 4px rgba(102,112,133,.08); }
.status-card.active .status-dot { background: var(--green); box-shadow: 0 0 0 4px rgba(67,229,160,.08), 0 0 18px rgba(67,229,160,.55); }
.status-kicker { color: #718099; font-size: 9px; font-weight: 900; letter-spacing: .13em; }
.status-card strong { display: block; color: #fff; font-size: 15px; margin-bottom: 5px; }
.status-details { display: block; color: #8e9aaf; font-size: 11px; line-height: 1.45; }
.section-head {
margin-bottom: 16px;
padding: 2px 2px 0;
}
.section-head .section-kicker { color: var(--cyan); font-size: 10px; font-weight: 900; letter-spacing: .13em; text-transform: uppercase; }
.section-head h2 { margin: 6px 0 5px; font-size: 21px; color: #fff; letter-spacing: -.025em; }
.section-head p { margin: 0; color: #7f8aa0; font-size: 12px; line-height: 1.55; }
#main-tabs [role="tablist"] {
padding: 20px 22px 21px !important;
gap: 14px !important;
min-height: 88px !important;
align-items: center !important;
justify-content: flex-start !important;
border-top: 1px solid rgba(255,255,255,.045) !important;
border-bottom: 2px solid rgba(255,255,255,.10) !important;
background: linear-gradient(180deg, rgba(255,255,255,.028), rgba(255,255,255,.012)) !important;
}
#main-tabs button[role="tab"] {
margin: 0 !important;
padding: 16px 24px !important;
min-height: 54px !important;
min-width: 172px !important;
display: inline-flex !important;
align-items: center !important;
justify-content: center !important;
text-align: center !important;
border: 1px solid rgba(255,255,255,.09) !important;
border-radius: 14px !important;
color: #a7b0c2 !important;
background: rgba(255,255,255,.018) !important;
font-family: "Segoe UI Variable", "Segoe UI", Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, Arial, sans-serif !important;
font-size: 14px !important;
font-weight: 700 !important;
line-height: 1.2 !important;
letter-spacing: -.015em !important;
font-style: normal !important;
transition:
background .18s ease,
border-color .18s ease,
color .18s ease,
box-shadow .18s ease,
transform .18s ease !important;
}
/* Disable Gradio's built-in active underline so it cannot extend past the tab. */
#main-tabs button[role="tab"]::before,
#main-tabs button[role="tab"]::after {
display: none !important;
content: none !important;
width: 0 !important;
height: 0 !important;
border: 0 !important;
box-shadow: none !important;
}
#main-tabs button[role="tab"]:hover {
color: #f4f7ff !important;
background: rgba(255,255,255,.045) !important;
border-color: rgba(255,255,255,.12) !important;
transform: translateY(-1px);
}
#main-tabs button[role="tab"][aria-selected="true"] {
color: #ffffff !important;
background: linear-gradient(135deg, rgba(49,215,255,.13), rgba(139,92,246,.11)) !important;
border-color: rgba(49,215,255,.30) !important;
box-shadow:
inset 0 0 0 1px rgba(255,255,255,.025),
0 8px 22px rgba(0,0,0,.20) !important;
}
.tab-body { padding: 34px 28px 32px !important; }
#load-button, #agent-button, #quality-button, #schema-button {
font-weight: 900 !important;
color: #041017 !important;
background: linear-gradient(135deg, #70e6ff 0%, #31d7ff 42%, #8b7cff 100%) !important;
border: 0 !important;
border-radius: 12px !important;
box-shadow: 0 10px 28px rgba(49,215,255,.15), inset 0 1px 0 rgba(255,255,255,.35) !important;
transition: transform .16s ease, filter .16s ease, box-shadow .16s ease !important;
}
#load-button:hover, #agent-button:hover, #quality-button:hover, #schema-button:hover {
transform: translateY(-1px);
filter: brightness(1.07);
box-shadow: 0 14px 34px rgba(49,215,255,.21), 0 0 28px rgba(139,92,246,.08) !important;
}
#agent-button { min-height: 50px !important; font-size: 14px !important; }
button.secondary, button:not(.primary) { border-radius: 11px !important; }
/* Data Health output heading normalization */
#quality-summary {
margin-top: 20px !important;
padding: 0 !important;
overflow: visible !important;
}
#quality-summary h3 {
margin: 0 0 14px !important;
padding: 0 !important;
color: #ffffff !important;
font-family: "Segoe UI Variable", "Segoe UI", Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, Arial, sans-serif !important;
font-size: 18px !important;
font-weight: 750 !important;
line-height: 1.3 !important;
letter-spacing: -.02em !important;
overflow: visible !important;
}
#quality-summary ul {
margin: 0 !important;
padding-left: 20px !important;
}
#quality-summary li {
margin: 7px 0 !important;
color: #c4ccda !important;
line-height: 1.55 !important;
}
#quality-issues {
margin-top: 20px !important;
overflow: visible !important;
}
#quality-issues [data-testid="block-info"] {
position: relative !important;
inset: auto !important;
transform: none !important;
display: block !important;
width: 100% !important;
margin: 0 0 10px !important;
padding: 0 2px !important;
color: #ffffff !important;
font-family: "Segoe UI Variable", "Segoe UI", Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, Arial, sans-serif !important;
font-size: 13px !important;
font-weight: 700 !important;
line-height: 1.35 !important;
letter-spacing: -.01em !important;
overflow: visible !important;
clip: auto !important;
white-space: normal !important;
}
#quality-issues [data-testid="block-info"] span {
display: inline-block !important;
line-height: 1.35 !important;
overflow: visible !important;
}
/* Schema validation output heading normalization */
#schema-summary {
margin-top: 22px !important;
padding: 0 !important;
overflow: visible !important;
}
#schema-summary h3 {
margin: 0 0 14px !important;
padding: 0 !important;
color: #ffffff !important;
font-family: "Segoe UI Variable", "Segoe UI", Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, Arial, sans-serif !important;
font-size: 18px !important;
font-weight: 750 !important;
line-height: 1.3 !important;
letter-spacing: -.02em !important;
overflow: visible !important;
}
#schema-summary p {
margin: 8px 0 !important;
color: #c4ccda !important;
line-height: 1.6 !important;
}
#schema-summary strong {
color: #ffffff !important;
font-weight: 800 !important;
}
#schema-summary ul {
margin: 10px 0 0 !important;
padding-left: 20px !important;
}
#schema-summary li {
margin: 7px 0 !important;
color: #c4ccda !important;
line-height: 1.55 !important;
}
/* Gradio form surfaces */
.gradio-container textarea,
.gradio-container input,
.gradio-container select {
color: #eef3ff !important;
}
.gradio-container .wrap,
.gradio-container .form,
.gradio-container [data-testid="block-info"] { border-color: rgba(255,255,255,.075) !important; }
.gradio-container .block {
border-radius: 15px !important;
}
.gradio-container label span { color: #9ca8bc !important; }
/* File upload dropzone */
.gradio-container [data-testid="file-upload"] {
border: 1px dashed rgba(49,215,255,.23) !important;
background: linear-gradient(145deg, rgba(49,215,255,.035), rgba(139,92,246,.025)) !important;
border-radius: 16px !important;
}
/* Dataset dataframe header fix only */
#dataset-preview,
#dataset-schema {
margin-top: 18px !important;
overflow: visible !important;
}
#dataset-preview [data-testid="block-info"],
#dataset-schema [data-testid="block-info"] {
position: relative !important;
inset: auto !important;
transform: none !important;
display: block !important;
width: 100% !important;
margin: 0 0 9px !important;
padding: 0 2px !important;
line-height: 1.35 !important;
overflow: visible !important;
clip: auto !important;
white-space: normal !important;
}
#dataset-preview [data-testid="block-info"] span,
#dataset-schema [data-testid="block-info"] span {
display: inline-block !important;
line-height: 1.35 !important;
overflow: visible !important;
}
/* Tables */
.gradio-container table { border-collapse: separate !important; border-spacing: 0 !important; }
.gradio-container th {
background: #111626 !important;
color: #c9d3e6 !important;
border-color: rgba(255,255,255,.07) !important;
font-size: 11px !important;
}
.gradio-container td {
background: rgba(9,12,22,.84) !important;
color: #abb7cb !important;
border-color: rgba(255,255,255,.055) !important;
font-size: 11px !important;
}
/* Nested result tabs */
.result-tabs [role="tablist"] { gap: 6px !important; margin-top: 8px !important; }
.result-tabs button[role="tab"] {
border-radius: 9px !important;
font-weight: 800 !important;
}
.agent-answer {
border: 1px solid rgba(139,92,246,.14) !important;
background: linear-gradient(145deg, rgba(139,92,246,.035), rgba(49,215,255,.025)) !important;
border-radius: 16px !important;
padding: 6px 16px 12px !important;
margin-top: 4px !important;
}
.warning-note {
margin-top: 15px;
padding: 12px 14px;
border: 1px solid rgba(255,189,89,.18);
border-left: 3px solid var(--amber);
border-radius: 12px;
background: rgba(255,189,89,.045);
color: #b9b3a5;
font-size: 11px;
line-height: 1.55;
}
.footer-note {
color: #5f6b80;
font-size: 10px;
text-align: center;
margin-top: 18px;
letter-spacing: .05em;
}
/* =========================================================
VISUAL-ONLY POLISH
No textual content or application logic is changed.
========================================================= */
/* Ambient hero motion */
.hero::after {
animation: heroGlowDrift 14s ease-in-out infinite alternate;
transform-origin: center;
}
@keyframes heroGlowDrift {
0% { transform: translate3d(0, 0, 0) scale(1); opacity: .68; }
50% { transform: translate3d(-20px, 14px, 0) scale(1.035); opacity: .88; }
100% { transform: translate3d(12px, -8px, 0) scale(.985); opacity: .72; }
}
/* Animated orbital graphic */
.hero-orbit {
overflow: hidden;
transition: transform .35s ease, border-color .35s ease, box-shadow .35s ease;
}
.hero-orbit:hover {
transform: translateY(-3px) scale(1.015);
border-color: rgba(49,215,255,.22);
box-shadow:
inset 0 0 55px rgba(49,215,255,.055),
0 18px 50px rgba(0,0,0,.24);
}
.hero-orbit::before {
animation: orbitSpin 13s linear infinite;
}
.hero-orbit::after {
animation: orbitSpinReverse 8s linear infinite;
}
.hero-orbit-core {
animation: corePulse 2.8s ease-in-out infinite;
}
@keyframes orbitSpin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
@keyframes orbitSpinReverse {
from { transform: rotate(360deg); }
to { transform: rotate(0deg); }
}
@keyframes corePulse {
0%,100% {
transform: translate(-50%, -50%) scale(1);
box-shadow: 0 0 20px rgba(49,215,255,.50);
}
50% {
transform: translate(-50%, -50%) scale(1.14);
box-shadow:
0 0 28px rgba(49,215,255,.76),
0 0 42px rgba(139,92,246,.20);
}
}
/* Badge interaction */
.badge {
position: relative;
overflow: hidden;
transition:
transform .18s ease,
border-color .18s ease,
background .18s ease,
color .18s ease;
}
.badge:hover {
transform: translateY(-2px);
border-color: rgba(49,215,255,.24);
background: rgba(49,215,255,.055);
color: #dff8ff;
}
/* Workflow cards gain depth and a subtle pipeline connector */
.workflow-strip {
position: relative;
}
.workflow-step {
overflow: hidden;
transition:
transform .20s ease,
border-color .20s ease,
background .20s ease,
box-shadow .20s ease;
}
.workflow-step::after {
content: "";
position: absolute;
left: 0;
right: 0;
bottom: 0;
height: 1px;
background: linear-gradient(90deg, transparent, rgba(49,215,255,.32), transparent);
opacity: 0;
transform: scaleX(.35);
transition: opacity .20s ease, transform .20s ease;
}
.workflow-step:hover {
transform: translateY(-3px);
background: linear-gradient(145deg, rgba(255,255,255,.052), rgba(255,255,255,.022));
border-color: rgba(255,255,255,.15);
box-shadow:
inset 0 1px 0 rgba(255,255,255,.04),
0 12px 30px rgba(0,0,0,.20);
}
.workflow-step:hover::after {
opacity: 1;
transform: scaleX(1);
}
/* Main application panel gets a faint inner glow */
.app-panel {
position: relative;
}
.app-panel::before {
content: "";
position: absolute;
inset: 0;
pointer-events: none;
border-radius: inherit;
/* Keep the glass effect on a non-ancestor layer so fixed dropdowns stay viewport-anchored. */
backdrop-filter: blur(18px);
-webkit-backdrop-filter: blur(18px);
box-shadow:
inset 0 0 60px rgba(49,215,255,.018),
inset 0 0 90px rgba(139,92,246,.012);
z-index: 0;
}
#main-tabs {
position: relative;
z-index: 1;
}
/* Active tab gets a restrained glow */
#main-tabs button[role="tab"][aria-selected="true"] {
box-shadow:
inset 0 0 0 1px rgba(255,255,255,.025),
0 8px 22px rgba(0,0,0,.20),
0 0 18px rgba(49,215,255,.055) !important;
}
/* Sidebar cards feel less static */
.sidebar-card {
position: relative;
overflow: hidden;
transition:
transform .20s ease,
border-color .20s ease,
box-shadow .20s ease;
}
.sidebar-card::before {
content: "";
position: absolute;
width: 140px;
height: 140px;
right: -85px;
top: -85px;
border-radius: 50%;
background: radial-gradient(circle, rgba(49,215,255,.075), transparent 68%);
pointer-events: none;
opacity: .7;
}
.sidebar-card:hover {
transform: translateY(-2px);
border-color: rgba(49,215,255,.15);
box-shadow:
inset 0 1px 0 rgba(255,255,255,.035),
0 18px 45px rgba(0,0,0,.24);
}
/* Workspace status pulse */
.status-card .status-dot {
animation: statusPulse 3s ease-in-out infinite;
}
.status-card.active .status-dot {
animation: statusActivePulse 2.2s ease-in-out infinite;
}
@keyframes statusPulse {
0%,100% { opacity: .65; transform: scale(1); }
50% { opacity: 1; transform: scale(1.12); }
}
@keyframes statusActivePulse {
0%,100% {
transform: scale(1);
box-shadow: 0 0 0 4px rgba(67,229,160,.08), 0 0 12px rgba(67,229,160,.40);
}
50% {
transform: scale(1.13);
box-shadow: 0 0 0 6px rgba(67,229,160,.06), 0 0 22px rgba(67,229,160,.62);
}
}
/* Primary button sheen */
#load-button,
#agent-button,
#quality-button,
#schema-button {
position: relative;
overflow: hidden;
}
#load-button::after,
#agent-button::after,
#quality-button::after,
#schema-button::after {
content: "";
position: absolute;
top: -45%;
left: -38%;
width: 28%;
height: 190%;
transform: rotate(18deg);
background: linear-gradient(
90deg,
transparent,
rgba(255,255,255,.42),
transparent
);
opacity: 0;
pointer-events: none;
}
#load-button:hover::after,
#agent-button:hover::after,
#quality-button:hover::after,
#schema-button:hover::after {
opacity: .75;
animation: buttonSheen .75s ease forwards;
}
@keyframes buttonSheen {
from { left: -38%; }
to { left: 112%; }
}
/* Upload zone becomes more tactile */
.gradio-container [data-testid="file-upload"] {
transition:
border-color .22s ease,
background .22s ease,
box-shadow .22s ease,
transform .22s ease;
}
.gradio-container [data-testid="file-upload"]:hover {
border-color: rgba(49,215,255,.42) !important;
background:
linear-gradient(145deg, rgba(49,215,255,.055), rgba(139,92,246,.035)) !important;
box-shadow:
inset 0 0 34px rgba(49,215,255,.025),
0 10px 26px rgba(0,0,0,.14);
}
/* Data tables get a subtle row hover */
.gradio-container tbody tr {
transition: background .16s ease;
}
.gradio-container tbody tr:hover td {
background: rgba(49,215,255,.035) !important;
color: #d7e2f5 !important;
}
/* Accordions feel interactive without changing layout */
.gradio-container details {
transition:
border-color .20s ease,
box-shadow .20s ease,
background .20s ease;
}
.gradio-container details:hover {
border-color: rgba(49,215,255,.13) !important;
box-shadow: 0 10px 28px rgba(0,0,0,.12);
}
/* Respect reduced-motion preferences */
@media (prefers-reduced-motion: reduce) {
.hero::after,
.hero-orbit::before,
.hero-orbit::after,
.hero-orbit-core,
.status-card .status-dot,
.status-card.active .status-dot {
animation: none !important;
}
#load-button:hover::after,
#agent-button:hover::after,
#quality-button:hover::after,
#schema-button:hover::after {
animation: none !important;
}
.workflow-step,
.sidebar-card,
.badge,
.hero-orbit {
transition: none !important;
}
}
@media (max-width: 980px) {
.hero-grid { grid-template-columns: 1fr; }
.hero-orbit { display: none; }
.workflow-strip { grid-template-columns: repeat(2, minmax(0,1fr)); }
}
@media (max-width: 800px) {
.main-shell { padding: 14px 10px 28px; }
.hero { padding: 28px 21px 26px; border-radius: 22px; }
.hero h1 { font-size: 42px; }
.workflow-strip { grid-template-columns: 1fr 1fr; }
.tab-body { padding: 20px 15px 22px !important; }
#main-tabs [role="tablist"] { padding: 14px !important; min-height: 74px !important; gap: 10px !important; overflow-x: auto; flex-wrap: nowrap !important; }
#main-tabs button[role="tab"] { min-width: 160px !important; padding: 14px 20px !important; }
}
@media (max-width: 560px) {
.hero h1 { font-size: 36px; }
.workflow-strip { grid-template-columns: 1fr; }
}
'''
HEAD = '''
<meta name="theme-color" content="#070913">
<meta name="description" content="Modern agentic data engineering workspace for profiling, schema validation, quality auditing, SQL, and PySpark generation.">
'''
def build_app():
runtime = f"HF Inference 路 {HF_MODEL_ID}" if HF_TOKEN else "Qwen/Qwen3-Coder-30B-A3B-Instruct"
provider = HF_PROVIDER or "HF automatic routing"
hero = f'''
<div class="hero">
<div class="hero-grid">
<div>
<div class="eyebrow"><span class="eyebrow-dot"></span> Agentic Data Engineering - SQL & PySpark</div>
<h1>Agentic Data Engineer</h1>
<p>Use this Data Engineering AI agent to audit datasets, find data quality issues, check data constraints, and generate SQL and PySpark pipelines to fix them.</p>
<div class="badges">
<span class="badge badge-accent"><strong>Agent</strong> HF tool calling</span>
<span class="badge"><strong>Model</strong> {html.escape(HF_MODEL_ID)}</span>
<span class="badge"><strong>SQL</strong> DuckDB + SQLGlot</span>
<span class="badge"><strong>Output</strong> PySpark + SQL</span>
</div>
</div>
<div class="hero-orbit" aria-hidden="true"><div class="hero-orbit-core"></div></div>
</div>
</div>
'''
workflow = '''
<div class="workflow-strip">
<div class="workflow-step"><span>01 路 INGEST</span><strong>load dataset</strong><small>CSV 路 Parquet 路 JSON 路 Excel</small></div>
<div class="workflow-step"><span>02 路 ANALYZE</span><strong>data profiling + audit</strong><small>schema 路 nulls 路 duplicates 路 quality</small></div>
<div class="workflow-step"><span>03 路 VALIDATE</span><strong>data validation</strong><small>Expected schema + warnings</small></div>
<div class="workflow-step"><span>04 路 BUILD</span><strong>generate pipeline</strong><small>SQL 路 PySpark 路 tool trace</small></div>
</div>
'''
with gr.Blocks(title=APP_TITLE) as demo:
sid = gr.State("")
with gr.Column(elem_classes=["main-shell"]):
gr.HTML(hero)
gr.HTML(workflow)
with gr.Row(equal_height=False):
with gr.Column(scale=9, min_width=590, elem_classes=["app-panel"]):
with gr.Tabs(selected="load", elem_id="main-tabs"):
with gr.Tab("01. Dataset", id="load"):
with gr.Column(elem_classes=["tab-body"]):
gr.HTML('''<div class="section-head"><div class="section-kicker">Dataset workspace</div><h2>Load Your Data In</h2><p>Upload a supported dataset file and generate a fast structural profile before running the agent</p></div>''')
upload = gr.File(
label="Dataset file",
file_types=[".csv", ".parquet", ".json", ".jsonl", ".xlsx", ".xls"],
)
with gr.Row():
load_btn = gr.Button("Load + Profile Dataset", variant="primary", elem_id="load-button")
example_btn = gr.Button("Load HF Example Dataset")
clear_btn = gr.Button("Clear Workspace")
preview = gr.Dataframe(label="Data preview", interactive=False, wrap=True, elem_id="dataset-preview")
schema_table = gr.Dataframe(label="Inferred schema", interactive=False, wrap=True, elem_id="dataset-schema")
gr.HTML("<div class='warning-note'><strong>Public Space:</strong> do not upload confidential, regulated, proprietary, production customer, or PII.</div>")
with gr.Tab("02. Data Quality (Audits)", id="quality"):
with gr.Column(elem_classes=["tab-body"]):
gr.HTML('''<div class="section-head"><div class="section-kicker">Data Auditing</div><h2>Data Quality (audits)</h2><p>Run data quality checks first, then compare the dataset against an editable expected schema</p></div>''')
with gr.Row():
quality_btn = gr.Button("Run Quality Audit", variant="primary", elem_id="quality-button")
schema_btn = gr.Button("Validate Schema", variant="primary", elem_id="schema-button")
quality_summary = gr.Markdown("", elem_id="quality-summary")
quality_table = gr.Dataframe(label="Detected issues", interactive=False, wrap=True, elem_id="quality-issues")
expected_schema = gr.Code(label="Expected schema JSON", language="json", value="{}", lines=16)
schema_summary = gr.Markdown("", elem_id="schema-summary")
schema_result = gr.JSON(label="Schema validation details")
with gr.Tab("03. Data Engineering Agent", id="agent"):
with gr.Column(elem_classes=["tab-body"]):
gr.HTML('''<div class="section-head"><div class="section-kicker">Data Engineering Agent</div><h2>Turn Findings Into Pipeline Code</h2><p>Describe the engineering outcome. the agent can inspect the dataset, call data quality tools, validate SQL, and generate relevant code</p></div>''')
task = gr.Textbox(
label="Engineering task",
placeholder="Example: Audit this dataset, identify the highest-risk quality issues, then generate an idempotent SQL cleanup pipeline and a PySpark equivalent.",
lines=5,
max_lines=10,
)
with gr.Row():
dialect = gr.Dropdown(
choices=["duckdb", "spark", "postgres", "mysql", "snowflake", "bigquery", "tsql"],
value="duckdb",
label="SQL dialect",
)
target = gr.Dropdown(
choices=["SQL + PySpark", "SQL only", "PySpark only"],
value="SQL + PySpark",
label="Target output",
)
agent_btn = gr.Button("Run Data Engineering Agent", variant="primary", elem_id="agent-button")
answer = gr.Markdown("")
with gr.Tabs(elem_classes=["result-tabs"]):
with gr.Tab("SQL Pipeline"):
sql_out = gr.Code(label="Generated SQL", language="sql", lines=22)
with gr.Tab("PySpark Pipeline"):
py_out = gr.Code(label="Generated PySpark", language="python", lines=22)
with gr.Tab("Tool Trace"):
trace = gr.JSON(label="Agent tool calls and observations")
with gr.Column(scale=4, min_width=330, elem_classes=["side-column"]):
status = gr.HTML(_status(None))
gr.HTML(f'''
<div class="sidebar-card">
<h3>Runtime</h3>
<div class="runtime-line"><span>Base Model path</span><strong>{html.escape(runtime)}</strong></div>
<div class="runtime-line"><span>Provider</span><strong>{html.escape(provider)}</strong></div>
<div class="runtime-line"><span>Execution</span><strong>DuckDB sandbox</strong></div>
</div>
<div class="sidebar-card">
<h3>Agent Capabilities</h3>
<ol>
<li>Inspect shape, types, nulls, cardinality, and examples</li>
<li>Run data-quality checks</li>
<li>Validate the expected schema contract</li>
<li>Validate and execute safe read-only SQL against <code>dataset</code></li>
<li>Generate SQL and PySpark remediation pipelines</li>
</ol>
</div>
<div class="sidebar-card">
<h3>Execution Model</h3>
<p>DuckDB runs lightweight validation inside the Space. PySpark is generated as deployable target code, so the UI stays fast without carrying a Spark JVM runtime.</p>
</div>
''')
with gr.Accordion("Agent controls", open=True):
temperature = gr.Slider(0.0, .8, value=.15, step=.05, label="Temperature")
max_tokens = gr.Slider(600, 3200, value=DEFAULT_MAX_TOKENS, step=100, label="Maximum model tokens")
with gr.Accordion("Diagnostics", open=False):
diagnostics = gr.JSON(value={}, label="Runtime diagnostics")
gr.HTML("<div class='footer-note'>QWEN3-CODER 路 HUGGING FACE INFERENCE 路 DUCKDB 路 SQLGLOT 路 GRADIO</div>")
dataset_outputs = [sid, status, preview, schema_table, expected_schema, diagnostics]
# Preload the HF example when a new browser session opens.
demo.load(
load_example_dataset,
inputs=[sid],
outputs=dataset_outputs,
)
# Users can still replace the example with their own upload at any time
load_btn.click(load_dataset, [upload, sid], dataset_outputs)
example_btn.click(load_example_dataset, [sid], dataset_outputs)
quality_btn.click(run_quality, [sid], [quality_summary, quality_table, diagnostics])
schema_btn.click(validate_schema, [expected_schema, sid], [schema_summary, schema_result])
agent_btn.click(run_agent, [task, expected_schema, dialect, target, temperature, max_tokens, sid], [answer, sql_out, py_out, trace, diagnostics])
task.submit(run_agent, [task, expected_schema, dialect, target, temperature, max_tokens, sid], [answer, sql_out, py_out, trace, diagnostics])
clear_btn.click(clear_session, [sid], [sid, status, preview, schema_table, expected_schema, quality_summary, quality_table, schema_summary, sql_out, py_out, trace, diagnostics])
return demo
if __name__ == "__main__":
app = build_app()
app.queue(default_concurrency_limit=2).launch(
server_name="0.0.0.0",
server_port=int(os.getenv("PORT", "7860")),
show_error=True,
ssr_mode=False,
theme=gr.themes.Base(),
css=CSS,
head=HEAD,
)