Chupacharcos commited on
Commit
fbb2da8
·
verified ·
1 Parent(s): 1e3e9ac

Upload app.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. app.py +175 -0
app.py ADDED
@@ -0,0 +1,175 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Voice-to-SQL — pregúntale a una base de datos SaaS de ejemplo en lenguaje natural.
3
+ Un LLM (Llama 3.3 70B vía Groq) traduce la pregunta a SQL, se valida que sea de
4
+ SOLO LECTURA y se ejecuta sobre una BD SQLite generada en memoria.
5
+
6
+ Demo completa (con voz, Web Speech API) en https://adrianmoreno-dev.com/demo/voice-to-sql-dashboard
7
+ Por Adrián Moreno · https://adrianmoreno-dev.com
8
+ """
9
+ import os, re, sqlite3, random
10
+ from datetime import datetime, timedelta
11
+
12
+ import requests
13
+ import pandas as pd
14
+ import gradio as gr
15
+
16
+ GROQ_KEY = os.environ.get("GROQ_API_KEY", "")
17
+ MODELS = ["llama-3.3-70b-versatile", "llama-3.1-8b-instant"]
18
+
19
+ SCHEMA_DESC = """TABLAS DISPONIBLES (esquema exacto, NO inventar columnas):
20
+
21
+ clientes (200 filas)
22
+ - id INTEGER PRIMARY KEY
23
+ - nombre TEXT
24
+ - pais TEXT -- "España"|"Francia"|"Italia"|"Alemania"|"Reino Unido"|"Portugal"|"México"|"Argentina"|"Chile"|"Colombia"
25
+ - sector TEXT -- "E-commerce"|"SaaS"|"FinTech"|"EdTech"|"HealthTech"|"MarTech"|"PropTech"|"AgriTech"
26
+ - plan TEXT -- "free"|"starter"|"pro"|"enterprise"
27
+ - fecha_alta DATE -- ISO YYYY-MM-DD
28
+ - activo INTEGER -- 1=activo, 0=churn
29
+
30
+ productos (8 filas)
31
+ - id INTEGER PRIMARY KEY
32
+ - nombre TEXT
33
+ - tier TEXT -- "addon"|"core"|"premium"
34
+ - precio_mensual REAL -- en EUR
35
+
36
+ ventas (5000 filas, último año)
37
+ - id INTEGER PRIMARY KEY
38
+ - cliente_id INTEGER → clientes.id
39
+ - producto_id INTEGER → productos.id
40
+ - fecha DATE -- ISO YYYY-MM-DD
41
+ - importe REAL -- EUR pagados
42
+ - mrr_aporte REAL -- contribución MRR
43
+
44
+ REGLAS:
45
+ - SQLite dialect (NOT PostgreSQL/MySQL).
46
+ - Para "últimos N meses" usa: date('now', '-N months')
47
+ - Para agrupar por mes: strftime('%Y-%m', fecha)
48
+ - Si la pregunta es ambigua, asume la interpretación más útil para un dashboard analítico."""
49
+
50
+ SYSTEM_PROMPT = (
51
+ "Eres un experto en SQLite que convierte preguntas en lenguaje natural en "
52
+ "consultas SQL ejecutables. Responde ÚNICAMENTE con la consulta SQL, sin "
53
+ "explicación, sin markdown, sin punto y coma final, sin prefijos.\n\n"
54
+ + SCHEMA_DESC
55
+ + "\n\nREGLA CRÍTICA: solo SELECT. Nunca UPDATE/INSERT/DELETE/DROP."
56
+ )
57
+
58
+
59
+ def build_db():
60
+ con = sqlite3.connect(":memory:", check_same_thread=False)
61
+ con.executescript("""
62
+ CREATE TABLE clientes (id INTEGER PRIMARY KEY, nombre TEXT, pais TEXT, sector TEXT,
63
+ plan TEXT, fecha_alta DATE, activo INTEGER DEFAULT 1);
64
+ CREATE TABLE productos (id INTEGER PRIMARY KEY, nombre TEXT, tier TEXT, precio_mensual REAL);
65
+ CREATE TABLE ventas (id INTEGER PRIMARY KEY, cliente_id INTEGER, producto_id INTEGER,
66
+ fecha DATE, importe REAL, mrr_aporte REAL);
67
+ """)
68
+ paises = ["España", "Francia", "Italia", "Alemania", "Reino Unido", "Portugal",
69
+ "México", "Argentina", "Chile", "Colombia"]
70
+ sectores = ["E-commerce", "SaaS", "FinTech", "EdTech", "HealthTech", "MarTech", "PropTech", "AgriTech"]
71
+ planes = ["free", "starter", "pro", "enterprise"]
72
+ productos = [("Plan Free", "core", 0), ("Plan Starter", "core", 29), ("Plan Pro", "core", 99),
73
+ ("Plan Enterprise", "core", 499), ("Add-on Analytics", "addon", 19),
74
+ ("Add-on API Plus", "addon", 39), ("Add-on White Label", "premium", 149),
75
+ ("Add-on Priority Support", "premium", 79)]
76
+ con.executemany("INSERT INTO productos (nombre,tier,precio_mensual) VALUES (?,?,?)", productos)
77
+ empresas = ["Acme", "Globex", "Initech", "Umbrella", "Stark", "Wayne", "Pied Piper", "Hooli",
78
+ "Soylent", "Cyberdyne", "Tyrell", "Weyland", "Aperture", "Oscorp", "LexCorp", "Wonka"]
79
+ sufijos = ["Tech", "Labs", "Group", "Solutions", "Systems", "Digital", "Cloud", "Studio"]
80
+ hoy = datetime.now().date()
81
+ for i in range(1, 201):
82
+ con.execute("INSERT INTO clientes (nombre,pais,sector,plan,fecha_alta,activo) VALUES (?,?,?,?,?,?)",
83
+ (f"{random.choice(empresas)} {random.choice(sufijos)} {i}", random.choice(paises),
84
+ random.choice(sectores), random.choices(planes, weights=[0.4, 0.3, 0.2, 0.1])[0],
85
+ (hoy - timedelta(days=random.randint(7, 730))).isoformat(),
86
+ 1 if random.random() > 0.12 else 0))
87
+ precios = [p[2] for p in productos]
88
+ ventas = []
89
+ for _ in range(5000):
90
+ pid = random.randint(1, 8)
91
+ importe = round(precios[pid - 1] * random.uniform(0.85, 1.0), 2)
92
+ ventas.append((random.randint(1, 200), pid,
93
+ (hoy - timedelta(days=random.randint(1, 365))).isoformat(), importe, importe))
94
+ con.executemany("INSERT INTO ventas (cliente_id,producto_id,fecha,importe,mrr_aporte) VALUES (?,?,?,?,?)", ventas)
95
+ con.commit()
96
+ return con
97
+
98
+
99
+ CON = build_db()
100
+
101
+
102
+ def generate_sql(question: str) -> str:
103
+ if not GROQ_KEY:
104
+ raise RuntimeError("Falta GROQ_API_KEY (secreto del Space).")
105
+ last = ""
106
+ for model in MODELS:
107
+ try:
108
+ r = requests.post(
109
+ "https://api.groq.com/openai/v1/chat/completions",
110
+ headers={"Authorization": f"Bearer {GROQ_KEY}"},
111
+ json={"model": model, "temperature": 0, "max_tokens": 400,
112
+ "messages": [{"role": "system", "content": SYSTEM_PROMPT},
113
+ {"role": "user", "content": question}]},
114
+ timeout=30)
115
+ if r.status_code == 200:
116
+ sql = r.json()["choices"][0]["message"]["content"].strip()
117
+ sql = re.sub(r"^```\w*", "", sql).replace("```", "").strip().rstrip(";").strip()
118
+ return sql
119
+ last = f"HTTP {r.status_code}"
120
+ except Exception as e:
121
+ last = str(e)
122
+ raise RuntimeError(f"Groq no disponible ({last})")
123
+
124
+
125
+ def sanitize(sql: str) -> str:
126
+ low = sql.strip().lower()
127
+ if not (low.startswith("select") or low.startswith("with")):
128
+ raise ValueError("Solo se permiten consultas SELECT.")
129
+ if re.search(r"\b(insert|update|delete|drop|alter|create|pragma|attach|replace|vacuum)\b", low):
130
+ raise ValueError("Solo lectura: palabra prohibida detectada.")
131
+ if ";" in sql.strip().rstrip(";"):
132
+ raise ValueError("Solo una sentencia.")
133
+ return sql
134
+
135
+
136
+ def ask(question: str):
137
+ if not question or len(question.strip()) < 3:
138
+ return "-- Escribe una pregunta", None
139
+ try:
140
+ sql = generate_sql(question)
141
+ sanitize(sql)
142
+ cur = CON.execute(sql)
143
+ cols = [d[0] for d in cur.description]
144
+ df = pd.DataFrame(cur.fetchmany(100), columns=cols)
145
+ return sql, df
146
+ except Exception as e:
147
+ return f"-- Error: {e}", None
148
+
149
+
150
+ with gr.Blocks(title="Voice-to-SQL", theme=gr.themes.Soft(primary_hue="teal")) as demo:
151
+ gr.Markdown(
152
+ "# 🗣️→🗄️ Voice-to-SQL — pregúntale a tu base de datos\n"
153
+ "Escribe una pregunta de negocio en **lenguaje natural** y un LLM (Llama 3.3 70B) la traduce "
154
+ "a SQL, la valida (solo lectura) y la ejecuta sobre una base de datos **SaaS de ejemplo** "
155
+ "(200 clientes · 8 planes · 5 000 ventas).\n\n"
156
+ "👉 Versión con **voz** (Web Speech API) y más en "
157
+ "[adrianmoreno-dev.com](https://adrianmoreno-dev.com/demo/voice-to-sql-dashboard?utm_source=huggingface)"
158
+ )
159
+ q = gr.Textbox(label="Tu pregunta", placeholder="¿Cuáles son los 5 productos más vendidos?", lines=1)
160
+ btn = gr.Button("Consultar", variant="primary")
161
+ sql_out = gr.Code(label="SQL generado", language="sql")
162
+ df_out = gr.Dataframe(label="Resultados", wrap=True)
163
+ gr.Examples(
164
+ ["¿Cuáles son los 5 productos más vendidos?",
165
+ "Ingresos totales por país",
166
+ "MRR medio por plan",
167
+ "¿Cuántos clientes han hecho churn?",
168
+ "Top 10 clientes por importe total gastado"],
169
+ inputs=q)
170
+ btn.click(ask, q, [sql_out, df_out])
171
+ q.submit(ask, q, [sql_out, df_out])
172
+ gr.Markdown("<sub>Proyecto open source de **Adrián Moreno** · [portfolio](https://adrianmoreno-dev.com?utm_source=huggingface) · solo SELECT, base de datos sintética</sub>")
173
+
174
+ if __name__ == "__main__":
175
+ demo.launch()