File size: 9,391 Bytes
2454510
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
"""
Text-to-SQL Trace RAG — Gradio Prototype (HuggingFace Spaces, no Docker)
--------------------------------------------------------------------------
Same trace RAG logic as before, wrapped in a Gradio UI so it deploys on
HF Spaces' free Gradio SDK (Docker Spaces are no longer free).

Set GROQ_API_KEY as a Space secret (Settings -> Repository secrets),
or export it locally before running.
"""

import os
import re
import duckdb
import faiss
import numpy as np
import gradio as gr
from sentence_transformers import SentenceTransformer
from groq import Groq

# ---------------------------------------------------------------------------
# 1. Toy retail dataset (swap this for a real BinDawood extract later)
# ---------------------------------------------------------------------------
con = duckdb.connect(":memory:")
con.execute("""
CREATE TABLE sales (
    sale_id INTEGER,
    region VARCHAR,
    store VARCHAR,
    category VARCHAR,
    product VARCHAR,
    quantity INTEGER,
    revenue DOUBLE,
    sale_date DATE
)
""")
con.execute("""
INSERT INTO sales VALUES
    (1,'Riyadh','Store A','Dairy','Milk 1L',120,600.0,'2026-07-05'),
    (2,'Riyadh','Store A','Bakery','Bread',80,240.0,'2026-07-05'),
    (3,'Jeddah','Store B','Dairy','Milk 1L',95,475.0,'2026-07-06'),
    (4,'Jeddah','Store B','Produce','Tomatoes',200,300.0,'2026-07-06'),
    (5,'Dammam','Store C','Dairy','Milk 1L',60,300.0,'2026-07-07'),
    (6,'Riyadh','Store A','Produce','Tomatoes',150,225.0,'2026-08-01'),
    (7,'Jeddah','Store B','Bakery','Bread',110,330.0,'2026-08-01')
""")

SCHEMA_DESC = """
Table: sales
Columns: sale_id (INTEGER), region (VARCHAR), store (VARCHAR),
category (VARCHAR), product (VARCHAR), quantity (INTEGER),
revenue (DOUBLE), sale_date (DATE)
"""

# ---------------------------------------------------------------------------
# 2. Trace library -- VERIFIED past question -> SQL -> reasoning.
#    Grows over time; new verified entries get appended here at runtime.
# ---------------------------------------------------------------------------
TRACE_LIBRARY = [
    {
        "question": "What is total revenue by region for July 2026?",
        "reasoning": (
            "Step 1: Need SUM(revenue) grouped by region.\n"
            "Step 2: Filter sale_date within July 2026.\n"
            "Step 3: GROUP BY region."
        ),
        "sql": (
            "SELECT region, SUM(revenue) AS total_revenue "
            "FROM sales WHERE sale_date >= '2026-07-01' "
            "AND sale_date < '2026-08-01' GROUP BY region;"
        ),
    },
    {
        "question": "Show total quantity sold by category for August 2026.",
        "reasoning": (
            "Step 1: Need SUM(quantity) grouped by category.\n"
            "Step 2: Filter sale_date within August 2026.\n"
            "Step 3: GROUP BY category."
        ),
        "sql": (
            "SELECT category, SUM(quantity) AS total_qty "
            "FROM sales WHERE sale_date >= '2026-08-01' "
            "AND sale_date < '2026-09-01' GROUP BY category;"
        ),
    },
    {
        "question": "Which store had the highest revenue overall?",
        "reasoning": (
            "Step 1: Need SUM(revenue) grouped by store.\n"
            "Step 2: ORDER BY total revenue descending.\n"
            "Step 3: LIMIT 1 to get the top store."
        ),
        "sql": (
            "SELECT store, SUM(revenue) AS total_revenue FROM sales "
            "GROUP BY store ORDER BY total_revenue DESC LIMIT 1;"
        ),
    },
]

# ---------------------------------------------------------------------------
# 3. Embedder + FAISS index (rebuilt whenever the library grows)
# ---------------------------------------------------------------------------
embedder = SentenceTransformer("all-MiniLM-L6-v2")


def build_index():
    texts = [t["question"] for t in TRACE_LIBRARY]
    embs = embedder.encode(texts, normalize_embeddings=True)
    idx = faiss.IndexFlatIP(embs.shape[1])
    idx.add(np.array(embs, dtype="float32"))
    return idx


index = build_index()


def retrieve_similar(query: str, k: int = 2):
    q_emb = embedder.encode([query], normalize_embeddings=True)
    scores, idxs = index.search(np.array(q_emb, dtype="float32"), k)
    return [{**TRACE_LIBRARY[i], "score": float(s)} for s, i in zip(scores[0], idxs[0])]


