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'''
DATASET STATUS
{html.escape(message)}
{details}
'''
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 = '''
'''
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'''
Agentic Data Engineering - SQL & PySpark
Agentic Data Engineer
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.
Agent HF tool calling
Model {html.escape(HF_MODEL_ID)}
SQL DuckDB + SQLGlot
Output PySpark + SQL
'''
workflow = '''
01 · INGESTload datasetCSV · Parquet · JSON · Excel
02 · ANALYZEdata profiling + auditschema · nulls · duplicates · quality
03 · VALIDATEdata validationExpected schema + warnings
04 · BUILDgenerate pipelineSQL · PySpark · tool trace
'''
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('''Dataset workspace
Load Your Data In
Upload a supported dataset file and generate a fast structural profile before running the agent
''')
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("Public Space: do not upload confidential, regulated, proprietary, production customer, or PII.
")
with gr.Tab("02. Data Quality (Audits)", id="quality"):
with gr.Column(elem_classes=["tab-body"]):
gr.HTML('''Data Auditing
Data Quality (audits)
Run data quality checks first, then compare the dataset against an editable expected schema
''')
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('''Data Engineering Agent
Turn Findings Into Pipeline Code
Describe the engineering outcome. the agent can inspect the dataset, call data quality tools, validate SQL, and generate relevant code
''')
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'''
''')
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("")
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,
)