Spaces:
Running on Zero
Running on Zero
Add schemagate demo app
Browse files- README.md +33 -5
- app.py +106 -0
- requirements.txt +1 -0
README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
| 1 |
---
|
| 2 |
-
title:
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
-
colorTo:
|
| 6 |
sdk: gradio
|
| 7 |
sdk_version: 6.26.0
|
| 8 |
python_version: '3.12'
|
|
@@ -10,6 +10,34 @@ app_file: app.py
|
|
| 10 |
pinned: false
|
| 11 |
license: apache-2.0
|
| 12 |
short_description: Identity-scoped schema selection for text-to-SQL
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
---
|
| 14 |
|
| 15 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
+
title: schemagate
|
| 3 |
+
emoji: 🔒
|
| 4 |
+
colorFrom: green
|
| 5 |
+
colorTo: gray
|
| 6 |
sdk: gradio
|
| 7 |
sdk_version: 6.26.0
|
| 8 |
python_version: '3.12'
|
|
|
|
| 10 |
pinned: false
|
| 11 |
license: apache-2.0
|
| 12 |
short_description: Identity-scoped schema selection for text-to-SQL
|
| 13 |
+
tags:
|
| 14 |
+
- text-to-sql
|
| 15 |
+
- nl2sql
|
| 16 |
+
- rag
|
| 17 |
+
- sql
|
| 18 |
+
- security
|
| 19 |
---
|
| 20 |
|
| 21 |
+
# schemagate
|
| 22 |
+
|
| 23 |
+
Picks the handful of tables a text-to-SQL model needs, and filters them by who is
|
| 24 |
+
asking — **before** the model sees anything.
|
| 25 |
+
|
| 26 |
+
Schema selection runs before the query runs, so it runs before row-level security
|
| 27 |
+
can act. A selector that is not identity-aware hands the model a table the caller
|
| 28 |
+
cannot read: the SQL is correct, RLS filters every row, and the user is told
|
| 29 |
+
*no records found*. A wrong answer in a confident tone, not an access-denied message.
|
| 30 |
+
|
| 31 |
+
This Space runs the real selector over a bundled 42-object demo schema.
|
| 32 |
+
No database, no API key, no model call — BM25 plus a hashed embedder, offline.
|
| 33 |
+
`hr_compensation` is restricted to the `payroll` role: change the roles box and
|
| 34 |
+
watch it appear and disappear from the prompt.
|
| 35 |
+
|
| 36 |
+
```
|
| 37 |
+
pip install schemagate
|
| 38 |
+
schemagate demo "which customers owe us money"
|
| 39 |
+
```
|
| 40 |
+
|
| 41 |
+
- Source: https://github.com/ashishsinha1602/schemagate
|
| 42 |
+
- PyPI: https://pypi.org/project/schemagate/
|
| 43 |
+
- Apache-2.0. Certified live on Oracle Autonomous Database 26ai and PostgreSQL 16.
|
app.py
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""schemagate on Hugging Face Spaces.
|
| 2 |
+
|
| 3 |
+
Two questions this page answers, without a database and without a model:
|
| 4 |
+
1. What does the selector actually send to the LLM?
|
| 5 |
+
2. What happens to a restricted table when the caller lacks the role?
|
| 6 |
+
"""
|
| 7 |
+
from __future__ import annotations
|
| 8 |
+
|
| 9 |
+
import gradio as gr
|
| 10 |
+
|
| 11 |
+
from schemagate import Catalog, Principal
|
| 12 |
+
from schemagate.demo_schema import HINTS, create_demo_db
|
| 13 |
+
|
| 14 |
+
CAT = Catalog().bootstrap(create_demo_db())
|
| 15 |
+
for _table, _text in HINTS.items():
|
| 16 |
+
CAT.hint(_table, _text)
|
| 17 |
+
CAT.restrict("hr_compensation", ["payroll"])
|
| 18 |
+
|
| 19 |
+
FULL_DDL = "\n\n".join(d.render_ddl() for d in CAT.objects())
|
| 20 |
+
FULL_TOKENS = max(1, len(FULL_DDL) // 4) # ~4 chars per token
|
| 21 |
+
OBJECTS = len(CAT)
|
| 22 |
+
|
| 23 |
+
EXAMPLES = [
|
| 24 |
+
["which customers owe us money", "demo:analyst", ""],
|
| 25 |
+
["salary and pay grade by employee", "okta:jdoe", ""],
|
| 26 |
+
["salary and pay grade by employee", "okta:hr", "payroll"],
|
| 27 |
+
["things we are running out of", "demo:analyst", ""],
|
| 28 |
+
["total revenue by month last year", "demo:analyst", ""],
|
| 29 |
+
]
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
def run(question: str, subject: str, roles: str, top_k: int):
|
| 33 |
+
question = (question or "").strip()
|
| 34 |
+
if not question:
|
| 35 |
+
return "Ask something.", "", ""
|
| 36 |
+
|
| 37 |
+
role_set = {r.strip() for r in (roles or "").split(",") if r.strip()}
|
| 38 |
+
principal = Principal(subject or "demo:analyst", roles=role_set)
|
| 39 |
+
sel = CAT.select(question, top_k=int(top_k), principal=principal)
|
| 40 |
+
|
| 41 |
+
fragment = sel.prompt_fragment()
|
| 42 |
+
tokens = max(1, len(fragment) // 4)
|
| 43 |
+
cut = 100 - round(tokens / FULL_TOKENS * 100)
|
| 44 |
+
|
| 45 |
+
picked = "\n".join(f"- `{n}`" for n in sel.table_names) or "_nothing matched_"
|
| 46 |
+
restricted = any(n.split(".")[-1] == "hr_compensation" for n in sel.table_names)
|
| 47 |
+
note = (
|
| 48 |
+
"`hr_compensation` **is** in the selection — this caller holds `payroll`."
|
| 49 |
+
if restricted
|
| 50 |
+
else "`hr_compensation` is **absent**: not ranked low, not present. Its name "
|
| 51 |
+
"does not appear anywhere in the text below."
|
| 52 |
+
)
|
| 53 |
+
|
| 54 |
+
summary = (
|
| 55 |
+
f"**{len(sel.table_names)} of {OBJECTS} objects** for "
|
| 56 |
+
f"`{principal.subject}` roles={sorted(role_set) or '—'}\n\n"
|
| 57 |
+
f"{picked}\n\n{note}\n\n"
|
| 58 |
+
f"**{tokens} tokens** in the prompt fragment, against **{FULL_TOKENS}** "
|
| 59 |
+
f"for the whole schema — a {cut}% cut. The selector never called a model "
|
| 60 |
+
f"to decide this."
|
| 61 |
+
)
|
| 62 |
+
return summary, fragment, sel.explain()
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
with gr.Blocks(title="schemagate", theme=gr.themes.Soft()) as demo:
|
| 66 |
+
gr.Markdown(
|
| 67 |
+
"# schemagate\n"
|
| 68 |
+
"### Identity-scoped schema selection for text-to-SQL\n\n"
|
| 69 |
+
"Schema selection happens **before** the query runs — so it happens before "
|
| 70 |
+
"row-level security can act. A selector that is not identity-aware hands the "
|
| 71 |
+
"model a table the caller cannot read; the SQL is correct, RLS filters every "
|
| 72 |
+
"row, and the user is told *no records found*. That is a wrong answer in a "
|
| 73 |
+
"confident tone, not an access-denied message.\n\n"
|
| 74 |
+
f"This page runs the real selector over a bundled {OBJECTS}-object schema. "
|
| 75 |
+
"No database, no API key, no model call — BM25 plus a hashed embedder, offline. "
|
| 76 |
+
"`hr_compensation` is restricted to the `payroll` role: change the roles box "
|
| 77 |
+
"and watch it appear and disappear."
|
| 78 |
+
)
|
| 79 |
+
with gr.Row():
|
| 80 |
+
with gr.Column(scale=3):
|
| 81 |
+
question = gr.Textbox(label="Question", value="salary and pay grade by employee")
|
| 82 |
+
with gr.Column(scale=2):
|
| 83 |
+
subject = gr.Textbox(label="Caller", value="okta:jdoe")
|
| 84 |
+
roles = gr.Textbox(label="Roles (comma-separated)", placeholder="payroll")
|
| 85 |
+
top_k = gr.Slider(1, 12, value=6, step=1, label="Tables to select")
|
| 86 |
+
go = gr.Button("Select", variant="primary")
|
| 87 |
+
|
| 88 |
+
out = gr.Markdown()
|
| 89 |
+
with gr.Accordion("Prompt fragment — exactly what the model would receive", open=True):
|
| 90 |
+
fragment = gr.Code(label="", language="sql")
|
| 91 |
+
with gr.Accordion("Why these tables", open=False):
|
| 92 |
+
explain = gr.Code(label="")
|
| 93 |
+
|
| 94 |
+
gr.Examples(EXAMPLES, inputs=[question, subject, roles])
|
| 95 |
+
gr.Markdown(
|
| 96 |
+
"`pip install schemagate` · "
|
| 97 |
+
"[GitHub](https://github.com/ashishsinha1602/schemagate) · "
|
| 98 |
+
"[PyPI](https://pypi.org/project/schemagate/) · Apache-2.0\n\n"
|
| 99 |
+
"Certified live on Oracle Autonomous Database 26ai and PostgreSQL 16."
|
| 100 |
+
)
|
| 101 |
+
|
| 102 |
+
go.click(run, [question, subject, roles, top_k], [out, fragment, explain])
|
| 103 |
+
question.submit(run, [question, subject, roles, top_k], [out, fragment, explain])
|
| 104 |
+
|
| 105 |
+
if __name__ == "__main__":
|
| 106 |
+
demo.launch()
|
requirements.txt
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
schemagate>=0.1.8
|