# ---------------------------------------------------------------------------
# 4. Generation + execution
# ---------------------------------------------------------------------------
def generate_sql(new_question: str, k: int = 2):
    examples = retrieve_similar(new_question, k=k)

    exemplar_block = ""
    for i, ex in enumerate(examples, 1):
        exemplar_block += (
            f"\nExample {i} (similarity {ex['score']:.2f}):\n"
            f"Question: {ex['question']}\n"
            f"Reasoning:\n{ex['reasoning']}\n"
            f"SQL: {ex['sql']}\n"
        )

    prompt = f"""You write DuckDB SQL for a retail sales table.
{SCHEMA_DESC}

Here are verified past questions with their reasoning and correct SQL.
Use the SAME reasoning pattern for the new question.
{exemplar_block}

Now answer this new question the same way. Give brief reasoning, then
output ONLY the final SQL on a line starting with "SQL:".

Question: {new_question}
Reasoning:"""

    client = Groq(api_key=os.environ["GROQ_API_KEY"])
    response = client.chat.completions.create(
        model="llama-3.3-70b-versatile",
        messages=[{"role": "user", "content": prompt}],
        temperature=0.1,
    )
    text = response.choices[0].message.content

    match = re.search(r"SQL:\s*(.+)", text, re.DOTALL)
    sql = match.group(1).strip() if match else None
    return examples, text, sql


def run_pipeline(question: str):
    if not question or not question.strip():
        return "Enter a question first.", "", None, ""

    if not os.environ.get("GROQ_API_KEY"):
        return "GROQ_API_KEY is not set (add it as a Space secret).", "", None, ""

    examples, full_response, sql = generate_sql(question)

    retrieved_md = "\n".join(
        f"- **[{ex['score']:.2f}]** {ex['question']}" for ex in examples
    )

    if not sql:
        return retrieved_md, full_response, None, "No SQL extracted from model output."

    try:
        df = con.execute(sql).fetchdf()
        return retrieved_md, f"```sql\n{sql}\n```", df, "Query ran successfully."
    except Exception as e:
        return retrieved_md, f"```sql\n{sql}\n```", None, f"SQL failed: {e}"


def add_verified_example(question: str, reasoning: str, sql: str):
    """Manually confirm a generated query was correct, then grow the
    trace library with it. This is the feedback loop that makes the
    library compound in usefulness over time -- only call this after
    a human has checked the result."""
    global index
    if not question or not sql:
        return "Need both a question and SQL to add.", library_preview()
    TRACE_LIBRARY.append({
        "question": question.strip(),
        "reasoning": reasoning.strip() or "Verified example.",
        "sql": sql.strip(),
    })
    index = build_index()
    return f"Added. Library now has {len(TRACE_LIBRARY)} verified examples.", library_preview()


def library_preview():
    return "\n".join(f"{i+1}. {t['question']}" for i, t in enumerate(TRACE_LIBRARY))


# ---------------------------------------------------------------------------
# 5. Gradio UI
# ---------------------------------------------------------------------------
with gr.Blocks(title="Text-to-SQL Trace RAG Prototype") as demo:
    gr.Markdown(
        "# Text-to-SQL Trace RAG — Prototype\n"
        "Retrieves **verified past question -> SQL -> reasoning** examples "
        "(not documents) and uses them as few-shot patterns to generate new SQL. "
        "Toy `sales` table included -- swap for a real schema for production use."
    )

    with gr.Tab("Ask a question"):
        question_in = gr.Textbox(
            label="Ask a BI question",
            placeholder="e.g. What is total revenue by category for August 2026?",
        )
        ask_btn = gr.Button("Generate & Run", variant="primary")

        retrieved_out = gr.Markdown(label="Retrieved examples used")
        sql_out = gr.Markdown(label="Generated SQL")
        result_out = gr.Dataframe(label="Query result")
        status_out = gr.Textbox(label="Status", interactive=False)

        ask_btn.click(
            run_pipeline,
            inputs=[question_in],
            outputs=[retrieved_out, sql_out, result_out, status_out],
        )

    with gr.Tab("Add verified example"):
        gr.Markdown(
            "Once you've manually confirmed a generated query was correct, "
            "add it here so future similar questions retrieve it too."
        )
        v_question = gr.Textbox(label="Question")
        v_reasoning = gr.Textbox(label="Reasoning (steps taken)", lines=3)
        v_sql = gr.Textbox(label="Verified correct SQL", lines=3)
        add_btn = gr.Button("Add to trace library")
        add_status = gr.Textbox(label="Status", interactive=False)
        lib_view = gr.Textbox(
            label="Current library", value=library_preview(), lines=8, interactive=False
        )

        add_btn.click(
            add_verified_example,
            inputs=[v_question, v_reasoning, v_sql],
            outputs=[add_status, lib_view],
        )

if __name__ == "__main__":
    demo.launch()