| import concurrent.futures |
| import gc |
| import html |
| import inspect |
| import os |
| import re |
| import threading |
| import time |
| import traceback |
| import unicodedata |
|
|
| import gradio as gr |
| import sqlparse |
|
|
| import chat_state as chat_core |
| import intent as intent_core |
| import model_io as model_core |
| import sql_tools as sql_core |
|
|
|
|
| FINE_TUNED_MODEL_ID = "Shizu0n/phi3-mini-sql-generator-merged" |
|
|
| FINE_TUNED_MODEL_KEY = "fine_tuned" |
| DEFAULT_MODEL_KEY = FINE_TUNED_MODEL_KEY |
| FALLBACK_RESPONSE = ( |
| "Select a schema and ask a SQL question, " |
| "or ask to create or edit a table. " |
| "Example: 'what is the most expensive product?' or " |
| "'create table products with id name price'." |
| ) |
| UNSUPPORTED_MUTATION_RESPONSE = ( |
| "This demo does not generate INSERT, UPDATE, DELETE, or DROP statements. " |
| "It only supports SELECT/WITH model SQL plus deterministic CREATE TABLE tools." |
| ) |
| SOURCE_FINE_TUNED_MODEL = "Source: fine-tuned model" |
| SOURCE_DETERMINISTIC_SQL_TEMPLATE = "Source: deterministic SQL template" |
| SOURCE_DETERMINISTIC_SCHEMA_PARSER = "Source: deterministic schema parser" |
| SOURCE_STATIC_FALLBACK = "Source: static fallback" |
|
|
| MODEL_CATALOG = { |
| FINE_TUNED_MODEL_KEY: { |
| "label": "Fine-tuned QLoRA model", |
| "short_label": "Fine-tuned", |
| "tag": "Fine-tuned", |
| "title": "QLoRA merged", |
| "model_id": FINE_TUNED_MODEL_ID, |
| "exact_match": "73.5%", |
| "trust_remote_code": False, |
| "ready_text": "Fine-tuned model ready", |
| "metadata": ( |
| "Model: Shizu0n/phi3-mini-sql-generator-merged\n" |
| "Base: microsoft/Phi-3-mini-4k-instruct\n" |
| "Fine-tuning data: b-mc2/sql-create-context, 1,000 examples\n" |
| "Metric: 73.5% exact match vs 2.0% base (+71.5pp)" |
| ), |
| }, |
| } |
|
|
| PRESETS = { |
| "employees": "CREATE TABLE employees (id INTEGER, name TEXT, department TEXT, salary NUMERIC)", |
| "orders": "CREATE TABLE orders (id INTEGER, customer_id INTEGER, product TEXT, amount NUMERIC, date DATE)", |
| "students": "CREATE TABLE students (id INTEGER, name TEXT, course TEXT, grade NUMERIC, year INTEGER)", |
| "products": "CREATE TABLE products (id INTEGER, name TEXT, category TEXT, price NUMERIC, stock INTEGER)", |
| "sales": "CREATE TABLE sales (id INTEGER, product_id INTEGER, quantity INTEGER, total NUMERIC, date DATE)", |
| } |
|
|
| PROMPT_TEMPLATE = ( |
| "<|user|>\n" |
| "Given the following SQL table, write a SQL query.\n\n" |
| "Table: {schema}\n\n" |
| "Question: {question}<|end|>\n" |
| "<|assistant|>" |
| ) |
|
|
| GENERAL_PROMPT_TEMPLATE = ( |
| "<|user|>\n" |
| "You are a SQL assistant. Answer the user's question.\n\n" |
| "Question: {message}<|end|>\n" |
| "<|assistant|>" |
| ) |
|
|
| EMPTY_VALIDATOR = '<span class="validator-badge validator-empty">No SQL yet</span>' |
| CHAT_VALIDATOR = '<span class="validator-badge validator-empty">Chat response</span>' |
| EMPTY_CHAT_OUTPUT = "" |
| LOAD_TIMEOUT_SECONDS = 900 |
| GENERATION_MAX_TIME_SECONDS = 285 |
| GENERATION_TIMEOUT_SECONDS = 320 |
| LOCAL_FILES_ONLY_ENV = "PHI3_SQL_LOCAL_FILES_ONLY" |
| LOAD_SCROLL_JS = """ |
| (selectedKey) => { |
| setTimeout(() => { |
| document.querySelector("#query-section")?.scrollIntoView({ |
| behavior: "smooth", |
| block: "start" |
| }); |
| }, 50); |
| return selectedKey; |
| } |
| """ |
|
|
| _current_model_id = None |
| _model = None |
| _tokenizer = None |
| _model_lock = threading.RLock() |
| _model_activity_lock = threading.Lock() |
|
|
|
|
| def import_model_runtime(): |
| try: |
| import torch |
| from transformers import AutoConfig, AutoModelForCausalLM, AutoTokenizer |
| except ModuleNotFoundError as exc: |
| missing = exc.name or "a runtime dependency" |
| raise RuntimeError( |
| f"Missing dependency: {missing}. Install the Space dependencies with " |
| "`python -m pip install -r requirements.txt` in the same Python environment " |
| "that runs app.py. HuggingFace Spaces installs requirements.txt during build." |
| ) from exc |
|
|
| return torch, AutoConfig, AutoModelForCausalLM, AutoTokenizer |
|
|
|
|
| def log_load_step(model_id, step, started=None): |
| elapsed = "" if started is None else f" elapsed={time.time() - started:.1f}s" |
| print(f"[LOAD_STEP] model={model_id} step={step}{elapsed}", flush=True) |
|
|
|
|
| def cached_model_weights_available(model_id): |
| try: |
| from huggingface_hub import try_to_load_from_cache |
| except ModuleNotFoundError: |
| return False |
|
|
| weight_files = ( |
| "model.safetensors", |
| "model.safetensors.index.json", |
| "pytorch_model.bin", |
| "pytorch_model.bin.index.json", |
| ) |
| for filename in weight_files: |
| try: |
| cached_path = try_to_load_from_cache(model_id, filename) |
| except Exception: |
| cached_path = None |
| if isinstance(cached_path, str) and os.path.exists(cached_path): |
| return True |
| return False |
|
|
|
|
| def cached_file_path(model_id, filename): |
| try: |
| from huggingface_hub import try_to_load_from_cache |
| except ModuleNotFoundError: |
| return None |
|
|
| try: |
| cached_path = try_to_load_from_cache(model_id, filename) |
| except Exception: |
| return None |
| if isinstance(cached_path, str) and os.path.exists(cached_path): |
| return cached_path |
| return None |
|
|
|
|
| def cached_snapshot_path(model_id): |
| config_path = cached_file_path(model_id, "config.json") |
| if not config_path or not cached_model_weights_available(model_id): |
| return None |
| return os.path.dirname(config_path) |
|
|
|
|
| def local_files_only_for(model_id): |
| explicit_local = os.getenv(LOCAL_FILES_ONLY_ENV, "").strip().lower() in {"1", "true", "yes", "on"} |
| offline_mode = bool(os.getenv("HF_HUB_OFFLINE") or os.getenv("TRANSFORMERS_OFFLINE")) |
| return explicit_local or offline_mode |
|
|
|
|
| def running_on_spaces(): |
| return bool(os.getenv("SPACE_ID")) |
|
|
|
|
| def resolve_model_source(model_id): |
| if local_files_only_for(model_id): |
| return cached_snapshot_path(model_id) or model_id |
| return model_id |
|
|
|
|
| def dtype_from_name(torch, dtype_name): |
| if not dtype_name: |
| return None |
| normalized = str(dtype_name).replace("torch.", "") |
| return { |
| "float16": torch.float16, |
| "bfloat16": torch.bfloat16, |
| "float32": torch.float32, |
| }.get(normalized) |
|
|
|
|
| def dtype_from_safetensors(torch, source): |
| safetensors_path = os.path.join(source, "model.safetensors") |
| if not os.path.exists(safetensors_path): |
| return None |
| try: |
| from safetensors import safe_open |
|
|
| with safe_open(safetensors_path, framework="pt", device="cpu") as handle: |
| keys = list(handle.keys()) |
| if not keys: |
| return None |
| return handle.get_tensor(keys[0]).dtype |
| except Exception: |
| return None |
|
|
|
|
| def cpu_model_dtype(torch): |
| return torch.bfloat16 |
|
|
|
|
| def model_load_kwargs(torch, config, source): |
| return { |
| "attn_implementation": "eager", |
| "device_map": {"": "cpu"}, |
| "low_cpu_mem_usage": True, |
| "torch_dtype": "auto", |
| } |
|
|
|
|
| def force_eager_attention(config): |
| for attr in ("attn_implementation", "_attn_implementation"): |
| try: |
| setattr(config, attr, "eager") |
| except Exception: |
| pass |
| return config |
|
|
|
|
| def _run_generation(model, inputs, kwargs): |
| if not _model_activity_lock.acquire(blocking=False): |
| raise RuntimeError( |
| "Another model operation is still running. Wait for it to finish before starting another request." |
| ) |
| torch, _, _, _ = import_model_runtime() |
| try: |
| with torch.no_grad(): |
| return model.generate(**inputs, **kwargs) |
| finally: |
| _model_activity_lock.release() |
|
|
|
|
| def _run_model_load(model_id): |
| return load_model(model_id) |
|
|
|
|
| def patch_phi3_config(config): |
| if hasattr(config, "rope_scaling") and config.rope_scaling: |
| rope_type = config.rope_scaling.get("rope_type", "longrope") |
| if "type" not in config.rope_scaling: |
| config.rope_scaling["type"] = rope_type |
| if hasattr(config, "rope_parameters") and config.rope_parameters is None: |
| config.rope_parameters = dict(config.rope_scaling) |
| return config |
|
|
|
|
| def unload_model(): |
| global _current_model_id, _model, _tokenizer |
| with _model_lock: |
| if _model is not None: |
| del _model |
| if _tokenizer is not None: |
| del _tokenizer |
| _model = None |
| _tokenizer = None |
| _current_model_id = None |
| gc.collect() |
| try: |
| import torch |
|
|
| if torch.cuda.is_available(): |
| torch.cuda.empty_cache() |
| except ImportError: |
| pass |
|
|
|
|
| def load_model(model_id): |
| global _current_model_id, _model, _tokenizer |
| started = time.time() |
| log_load_step(model_id, "requested", started) |
| if not _model_lock.acquire(blocking=False): |
| raise RuntimeError("Another model load is still running. Wait for it to finish before retrying.") |
| try: |
| if _current_model_id == model_id and _model is not None and _tokenizer is not None: |
| log_load_step(model_id, "already_loaded", started) |
| return _model, _tokenizer |
|
|
| if not _model_activity_lock.acquire(blocking=False): |
| raise RuntimeError( |
| "Another model operation is still running. Wait for it to finish before switching models." |
| ) |
| try: |
| log_load_step(model_id, "runtime_import_start", started) |
| torch, AutoConfig, AutoModelForCausalLM, AutoTokenizer = import_model_runtime() |
| log_load_step(model_id, "runtime_import_done", started) |
| local_files_only = local_files_only_for(model_id) |
| model_source = resolve_model_source(model_id) |
| log_load_step(model_id, f"cache_mode local_files_only={local_files_only}", started) |
| log_load_step(model_id, f"model_source {model_source}", started) |
| log_load_step(model_id, "unload_previous_start", started) |
| unload_model() |
| log_load_step(model_id, "unload_previous_done", started) |
| model_def = model_by_id(model_id) |
| common_kwargs = { |
| "trust_remote_code": model_def["trust_remote_code"], |
| "local_files_only": local_files_only, |
| } |
| log_load_step(model_id, "config_start", started) |
| config = AutoConfig.from_pretrained( |
| model_source, |
| **common_kwargs, |
| ) |
| if model_def["trust_remote_code"]: |
| config = patch_phi3_config(config) |
| config = force_eager_attention(config) |
| log_load_step(model_id, "config_done", started) |
| load_kwargs = model_load_kwargs(torch, config, model_source) |
| log_load_step(model_id, f"model_kwargs {load_kwargs}", started) |
| log_load_step(model_id, "tokenizer_start", started) |
| tokenizer = AutoTokenizer.from_pretrained( |
| model_source, |
| **common_kwargs, |
| ) |
| if tokenizer.pad_token_id is None and tokenizer.eos_token is not None: |
| tokenizer.pad_token = tokenizer.eos_token |
| log_load_step(model_id, "tokenizer_done", started) |
| log_load_step(model_id, "weights_start", started) |
| model = AutoModelForCausalLM.from_pretrained( |
| model_source, |
| config=config, |
| **common_kwargs, |
| **load_kwargs, |
| ) |
| log_load_step(model_id, "weights_done", started) |
| log_load_step(model_id, f"loaded_dtype {getattr(model, 'dtype', 'unknown')}", started) |
| log_load_step(model_id, "eval_start", started) |
| model.config.use_cache = False |
| model.eval() |
| log_load_step(model_id, "eval_done", started) |
|
|
| _model = model |
| _tokenizer = tokenizer |
| _current_model_id = model_id |
| log_load_step(model_id, "state_set_done", started) |
| return model, tokenizer |
| finally: |
| _model_activity_lock.release() |
| finally: |
| _model_lock.release() |
|
|
|
|
| def model_by_key(model_key): |
| return MODEL_CATALOG.get(model_key, MODEL_CATALOG[DEFAULT_MODEL_KEY]) |
|
|
|
|
| def model_by_id(model_id): |
| for model_def in MODEL_CATALOG.values(): |
| if model_def["model_id"] == model_id: |
| return model_def |
| raise ValueError(f"Unknown model id: {model_id}") |
|
|
|
|
| def model_key_by_id(model_id): |
| for key, model_def in MODEL_CATALOG.items(): |
| if model_def["model_id"] == model_id: |
| return key |
| return None |
|
|
|
|
| def content_to_text(value): |
| if value is None: |
| return "" |
| if isinstance(value, str): |
| return value |
| if isinstance(value, dict): |
| for key in ("text", "content", "value"): |
| if key in value: |
| return content_to_text(value[key]) |
| return " ".join(content_to_text(item) for item in value.values()) |
| if isinstance(value, (list, tuple)): |
| return "\n".join(content_to_text(item) for item in value) |
| return str(value) |
|
|
|
|
| def normalize_text(value): |
| text = content_to_text(value).lower() |
| text = unicodedata.normalize("NFKD", text) |
| text = "".join(char for char in text if not unicodedata.combining(char)) |
| return re.sub(r"\s+", " ", text).strip() |
|
|
|
|
| def safe_chat_fallback(_message=""): |
| return FALLBACK_RESPONSE |
|
|
|
|
| def clean_generation(text): |
| cleaned = content_to_text(text).strip() |
| if cleaned.startswith("```"): |
| lines = cleaned.splitlines() |
| if lines and lines[0].strip().lower() in {"```", "```sql"}: |
| lines = lines[1:] |
| if lines and lines[-1].strip() == "```": |
| lines = lines[:-1] |
| cleaned = "\n".join(lines).strip() |
| for marker in ("<|end|>", "<|user|>", "<|assistant|>", "</s>"): |
| if marker in cleaned: |
| cleaned = cleaned.split(marker, 1)[0].strip() |
| if cleaned.upper().startswith("SQL:"): |
| cleaned = cleaned[4:].strip() |
| return cleaned |
|
|
|
|
| def extract_sql_candidate(text): |
| cleaned = clean_generation(text) |
| match = re.search(r"\b(SELECT|WITH|INSERT|UPDATE|DELETE|CREATE|ALTER|DROP)\b", cleaned, flags=re.IGNORECASE) |
| if not match: |
| return cleaned |
| return cleaned[match.start() :].strip() |
|
|
|
|
| def is_sql_like(text): |
| text = (text or "").strip() |
| if not text: |
| return False |
| first_word = re.match(r"^\s*([A-Za-z]+)", text) |
| if not first_word: |
| return False |
| return first_word.group(1).upper() in { |
| "SELECT", |
| "WITH", |
| "INSERT", |
| "UPDATE", |
| "DELETE", |
| "CREATE", |
| "ALTER", |
| "DROP", |
| } |
|
|
|
|
|
|
|
|
| def render_theme_detection_style(): |
| return """ |
| <style> |
| @media (prefers-color-scheme: light) { |
| :root, |
| body, |
| gradio-app, |
| .gradio-container, |
| .gradio-container .main, |
| .gradio-container .wrap, |
| .gradio-container .contain { |
| color-scheme: light; |
| --bg-base: #edf2f7; |
| --bg-surface: #ffffff; |
| --bg-raised: #f4f7fb; |
| --border: #d8e0ea; |
| --border-hi: #c5cfdd; |
| --text-hi: #172033; |
| --text-mid: #667085; |
| --text-lo: #8a95a6; |
| --text-primary: var(--text-hi); |
| --text-secondary: var(--text-mid); |
| --text-muted: var(--text-lo); |
| --teal: #0d8b67; |
| --teal-soft: #dff6ec; |
| --teal-text: #076348; |
| --amber-soft: #fff3d8; |
| --amber-text: #7a4700; |
| --overlay-bg: rgba(23, 32, 51, 0.28); |
| --chat-bubble-bot-bg: #f4f7fb; |
| --chat-bubble-user-bg: #edf6ff; |
| --chat-pending-bg: #e8eef8; |
| --chat-pending-text: #172033; |
| --chat-copy-btn-bg: #ffffff; |
| --code-bg: #101828; |
| --code-border: #25344d; |
| --code-text: #d7e3f3; |
| --schema-context-bg: #f3fbf8; |
| --disabled-bg: #b7c3cf; |
| } |
| |
| .gradio-container, |
| .gradio-container .main, |
| .gradio-container .wrap, |
| .gradio-container .contain { |
| background: var(--bg-base) !important; |
| color: var(--text-primary) !important; |
| } |
| } |
| </style> |
| """ |
|
|
|
|
| def render_header(): |
| return """ |
| <section class="top-panel"> |
| <div> |
| <h1>Phi-3 Mini SQL Chatbot</h1> |
| <p>Text-to-SQL demo with explicit model and guardrail boundaries.</p> |
| </div> |
| <div class="top-badges"> |
| <span class="badge badge-green">Fine-tuned model: SQL_QUERY</span> |
| <span class="badge badge-cream">Deterministic guardrails</span> |
| <span class="badge badge-light">CPU lazy load</span> |
| </div> |
| </section> |
| """ |
|
|
|
|
| def render_step(number, title): |
| return f""" |
| <div class="step-title"> |
| <span>{number} — {title}</span> |
| </div> |
| """ |
|
|
|
|
| def render_model_card(model_key, selected_key): |
| model_def = model_by_key(model_key) |
| selected = model_key == selected_key |
| state_class = " selected" if selected else "" |
| return f""" |
| <article class="model-card{state_class}"> |
| <div class="model-tag">{model_def["tag"]}</div> |
| <h3>{model_def["title"]}</h3> |
| <code>{model_def["model_id"]}</code> |
| <div class="model-score"> |
| <span>{model_def["exact_match"]}</span> |
| <small>exact match</small> |
| </div> |
| <div class="model-card-footer"> |
| <span>{model_def["label"]}</span> |
| </div> |
| {render_baseline_evidence()} |
| </article> |
| """ |
|
|
|
|
| def render_status(selected_key=None, loaded_key=None, state="idle"): |
| selected_def = model_by_key(selected_key or DEFAULT_MODEL_KEY) |
| if state == "loading": |
| return ( |
| '<div class="status-pill status-loading">' |
| f'<span></span> {SOURCE_FINE_TUNED_MODEL}. Loading {selected_def["short_label"]} model - first load ~3-5 min' |
| "</div>" |
| ) |
| if loaded_key: |
| loaded_def = model_by_key(loaded_key) |
| return ( |
| '<div class="status-pill status-ready">' |
| f'<span></span> {SOURCE_FINE_TUNED_MODEL}. {loaded_def["short_label"]} model ready - ~3-5 min per query on CPU' |
| "</div>" |
| ) |
| return f'<div class="status-pill status-empty"><span></span> {SOURCE_FINE_TUNED_MODEL}. No model loaded</div>' |
|
|
|
|
| def render_loading_overlay(model_key=None, visible=False): |
| if not visible: |
| return '<div class="loading-overlay hidden"></div>' |
| model_def = model_by_key(model_key or DEFAULT_MODEL_KEY) |
| return ( |
| '<div class="loading-overlay">' |
| '<div class="loading-card">' |
| f'<div class="loading-title">Loading {model_def["short_label"]} model</div>' |
| '<div class="loading-line"><span></span></div>' |
| '<p>First load: ~3-5 min — cached for session</p>' |
| "</div>" |
| "</div>" |
| ) |
|
|
|
|
| def model_metadata(model_key=None): |
| return f""" |
| <section class="metadata-panel"> |
| <div class="panel-heading"> |
| <h2>Source contract</h2> |
| <span>SQL_QUERY + create/edit/fallback</span> |
| </div> |
| <div class="metadata-body"> |
| {render_source_contract_grid()} |
| </div> |
| </section> |
| """ |
|
|
|
|
| def render_source_contract_grid(): |
| return """ |
| <div class="stats-row stats-row-compact source-contract-grid"> |
| <div class="stat-card"><strong>SQL_QUERY</strong><span>templates first; model only for SELECT/WITH</span></div> |
| <div class="stat-card"><strong>Create</strong><span>deterministic CREATE TABLE parser</span></div> |
| <div class="stat-card"><strong>Edit</strong><span>deterministic schema updates</span></div> |
| <div class="stat-card"><strong>Fallback</strong><span>static non-SQL response</span></div> |
| </div> |
| """ |
|
|
|
|
| def render_source_legend(extra_class=""): |
| class_name = "source-panel" |
| if extra_class: |
| class_name = f"{class_name} {extra_class}" |
| return f""" |
| <section class="{class_name}"> |
| <div class="panel-heading"> |
| <h2>Source contract</h2> |
| <span>always visible</span> |
| </div> |
| <div class="metadata-body"> |
| {render_source_contract_grid()} |
| </div> |
| </section> |
| """ |
|
|
|
|
| def render_example_prompts(): |
| return """ |
| <section class="example-panel"> |
| <div class="panel-heading"> |
| <h2>Example prompts</h2> |
| <span>by source</span> |
| </div> |
| <div class="example-list"> |
| <div class="example-group"><span>fine-tuned model</span><code>show employees in Engineering ordered by salary</code></div> |
| <div class="example-group"><span>deterministic SQL template</span><code>what is the most expensive product?</code></div> |
| <div class="example-group"><span>deterministic schema parser</span><code>create table animals with id name species weight</code></div> |
| <div class="example-group"><span>static fallback</span><code>what can you do?</code></div> |
| </div> |
| </section> |
| """ |
|
|
|
|
| def render_baseline_evidence(): |
| return """ |
| <div class="model-card-evidence"> |
| <span class="model-card-evidence-heading">Offline evidence</span> |
| <div class="model-card-evidence-grid"> |
| <div class="model-card-evidence-chip"> |
| <strong>Base</strong> |
| <small>2.0%</small> |
| </div> |
| <div class="model-card-evidence-chip highlighted"> |
| <strong>Fine-tuned</strong> |
| <small>73.5%</small> |
| </div> |
| <div class="model-card-evidence-chip"> |
| <strong>Gain</strong> |
| <small>+71.5pp</small> |
| </div> |
| </div> |
| </div> |
| """ |
|
|
|
|
| def schema_name_by_value(schema): |
| schema = (schema or "").strip() |
| for name, value in PRESETS.items(): |
| if value == schema: |
| return name |
| table_name, _columns = sql_core.parse_create_table_schema(schema) |
| if table_name: |
| return table_name |
| return "custom" |
|
|
|
|
| def is_create_table_intent(message): |
| message = (message or "").strip().lower() |
| return bool( |
| re.search(r"\b(create|make|build|generate|criar|crie|cria|gerar|gere|faz|faça)\b", message) |
| and re.search(r"\b(table|schema|tabela)\b", message) |
| ) |
|
|
|
|
| def is_table_edit_intent(message): |
| message = (message or "").strip().lower() |
| edit_terms = r"\b(edit|update|modify|alter|add|include|remove|delete|drop|edita|editar|altera|altere|alterar|mude|mudar|adicione|adicionar|inclua|incluir|acrescente|remova|remover|delete|deletar|exclua|excluir|novo|nova|troca|trocar|troquecoloque|colocar)\b" |
| direct_add_terms = r"\b(add|include|adicione|adicionar|adicionando|inclua|incluir|acrescente)\b" |
| direct_remove_terms = r"\b(remove|delete|drop|remova|remover|deletar|exclua|excluir)\b" |
| target_terms = r"\b(column|field|element|coluna|campo|elemento|item)\b" |
| |
| sql_aggregation_terms = {"up", "sum", "total", "count", "average", "avg", "max", "min", "by"} |
| words = message.split() |
| |
| |
| add_match = re.search(direct_add_terms, message) |
| has_target = re.search(target_terms, message) |
| if add_match: |
| |
| match_pos = add_match.start() |
| after_add = message[match_pos + len(add_match.group()):].strip() |
| first_word_after = after_add.split()[0] if after_add.split() else "" |
| |
| is_sql_query = first_word_after in sql_aggregation_terms |
| is_add_intent = not is_sql_query |
| else: |
| is_add_intent = False |
| return bool( |
| is_add_intent |
| or re.search(direct_remove_terms, message) |
| or is_rename_intent(message) |
| or re.search(r"\b(?:altere|alterar|mude|mudar)\b.*\bter\b", message) |
| or (re.search(edit_terms, message) and (re.search(target_terms, message) or ":" in message or re.search(r"\bpor\b", message))) |
| ) |
|
|
|
|
| def infer_column_type(column_name): |
| name = column_name.strip().lower() |
| if name == "id" or name.endswith("_id") or name in {"quantity", "quantidade", "stock", "estoque", "year"}: |
| return "INTEGER" |
| if name in { |
| "salary", |
| "price", |
| "preco", |
| "amount", |
| "total", |
| "grade", |
| "peso", |
| "weight", |
| "idade", |
| "age", |
| "altura", |
| "height", |
| "largura", |
| "width", |
| "comprimento", |
| "length", |
| "desconto", |
| "discount", |
| }: |
| return "NUMERIC" |
| if name in {"date", "created_at", "updated_at"} or name.endswith("_date"): |
| return "DATE" |
| return "TEXT" |
|
|
|
|
| def normalize_identifier(value): |
| identifier = re.sub(r"\W+", "_", normalize_text(value)).strip("_") |
| if not identifier: |
| return "" |
| if identifier[0].isdigit(): |
| identifier = f"col_{identifier}" |
| return identifier |
|
|
|
|
| def parse_column_definition(raw_column): |
| raw_column = re.sub(r"\b(for me|please|por favor)\b", "", raw_column or "", flags=re.IGNORECASE) |
| raw_column = raw_column.strip(" .;:") |
| if not raw_column: |
| return None |
|
|
| |
| |
| type_matches = list( |
| re.finditer( |
| r"\b(integer|int|numeric|decimal|real|float|double|text|varchar|char|date|datetime|timestamp|boolean|bool)\b", |
| raw_column, |
| flags=re.IGNORECASE, |
| ) |
| ) |
| explicit_type = type_matches[-1] if type_matches else None |
| if explicit_type: |
| name_part = raw_column[: explicit_type.start()].strip() |
| column_type = explicit_type.group(1).upper() |
| if column_type == "INT": |
| column_type = "INTEGER" |
| elif column_type == "BOOL": |
| column_type = "BOOLEAN" |
| elif column_type == "DECIMAL": |
| column_type = "NUMERIC" |
| elif column_type in {"FLOAT", "DOUBLE"}: |
| column_type = "REAL" |
| if not name_part.strip(): |
| column_type = None |
| name_part = raw_column |
| else: |
| name_part = raw_column |
| column_type = None |
|
|
| name_part = re.sub(r"\b(column|field|coluna|campo)\b", "", name_part, flags=re.IGNORECASE) |
| column_name = normalize_identifier(name_part) |
| if not column_name: |
| return None |
| return column_name, column_type or infer_column_type(column_name) |
|
|
|
|
| def split_column_list(columns_text): |
| columns_text = re.sub(r"\s+(and|e)\s+", ",", columns_text or "", flags=re.IGNORECASE) |
| parts = [] |
| type_pattern = ( |
| r"\b(integer|int|numeric|decimal|real|float|double|text|varchar|char|date|datetime|timestamp|boolean|bool)\b" |
| ) |
| type_tokens = { |
| "integer", |
| "int", |
| "numeric", |
| "decimal", |
| "real", |
| "float", |
| "double", |
| "text", |
| "varchar", |
| "char", |
| "date", |
| "datetime", |
| "timestamp", |
| "boolean", |
| "bool", |
| } |
| STOPWORDS = { |
| "to", "from", "into", "as", "for", |
| "o", "a", "os", "de", "do", "da", "dos", "das", |
| } |
| for part in (item.strip() for item in columns_text.split(",") if item.strip()): |
| tokens = [token.strip() for token in re.split(r"\s+", part) if token.strip()] |
| tokens = [t for t in tokens if t.lower() not in STOPWORDS] |
| if not tokens: |
| continue |
| if re.search(type_pattern, part, flags=re.IGNORECASE) and len(tokens) > 2: |
| index = 0 |
| |
| |
| inferrable_names = {"total", "date", "time", "timestamp", "int", "text", "real", "char"} |
| while index < len(tokens): |
| current = tokens[index] |
| next_token = tokens[index + 1].lower() if index + 1 < len(tokens) else "" |
| |
| |
| if next_token in type_tokens and not (current.lower() in inferrable_names and next_token in {"date", "datetime", "timestamp"}): |
| parts.append(f"{current} {tokens[index + 1]}") |
| index += 2 |
| else: |
| parts.append(current) |
| index += 1 |
| continue |
| if re.search(type_pattern, part, flags=re.IGNORECASE): |
| parts.append(part) |
| continue |
| if len(tokens) > 1 and all(re.match(r"^[A-Za-z_][\wàáâãçèéêíóôõúÀÁÂÃÇÈÉÊÍÓÔÕÚ]*$", token) for token in tokens): |
| parts.extend(tokens) |
| else: |
| parts.append(part) |
| return parts |
|
|
|
|
| def format_create_table(table_name, columns): |
| if not table_name or not columns: |
| return "" |
| seen = set() |
| column_lines = [] |
| for column_name, column_type in columns: |
| if column_name in seen: |
| continue |
| seen.add(column_name) |
| column_lines.append(f" {column_name} {column_type}") |
| if not column_lines: |
| return "" |
| return f"CREATE TABLE {table_name} (\n" + ",\n".join(column_lines) + "\n);" |
|
|
|
|
| def parse_create_table_schema(schema): |
| schema = (schema or "").strip() |
| match = re.match( |
| r"^\s*(?:CREATE\s+TABLE\s+)?([A-Za-z_][\w]*)\s*\((.*?)\)\s*;?\s*$", |
| schema, |
| flags=re.IGNORECASE | re.DOTALL, |
| ) |
| if not match: |
| return "", [] |
| table_name = normalize_identifier(match.group(1)) |
| columns = [ |
| parsed |
| for parsed in (parse_column_definition(column) for column in split_column_list(match.group(2))) |
| if parsed |
| ] |
| return table_name, columns |
|
|
|
|
| def extract_create_table_statement(text): |
| cleaned = extract_sql_candidate(text) |
| match = re.search( |
| r"\bCREATE\s+TABLE\s+[A-Za-z_][\w]*\s*\(.*?\)\s*;?", |
| cleaned, |
| flags=re.IGNORECASE | re.DOTALL, |
| ) |
| return clean_generation(match.group(0)) if match else "" |
|
|
|
|
| def last_create_table_from_history(chat_history): |
| for item in reversed(list(chat_history or [])): |
| if not isinstance(item, dict) or item.get("role") != "assistant": |
| continue |
| statement = extract_create_table_statement(item.get("content", "")) |
| if statement: |
| return statement |
| return "" |
|
|
|
|
| def extract_added_columns(message): |
| message = (message or "").strip() |
| patterns = ( |
| r":\s*(.+)$", |
| r"\b(?:add|include|with|adicionar|adicione|adicionando|inclua|incluir|acrescente|ter)\b\s+(?:um\s+|uma\s+|a\s+|an\s+)?(?:novo\s+|nova\s+|new\s+)?(?:column|field|element|coluna|campo|elemento|item)?\s*(.+)$", |
| ) |
| for pattern in patterns: |
| match = re.search(pattern, message, flags=re.IGNORECASE) |
| if not match: |
| continue |
| columns = [ |
| parsed |
| for parsed in (parse_column_definition(column) for column in split_column_list(match.group(1))) |
| if parsed |
| ] |
| if columns: |
| return columns |
| return [] |
|
|
|
|
| def extract_removed_columns(message): |
| message = (message or "").strip() |
| patterns = ( |
| r"\b(?:remove|delete|drop|remova|remover|deletar|exclua|excluir)\b\s+(?:a\s+|o\s+|the\s+)?(?:column|field|element|coluna|campo|elemento|item)?\s*(.+)$", |
| ) |
| for pattern in patterns: |
| match = re.search(pattern, message, flags=re.IGNORECASE) |
| if not match: |
| continue |
| columns = [normalize_identifier(column) for column in split_column_list(match.group(1))] |
| columns = [column for column in columns if column] |
| if columns: |
| return columns |
| return [] |
|
|
|
|
| def is_rename_intent(message): |
| message = (message or "").strip().lower() |
| return bool( |
| re.search( |
| r"\b(rename|edit|change|renomeie|renomear|renomeia|renomeia|altere|mude|muda|troca|trocar)\s+\w+\s+(to|para|as|como|por)\s+\w+", |
| message, |
| flags=re.IGNORECASE, |
| ) |
| ) |
|
|
|
|
| def extract_renamed_columns(message): |
| pattern = ( |
| r"\b(?:rename|edit|change|renomeie|renomear|altere|mude)\s+" |
| r"(\w+)\s+(?:to|para|as|como)\s+(\w+)" |
| ) |
| matches = re.findall(pattern, message or "", flags=re.IGNORECASE) |
| |
| troca_matches = re.findall( |
| r"\btroca\b\s+(\w+)\s+\bpor\b\s+(\w+)", |
| message or "", |
| flags=re.IGNORECASE, |
| ) |
| all_matches = matches + troca_matches |
| return [ |
| (normalize_identifier(old), normalize_identifier(new)) |
| for old, new in all_matches |
| if normalize_identifier(old) and normalize_identifier(new) |
| ] |
|
|
|
|
| def parse_compound_edit(message): |
| """Split a compound prompt into segments and extract add/remove/rename.""" |
| segment_pattern = ( |
| r"\s+(?:and|e)\s+" |
| r"(?=\b(?:add|include|remove|delete|drop|rename|edit|change|" |
| r"adicione|adicionar|inclua|acrescente|remova|remover|deletar|" |
| r"exclua|renomeie|renomear|altere|mude|troca|trocar)\b)" |
| ) |
| segments = re.split(segment_pattern, message or "", flags=re.IGNORECASE) |
|
|
| added, removed, renamed = [], [], [] |
| for seg in segments: |
| seg = seg.strip() |
| if not seg: |
| continue |
| if is_rename_intent(seg): |
| renamed.extend(extract_renamed_columns(seg)) |
| elif re.search( |
| r"\b(remove|delete|drop|remova|remover|deletar|exclua|excluir)\b", |
| seg, |
| flags=re.IGNORECASE, |
| ): |
| removed.extend(extract_removed_columns(seg)) |
| else: |
| cols = extract_added_columns(seg) |
| if cols: |
| added.extend(cols) |
| return added, removed, renamed |
|
|
|
|
| def render_schema_context(schema=""): |
| schema = (schema or "").strip() |
| if not schema: |
| return '<div class="schema-context empty"><span>No active schema</span><code>Select a preset or create a table.</code></div>' |
| label = schema_name_by_value(schema) |
| escaped_schema = html.escape(schema) |
| escaped_label = html.escape(label) |
| return ( |
| '<div class="schema-context">' |
| f'<span>Context: {escaped_label}</span>' |
| f'<code>{escaped_schema}</code>' |
| "</div>" |
| ) |
|
|
|
|
| def query_control_updates(can_generate): |
| context_updates = [gr.update(interactive=True) for _ in range(6)] |
| |
| return [*context_updates, gr.update(interactive=True), gr.update(interactive=True)] |
|
|
|
|
| def render_message(message="", kind="error"): |
| if not message: |
| return '<div class="message-box message-empty"></div>' |
| class_name = "message-ok" if kind == "ok" else "message-error" |
| return f'<div class="message-box {class_name}">{html.escape(str(message))}</div>' |
|
|
|
|
| def load_selected_model(selected_key=FINE_TUNED_MODEL_KEY): |
| selected_key = FINE_TUNED_MODEL_KEY |
| model_def = model_by_key(selected_key) |
| print( |
| f"[LOAD_REQUEST] selected_key={selected_key} model_id={model_def['model_id']}", |
| flush=True, |
| ) |
| yield ( |
| None, |
| render_status(selected_key, None, state="loading"), |
| render_loading_overlay(selected_key, visible=True), |
| model_metadata(selected_key), |
| gr.update(interactive=False, visible=False), |
| *query_control_updates(False), |
| "", |
| EMPTY_VALIDATOR, |
| render_message(), |
| ) |
| started = time.time() |
| try: |
| executor = concurrent.futures.ThreadPoolExecutor(max_workers=1) |
| future = executor.submit(_run_model_load, model_def["model_id"]) |
| try: |
| result = future.result(timeout=LOAD_TIMEOUT_SECONDS) |
| except concurrent.futures.TimeoutError: |
| |
| |
| |
| result = future.result() |
| print(f"[LOAD] Completed after timeout warning ({int(time.time() - started)}s)", flush=True) |
| finally: |
| executor.shutdown(wait=False, cancel_futures=True) |
| except Exception as exc: |
| error = f"{SOURCE_FINE_TUNED_MODEL}. Load failed for {model_def['model_id']}: {type(exc).__name__}: {exc}" |
| print(f"[LOAD_ERROR] {error}", flush=True) |
| traceback.print_exc() |
| yield ( |
| None, |
| render_status(selected_key, None), |
| render_loading_overlay(visible=False), |
| model_metadata(selected_key), |
| gr.update(interactive=True, visible=True), |
| *query_control_updates(False), |
| "", |
| EMPTY_VALIDATOR, |
| render_message(error), |
| ) |
| return |
|
|
| elapsed = int(time.time() - started) |
| yield ( |
| selected_key, |
| render_status(selected_key, selected_key), |
| render_loading_overlay(visible=False), |
| model_metadata(selected_key), |
| gr.update(interactive=True, visible=True, value="Load fine-tuned model"), |
| *query_control_updates(True), |
| "", |
| EMPTY_VALIDATOR, |
| render_message(f"{SOURCE_FINE_TUNED_MODEL}. Loaded {model_def['model_id']} in {elapsed}s.", kind="ok"), |
| ) |
|
|
|
|
| def set_preset(name): |
| schema = PRESETS[name] |
| return schema, render_schema_context(schema), gr.update(visible=True), chat_core.default_state(schema) |
|
|
|
|
| def clear_schema_context(): |
| return "", render_schema_context(""), gr.update(visible=False), chat_core.default_state("") |
|
|
|
|
| def trim_chat_history(chat_history, max_exchanges=10): |
| history = list(chat_history or []) |
| return history[-max_exchanges * 2 :] |
|
|
|
|
| def _append_chat_turn(chat_history, message, assistant_content): |
| return trim_chat_history( |
| [ |
| *list(chat_history or []), |
| {"role": "user", "content": message}, |
| {"role": "assistant", "content": assistant_content}, |
| ] |
| ) |
|
|
|
|
| def _response_tuple( |
| chat_history, |
| message, |
| state, |
| assistant_content, |
| status_message, |
| *, |
| sql_text="", |
| validator=CHAT_VALIDATOR, |
| status_kind="ok", |
| ): |
| state = chat_core.ConversationState.from_value(state) |
| if sql_text and "CREATE TABLE" in sql_text.upper(): |
| state = state.with_active_schema(sql_text) |
| new_history = _append_chat_turn(chat_history, message, assistant_content) |
| return ( |
| new_history, |
| "", |
| state.active_schema, |
| message, |
| sql_text, |
| validator, |
| render_message(status_message, kind=status_kind), |
| state.to_dict(), |
| render_schema_context(state.active_schema), |
| gr.update(visible=bool(state.active_schema.strip())), |
| ) |
|
|
|
|
| def deterministic_response( |
| chat_history, |
| message, |
| active_schema, |
| loaded_key, |
| assistant_content, |
| status_message, |
| *, |
| sql_text="", |
| validator=CHAT_VALIDATOR, |
| status_kind="ok", |
| conversation_state=None, |
| ): |
| state = chat_core.ConversationState.from_value(conversation_state, active_schema=active_schema) |
| return _response_tuple( |
| chat_history, |
| message, |
| state, |
| assistant_content, |
| status_message, |
| sql_text=sql_text, |
| validator=validator, |
| status_kind=status_kind, |
| ) |
|
|
|
|
| def _model_ready(loaded_key): |
| if not loaded_key or _model is None or _tokenizer is None: |
| return False, "Load the fine-tuned model before generating SQL." |
| model_def = model_by_key(loaded_key) |
| if _current_model_id != model_def["model_id"]: |
| return False, "Loaded model state is inconsistent. Reload the selected model." |
| return True, "" |
|
|
|
|
| def _generate_model_text(prompt, generation_kind=model_core.SQL_GENERATION): |
| started = time.time() |
| import_model_runtime() |
| with _model_lock: |
| model = _model |
| tokenizer = _tokenizer |
| if model is None or tokenizer is None: |
| raise RuntimeError("Model runtime is not loaded.") |
|
|
| inputs = tokenizer(prompt, return_tensors="pt") |
| input_length = inputs["input_ids"].shape[-1] |
| generation_config = getattr(model, "generation_config", None) |
| gen_kwargs = { |
| "max_new_tokens": model_core.generation_budget(generation_kind), |
| "max_time": GENERATION_MAX_TIME_SECONDS, |
| "do_sample": False, |
| "use_cache": False, |
| "repetition_penalty": 1.1, |
| "eos_token_id": getattr(generation_config, "eos_token_id", tokenizer.eos_token_id), |
| "pad_token_id": tokenizer.pad_token_id or tokenizer.eos_token_id, |
| } |
| executor = concurrent.futures.ThreadPoolExecutor(max_workers=1) |
| future = executor.submit(_run_generation, model, inputs, gen_kwargs) |
| try: |
| output_ids = future.result(timeout=GENERATION_TIMEOUT_SECONDS) |
| except concurrent.futures.TimeoutError: |
| executor.shutdown(wait=False, cancel_futures=False) |
| raise TimeoutError(f"Generation timed out after {GENERATION_TIMEOUT_SECONDS}s") |
| finally: |
| executor.shutdown(wait=False, cancel_futures=True) |
| generated_ids = output_ids[0][input_length:] |
| generated_text = tokenizer.decode(generated_ids, skip_special_tokens=True) |
| return generated_text, int(time.time() - started) |
|
|
|
|
| def _empty_generation_response( |
| chat_history, |
| message, |
| state, |
| status_message, |
| *, |
| status_kind="error", |
| source_label="", |
| ): |
| status_text = f"{source_label}. {status_message}" if source_label else status_message |
| return ( |
| chat_history, |
| message, |
| state.active_schema, |
| "", |
| "", |
| EMPTY_VALIDATOR, |
| render_message(status_text, kind=status_kind), |
| state.to_dict(), |
| render_schema_context(state.active_schema), |
| gr.update(visible=bool(state.active_schema.strip())), |
| ) |
|
|
|
|
| def generate_response(message, chat_history, active_schema, loaded_key, conversation_state=None): |
| message = (message or "").strip() |
| chat_history = list(chat_history or []) |
| state = chat_core.ConversationState.from_value(conversation_state, active_schema=(active_schema or "")) |
| if not message: |
| return ( |
| chat_history, |
| "", |
| state.active_schema, |
| "", |
| "", |
| EMPTY_VALIDATOR, |
| render_message("Type a message before sending."), |
| state.to_dict(), |
| render_schema_context(state.active_schema), |
| gr.update(visible=bool(state.active_schema.strip())), |
| ) |
|
|
| intent_result = intent_core.classify_intent(message, state, chat_history) |
| state = state.with_intent(intent_result) |
| print( |
| f"[ROUTING] \"{message[:60]}\" -> intent={intent_result.intent} " |
| f"confidence={intent_result.confidence} reason={intent_result.reason}", |
| flush=True, |
| ) |
|
|
| if intent_result.intent == intent_core.EDIT_TABLE: |
| edited_table = sql_core.edit_create_table_from_message(message, chat_history, state.active_schema) |
| if edited_table: |
| display_response = f"```sql\n{edited_table}\n```" |
| return _response_tuple( |
| chat_history, |
| message, |
| state, |
| display_response, |
| f"{SOURCE_DETERMINISTIC_SCHEMA_PARSER}. Edited CREATE TABLE without calling the model.", |
| sql_text=edited_table, |
| validator=sql_core.validate_sql(edited_table), |
| ) |
| if sql_core.last_create_table_from_history(chat_history) or sql_core.create_table_from_schema(state.active_schema): |
| return _empty_generation_response( |
| chat_history, |
| message, |
| state, |
| "No matching schema column was changed.", |
| source_label=SOURCE_DETERMINISTIC_SCHEMA_PARSER, |
| ) |
| return _empty_generation_response( |
| chat_history, |
| message, |
| state, |
| "I need an existing CREATE TABLE in the chat or an active schema before editing columns.", |
| source_label=SOURCE_DETERMINISTIC_SCHEMA_PARSER, |
| ) |
|
|
| if intent_result.intent == intent_core.CREATE_TABLE: |
| sql_text = sql_core.create_table_from_message(message) or sql_core.create_table_from_schema(state.active_schema) |
| if sql_text: |
| display_response = f"```sql\n{sql_text}\n```" |
| return _response_tuple( |
| chat_history, |
| message, |
| state, |
| display_response, |
| f"{SOURCE_DETERMINISTIC_SCHEMA_PARSER}. Generated CREATE TABLE without calling the model.", |
| sql_text=sql_text, |
| validator=sql_core.validate_sql(sql_text), |
| ) |
| return _empty_generation_response( |
| chat_history, |
| message, |
| state, |
| "CREATE TABLE needs a table name and columns.", |
| source_label=SOURCE_DETERMINISTIC_SCHEMA_PARSER, |
| ) |
|
|
| if intent_result.intent == intent_core.UNKNOWN and intent_result.reason == "unsupported_data_mutation": |
| return _response_tuple( |
| chat_history, |
| message, |
| state, |
| UNSUPPORTED_MUTATION_RESPONSE, |
| f"{SOURCE_STATIC_FALLBACK}. Unsupported data mutation - no model call.", |
| sql_text="", |
| validator=EMPTY_VALIDATOR, |
| ) |
|
|
| if intent_result.intent in {intent_core.SMALLTALK, intent_core.UNKNOWN}: |
| return _response_tuple( |
| chat_history, |
| message, |
| state, |
| FALLBACK_RESPONSE, |
| f"{SOURCE_STATIC_FALLBACK}. Static fallback - no model call.", |
| ) |
|
|
| deterministic_sql = sql_core.deterministic_sql_query(message, state.active_schema) |
| if deterministic_sql: |
| return _response_tuple( |
| chat_history, |
| message, |
| state, |
| f"```sql\n{deterministic_sql}\n```", |
| f"{SOURCE_DETERMINISTIC_SQL_TEMPLATE}. Generated SQL with a deterministic SQL template.", |
| sql_text=deterministic_sql, |
| validator=sql_core.validate_sql(deterministic_sql), |
| ) |
|
|
| ready, error = _model_ready(loaded_key) |
| if not ready: |
| return _empty_generation_response( |
| chat_history, |
| message, |
| state, |
| error if "inconsistent" in error else "Load a model before generating SQL.", |
| source_label=SOURCE_FINE_TUNED_MODEL, |
| ) |
|
|
| try: |
| prompt = model_core.build_sql_prompt(state.active_schema, message, chat_history) |
| generated_text, elapsed = _generate_model_text(prompt, model_core.SQL_GENERATION) |
| except Exception as exc: |
| return _empty_generation_response( |
| chat_history, |
| message, |
| state, |
| f"Generation failed: {type(exc).__name__}: {exc}", |
| source_label=SOURCE_FINE_TUNED_MODEL, |
| ) |
|
|
| sql_text, _chat_text, validator = model_core.format_generation_result( |
| generated_text, |
| state.active_schema, |
| ) |
| model_def = model_by_key(loaded_key) |
| if not sql_text: |
| rejection_reason = model_core.model_sql_rejection_reason(generated_text, state.active_schema) |
| rejection_detail = ( |
| f"The fine-tuned model output was rejected because {rejection_reason}." |
| if rejection_reason |
| else "The fine-tuned model output was rejected by SQL/schema guardrails." |
| ) |
| return _response_tuple( |
| chat_history, |
| message, |
| state, |
| rejection_detail, |
| f"{SOURCE_FINE_TUNED_MODEL}. Rejected non-SELECT/WITH model output or schema-invalid model output from {model_def['model_id']} in {elapsed}s.", |
| sql_text="", |
| validator=validator, |
| status_kind="error", |
| ) |
|
|
| display_response = f"```sql\n{sql_text}\n```" |
| return _response_tuple( |
| chat_history, |
| message, |
| state, |
| display_response, |
| f"{SOURCE_FINE_TUNED_MODEL}. Generated SQL with {model_def['model_id']} in {elapsed}s.", |
| sql_text=str(sql_text), |
| validator=validator, |
| ) |
|
|
|
|
| def is_sql_intent(message, schema): |
| return sql_core.is_sql_intent(message, schema) |
|
|
|
|
| def build_generation_prompt(schema, message, chat_history=None): |
| return model_core.build_sql_prompt(schema, message, chat_history) |
|
|
|
|
| def normalize_sql_question_to_english(message, schema=""): |
| return sql_core.normalize_sql_question_to_english(message, schema) |
|
|
|
|
| def format_generation_result(text, schema=""): |
| return model_core.format_generation_result(text, schema) |
|
|
|
|
| def validate_sql(sql_text, schema=""): |
| return sql_core.validate_sql(sql_text, schema) |
|
|
|
|
| def create_table_from_message(message): |
| return sql_core.create_table_from_message(message) |
|
|
|
|
| def create_table_from_schema(schema): |
| return sql_core.create_table_from_schema(schema) |
|
|
|
|
| def edit_create_table_from_message(message, chat_history, active_schema): |
| return sql_core.edit_create_table_from_message(message, chat_history, active_schema) |
|
|
|
|
| def sync_on_load(): |
| if _model is not None and _current_model_id is not None: |
| loaded_key = model_key_by_id(_current_model_id) |
| if loaded_key: |
| return ( |
| loaded_key, |
| render_status(loaded_key, loaded_key), |
| render_loading_overlay(visible=False), |
| model_metadata(loaded_key), |
| gr.update(interactive=True, visible=True, value="Load fine-tuned model"), |
| *query_control_updates(True), |
| "", |
| EMPTY_VALIDATOR, |
| render_message(f"{SOURCE_FINE_TUNED_MODEL}. Model already loaded: {_current_model_id}", kind="ok"), |
| ) |
| return ( |
| None, |
| render_status(DEFAULT_MODEL_KEY, None), |
| render_loading_overlay(visible=False), |
| model_metadata(DEFAULT_MODEL_KEY), |
| gr.update(interactive=True, visible=True), |
| *query_control_updates(False), |
| "", |
| EMPTY_VALIDATOR, |
| render_message(), |
| ) |
|
|
|
|
| CSS = """ |
| @import url('https://fonts.googleapis.com/css2?family=Space+Mono:wght@400;500;700&display=swap'); |
| |
| /* Keep app contrast stable regardless of Spaces light/dark host theme. */ |
| [class*="badge"], |
| [class*="validator-"], |
| [class*="model-tag"], |
| [class*="stat-card"] { |
| color: inherit !important; |
| } |
| |
| :root { |
| color-scheme: dark; |
| --bg-base: #0c0c0b; |
| --bg-surface: #1a1a18; |
| --bg-raised: #242422; |
| --border: rgba(255, 255, 255, 0.1); |
| --border-hi: rgba(255, 255, 255, 0.22); |
| --text-hi: #f2ede4; |
| --text-mid: #9e9a93; |
| --text-lo: #5f5d58; |
| --text-primary: var(--text-hi); |
| --text-secondary: var(--text-mid); |
| --text-muted: var(--text-lo); |
| --teal: #1d9e75; |
| --teal-soft: #dff8ef; |
| --teal-text: #0f6e56; |
| --amber-soft: #faeeda; |
| --amber-text: #854f0b; |
| --overlay-bg: rgba(0, 0, 0, 0.6); |
| --chat-bubble-bot-bg: #242422; |
| --chat-bubble-user-bg: #2b2822; |
| --chat-pending-bg: #d9d9de; |
| --chat-pending-text: #111827; |
| --chat-copy-btn-bg: #1a1a18; |
| } |
| |
| @media (prefers-color-scheme: light) { |
| :root { |
| color-scheme: light; |
| --bg-base: #f4f6fa; |
| --bg-surface: #ffffff; |
| --bg-raised: #edf1f6; |
| --border: rgba(17, 24, 39, 0.12); |
| --border-hi: rgba(17, 24, 39, 0.24); |
| --text-hi: #111827; |
| --text-mid: #374151; |
| --text-lo: #6b7280; |
| --text-primary: var(--text-hi); |
| --text-secondary: var(--text-mid); |
| --text-muted: var(--text-lo); |
| --teal: #0f766e; |
| --teal-soft: #dff8ef; |
| --teal-text: #0f5f4c; |
| --amber-soft: #faeeda; |
| --amber-text: #854f0b; |
| --overlay-bg: rgba(17, 24, 39, 0.28); |
| --chat-bubble-bot-bg: #eef3fb; |
| --chat-bubble-user-bg: #f9f1df; |
| --chat-pending-bg: #e8eef8; |
| --chat-pending-text: #1f2937; |
| --chat-copy-btn-bg: #ffffff; |
| } |
| } |
| |
| * { |
| box-sizing: border-box; |
| } |
| |
| .gradio-container, |
| .gradio-container .main, |
| .gradio-container .wrap, |
| .gradio-container .contain { |
| background: var(--bg-base) !important; |
| color: var(--text-primary) !important; |
| font-family: Space Mono, ui-monospace, SFMono-Regular, Menlo, Consolas, monospace !important; |
| --body-text-color: var(--text-primary) !important; |
| --body-text-color-subdued: var(--text-secondary) !important; |
| --block-title-text-color: var(--text-secondary) !important; |
| --block-label-text-color: var(--text-secondary) !important; |
| --input-placeholder-color: var(--text-muted) !important; |
| --chatbot-bubble-color: var(--chat-bubble-bot-bg) !important; |
| --chatbot-user-bubble-color: var(--chat-bubble-user-bg) !important; |
| --chatbot-user-text-color: var(--text-primary) !important; |
| --chatbot-assistant-text-color: var(--text-primary) !important; |
| --chatbot-text-color: var(--text-primary) !important; |
| } |
| |
| .top-panel h1, |
| .model-card h3, |
| .model-score span, |
| .evidence-copy h2, |
| .evidence-card strong, |
| .loading-title { |
| color: var(--text-primary) !important; |
| } |
| |
| .top-panel p, |
| .step-title, |
| .model-card code, |
| .model-score small, |
| .model-card-footer, |
| .evidence-copy p, |
| .evidence-card span, |
| .evidence-card small, |
| .status-pill, |
| .schema-context, |
| .field-label, |
| .preset-label, |
| .message-box { |
| color: var(--text-secondary) !important; |
| } |
| |
| .app-shell { |
| max-width: 1120px; |
| margin: 22px auto 44px; |
| padding: 0 20px; |
| } |
| |
| .top-panel { |
| align-items: center; |
| background: var(--bg-surface); |
| border: 0.5px solid var(--border); |
| border-radius: 6px; |
| display: grid; |
| gap: 16px; |
| grid-template-columns: minmax(0, 1fr) auto; |
| padding: 14px 16px; |
| } |
| |
| .top-panel h1 { |
| color: var(--text-primary); |
| font-size: 15px; |
| font-weight: 500; |
| letter-spacing: 0; |
| line-height: 1.25; |
| margin: 0 0 4px; |
| } |
| |
| .top-panel p { |
| color: var(--text-secondary); |
| font-size: 13px; |
| font-weight: 400; |
| letter-spacing: 0; |
| line-height: 1.35; |
| margin: 0; |
| } |
| |
| .top-badges { |
| align-items: center; |
| display: flex; |
| flex-wrap: wrap; |
| gap: 8px; |
| justify-content: flex-end; |
| } |
| |
| .badge, |
| .validator-badge, |
| .model-tag { |
| border-radius: 5px; |
| display: inline-flex; |
| font-size: 11px; |
| font-weight: 500; |
| letter-spacing: 0; |
| line-height: 1; |
| padding: 6px 8px; |
| } |
| |
| .badge-green, |
| .validator-ok { |
| background: var(--teal-soft); |
| color: var(--teal-text) !important; |
| } |
| |
| .badge-cream, |
| .validator-warn { |
| background: var(--amber-soft); |
| color: var(--amber-text) !important; |
| } |
| |
| .badge-light, |
| .validator-empty { |
| background: var(--bg-raised); |
| color: var(--text-secondary) !important; |
| border: 0.5px solid var(--border); |
| } |
| |
| .step-title { |
| color: var(--text-secondary); |
| font-size: 11px; |
| font-weight: 500; |
| letter-spacing: 0.08em; |
| line-height: 1; |
| margin: 32px 0 12px; |
| text-transform: uppercase; |
| } |
| |
| .step-title span { |
| display: inline-flex; |
| } |
| |
| .model-grid, |
| .stats-row { |
| display: grid; |
| gap: 12px; |
| grid-template-columns: repeat(2, minmax(0, 1fr)); |
| } |
| |
| .model-grid > div, |
| .stats-row > div { |
| min-width: 0; |
| } |
| |
| .model-card { |
| background: var(--bg-surface); |
| border: 0.5px solid var(--border); |
| border-radius: 6px; |
| min-height: 176px; |
| padding: 16px; |
| transition: border-color 160ms ease, background 160ms ease; |
| } |
| |
| .model-card.selected { |
| border: 1.5px solid var(--teal); |
| } |
| |
| .model-tag { |
| background: var(--amber-soft); |
| color: var(--amber-text) !important; |
| margin-bottom: 18px; |
| } |
| |
| .model-card.selected .model-tag { |
| background: var(--teal-soft); |
| color: var(--teal-text) !important; |
| } |
| |
| .model-card h3 { |
| color: var(--text-primary); |
| font-size: 15px; |
| font-weight: 500; |
| letter-spacing: 0; |
| line-height: 1.3; |
| margin: 0 0 8px; |
| } |
| |
| .model-card code { |
| color: var(--text-secondary); |
| display: block; |
| font-family: inherit; |
| font-size: 12px; |
| font-weight: 400; |
| line-height: 1.35; |
| margin-bottom: 18px; |
| overflow: hidden; |
| text-overflow: ellipsis; |
| white-space: nowrap; |
| } |
| |
| .model-score { |
| align-items: baseline; |
| display: flex; |
| gap: 8px; |
| margin-bottom: 16px; |
| } |
| |
| .model-score span { |
| color: var(--text-primary); |
| font-size: 28px; |
| font-weight: 500; |
| letter-spacing: 0; |
| line-height: 1; |
| } |
| |
| .model-card.selected .model-score span { |
| color: var(--teal) !important; |
| } |
| |
| .model-score small, |
| .model-card-footer { |
| color: var(--text-secondary); |
| font-size: 13px; |
| font-weight: 400; |
| } |
| |
| .model-card-footer { |
| display: flex; |
| } |
| |
| .evidence-panel { |
| background: var(--bg-surface); |
| border: 0.5px solid var(--border); |
| border-radius: 6px; |
| margin-top: 12px; |
| padding: 16px; |
| } |
| |
| .evidence-copy h2 { |
| color: var(--text-primary); |
| font-size: 13px; |
| font-weight: 500; |
| line-height: 1.3; |
| margin: 0 0 6px; |
| } |
| |
| .evidence-copy p { |
| color: var(--text-secondary); |
| font-size: 12px; |
| line-height: 1.45; |
| margin: 0; |
| } |
| |
| .evidence-grid { |
| display: grid; |
| gap: 8px; |
| grid-template-columns: repeat(3, minmax(0, 1fr)); |
| margin-top: 14px; |
| } |
| |
| .evidence-card { |
| background: var(--bg-raised); |
| border: 0.5px solid var(--border); |
| border-radius: 6px; |
| padding: 10px; |
| } |
| |
| .evidence-card.highlighted { |
| border-color: rgba(29, 158, 117, 0.5); |
| } |
| |
| .evidence-card span, |
| .evidence-card small { |
| color: var(--text-secondary); |
| display: block; |
| font-size: 10px; |
| line-height: 1.25; |
| } |
| |
| .evidence-card strong { |
| color: var(--text-primary); |
| display: block; |
| font-size: 20px; |
| font-weight: 500; |
| line-height: 1.1; |
| margin: 5px 0; |
| } |
| |
| #load-button, |
| #generate-button { |
| width: 100% !important; |
| } |
| |
| .gradio-container button { |
| border-radius: 6px !important; |
| font-family: Space Mono, ui-monospace, monospace !important; |
| font-size: 11px !important; |
| font-weight: 500 !important; |
| letter-spacing: 0 !important; |
| transition: background 160ms ease, border-color 160ms ease, color 160ms ease, opacity 160ms ease !important; |
| } |
| |
| #load-button button, |
| #generate-button button { |
| background: var(--bg-raised) !important; |
| border: 0.5px solid var(--border-hi) !important; |
| color: var(--text-primary) !important; |
| min-height: 40px !important; |
| width: 100% !important; |
| } |
| |
| #generate-button button { |
| height: 42px !important; |
| min-height: 42px !important; |
| } |
| |
| #load-button button:hover, |
| #generate-button button:hover { |
| background: var(--text-primary) !important; |
| color: var(--bg-base) !important; |
| } |
| |
| #generate-button button:disabled { |
| opacity: 0.4 !important; |
| } |
| |
| .status-pill { |
| align-items: center; |
| background: var(--bg-surface); |
| border: 0.5px solid var(--border); |
| border-radius: 6px; |
| color: var(--text-secondary); |
| display: inline-flex; |
| font-size: 13px; |
| font-weight: 400; |
| gap: 8px; |
| margin: 12px 0 0; |
| padding: 8px 10px; |
| } |
| |
| .status-pill span { |
| background: var(--text-muted); |
| border-radius: 50%; |
| display: inline-flex; |
| height: 7px; |
| width: 7px; |
| } |
| |
| .status-ready span, |
| .status-loading span { |
| background: var(--teal); |
| } |
| |
| .stats-row { |
| grid-template-columns: repeat(4, minmax(0, 1fr)); |
| margin-top: 12px; |
| } |
| |
| .stat-card { |
| background: var(--bg-surface); |
| border: 0.5px solid var(--border); |
| border-radius: 6px; |
| padding: 12px; |
| } |
| |
| .stat-card strong { |
| color: var(--text-primary) !important; |
| display: block; |
| font-size: 15px; |
| font-weight: 500; |
| line-height: 1.2; |
| margin-bottom: 4px; |
| } |
| |
| .stat-card span { |
| color: var(--text-secondary) !important; |
| display: block; |
| font-size: 11px; |
| font-weight: 400; |
| line-height: 1.25; |
| } |
| |
| .query-section { |
| padding-bottom: 10px; |
| scroll-margin-top: 24px; |
| } |
| |
| .chat-history { |
| background: var(--bg-surface) !important; |
| border: 0.5px solid var(--border) !important; |
| border-radius: 6px !important; |
| margin-bottom: 8px; |
| } |
| |
| .chat-history .bubble-wrap, |
| .chat-history .message, |
| .chat-history .prose { |
| font-family: Space Mono, ui-monospace, monospace !important; |
| font-size: 13px !important; |
| line-height: 1.45 !important; |
| color: var(--text-primary) !important; |
| } |
| |
| .chat-history .message, |
| .chat-history .bubble-wrap .message { |
| background: var(--chat-bubble-bot-bg) !important; |
| border: 0.5px solid var(--border) !important; |
| } |
| |
| .chat-history [data-testid*="user"] .message, |
| .chat-history [class*="user"] .message { |
| background: var(--chat-bubble-user-bg) !important; |
| } |
| |
| .chat-history .message *, |
| .chat-history .prose * { |
| color: var(--text-primary) !important; |
| } |
| |
| .chat-history .message-row { |
| padding: 6px 8px !important; |
| } |
| |
| .chat-history pre, |
| .chat-history code { |
| font-family: Space Mono, ui-monospace, monospace !important; |
| font-size: 12px !important; |
| color: var(--text-primary) !important; |
| } |
| |
| .chat-history [class*="pending"], |
| .chat-history [class*="loading"], |
| .chat-history [class*="typing"], |
| .chat-history [class*="spinner"], |
| .chat-history [class*="generating"] { |
| background: var(--chat-pending-bg) !important; |
| border-color: var(--border) !important; |
| color: var(--chat-pending-text) !important; |
| } |
| |
| .chat-history [class*="pending"] *, |
| .chat-history [class*="loading"] *, |
| .chat-history [class*="typing"] *, |
| .chat-history [class*="spinner"] *, |
| .chat-history [class*="generating"] * { |
| color: var(--chat-pending-text) !important; |
| } |
| |
| .chat-history button, |
| .chat-history [role="button"] { |
| background: var(--chat-copy-btn-bg) !important; |
| border: 0.5px solid var(--border) !important; |
| color: var(--text-secondary) !important; |
| } |
| |
| .chat-history button:hover, |
| .chat-history [role="button"]:hover { |
| border-color: var(--border-hi) !important; |
| color: var(--text-primary) !important; |
| } |
| |
| .schema-context-row { |
| align-items: center; |
| gap: 6px !important; |
| margin: 2px 0 4px; |
| } |
| |
| .schema-context { |
| align-items: center; |
| background: var(--schema-context-bg, var(--bg-surface)); |
| border: 0.5px solid var(--border); |
| border-radius: 6px; |
| color: var(--text-secondary); |
| display: inline-flex; |
| gap: 8px; |
| min-height: 28px; |
| padding: 5px 10px; |
| max-width: 100%; |
| } |
| |
| .schema-context.empty { |
| background: transparent; |
| border-style: dashed; |
| color: var(--text-muted); |
| min-height: 24px; |
| padding: 4px 8px; |
| } |
| |
| .schema-context.empty span { |
| color: var(--text-muted) !important; |
| font-size: 11px; |
| flex-shrink: 0; |
| } |
| |
| .schema-context.empty code { |
| color: var(--text-muted); |
| font-size: 10px; |
| font-style: italic; |
| overflow: hidden; |
| text-overflow: ellipsis; |
| white-space: nowrap; |
| } |
| |
| .schema-context span { |
| color: var(--teal) !important; |
| font-size: 11px; |
| font-weight: 600; |
| white-space: nowrap; |
| flex-shrink: 0; |
| } |
| |
| .schema-context code { |
| background: var(--code-bg); |
| border: 0.5px solid var(--code-border); |
| border-radius: 4px; |
| color: var(--code-text); |
| display: block; |
| font-family: var(--font-code, "JetBrains Mono", ui-monospace, monospace); |
| font-size: 10px; |
| line-height: 1.4; |
| overflow: hidden; |
| padding: 3px 6px; |
| text-overflow: ellipsis; |
| white-space: nowrap; |
| min-width: 0; |
| } |
| |
| .schema-strip { |
| align-items: center !important; |
| flex-wrap: wrap !important; |
| gap: 4px !important; |
| margin: 4px 0 0 !important; |
| padding: 0 !important; |
| } |
| |
| .schema-strip > div { |
| flex: 0 0 auto !important; |
| min-width: 0 !important; |
| } |
| |
| .schema-strip button { |
| background: var(--bg-raised) !important; |
| border: 0.5px solid var(--border) !important; |
| border-radius: 999px !important; |
| color: var(--text-secondary) !important; |
| font-size: 10px !important; |
| min-height: 22px !important; |
| padding: 0 8px !important; |
| } |
| |
| .schema-strip button:hover { |
| border-color: var(--border-hi) !important; |
| color: var(--text-primary) !important; |
| } |
| |
| .schema-strip-label { |
| color: var(--text-muted); |
| font-size: 10px; |
| font-weight: 500; |
| letter-spacing: 0.06em; |
| text-transform: uppercase; |
| white-space: nowrap; |
| } |
| |
| .composer-row { |
| align-items: flex-end !important; |
| display: flex !important; |
| gap: 6px !important; |
| padding-bottom: 12px !important; |
| } |
| |
| .composer-row > div { |
| display: flex !important; |
| flex-direction: column !important; |
| justify-content: flex-end !important; |
| } |
| |
| #message-input { |
| flex: 1 1 auto; |
| } |
| |
| #message-input textarea { |
| min-height: 42px !important; |
| max-height: 120px !important; |
| height: 42px !important; |
| transform: translateY(10px) !important; |
| resize: none !important; |
| overflow-y: auto !important; |
| } |
| |
| #generate-button { |
| align-self: flex-end !important; |
| height: 42px !important; |
| margin-bottom: 0 !important; |
| margin-top: 0 !important; |
| min-height: 42px !important; |
| } |
| |
| #generate-button button { |
| height: 42px !important; |
| min-height: 42px !important; |
| margin-bottom: 0 !important; |
| } |
| |
| #clear-schema-button button { |
| background: transparent !important; |
| border: 0.5px solid var(--border) !important; |
| color: var(--text-secondary) !important; |
| min-height: 30px !important; |
| min-width: 34px !important; |
| width: 34px !important; |
| } |
| |
| #clear-schema-button button:hover { |
| border-color: var(--border-hi) !important; |
| color: var(--text-primary) !important; |
| } |
| |
| .preset-row-gradio { |
| gap: 6px !important; |
| margin-bottom: 6px; |
| } |
| |
| .preset-row-gradio button { |
| background: var(--bg-raised) !important; |
| border: 0.5px solid var(--border) !important; |
| border-radius: 999px !important; |
| color: var(--text-secondary) !important; |
| font-size: 11px !important; |
| min-height: 26px !important; |
| padding: 0 10px !important; |
| } |
| |
| /* Queue/processing indicators aligned to the dark UI. */ |
| .gradio-container [data-testid="status-tracker"], |
| .gradio-container [class*="status-tracker"] { |
| background: var(--bg-raised) !important; |
| border: 0.5px solid var(--border) !important; |
| border-radius: 6px !important; |
| color: var(--text-secondary) !important; |
| font-size: 10px !important; |
| letter-spacing: 0.08em; |
| padding: 4px 8px !important; |
| text-transform: uppercase; |
| } |
| |
| .gradio-container [data-testid="status-tracker"] * { |
| color: var(--text-secondary) !important; |
| } |
| |
| .gradio-container [class*="processing"], |
| .gradio-container [class*="queue"], |
| .gradio-container [class*="progress"] { |
| color: var(--text-secondary) !important; |
| } |
| |
| .gradio-container [class*="spinner"], |
| .gradio-container [class*="spinner"] * { |
| color: var(--teal) !important; |
| } |
| |
| /* SVG loading spinners - override Gradio default fill/stroke */ |
| .gradio-container svg[class*="spinner"], |
| .gradio-container svg[class*="loading"], |
| .gradio-container [data-testid="loader"] svg, |
| .gradio-container [class*="loader"] svg { |
| stroke: var(--teal) !important; |
| fill: none !important; |
| } |
| |
| .gradio-container svg[class*="spinner"] circle, |
| .gradio-container svg[class*="spinner"] path, |
| .gradio-container [data-testid="loader"] circle, |
| .gradio-container [data-testid="loader"] path { |
| stroke: var(--teal) !important; |
| fill: none !important; |
| } |
| |
| /* Hide default Gradio loading animation and replace with styled container */ |
| .gradio-container [data-testid="loader"], |
| .gradio-container [class*="loader"] { |
| opacity: 1 !important; |
| background: transparent !important; |
| } |
| |
| /* Hide skeleton/shimmer animations that don't match dark theme */ |
| .gradio-container [class*="skeleton"], |
| .gradio-container [class*="shimmer"], |
| .gradio-container [class*="pulse"] { |
| background: var(--bg-raised) !important; |
| animation: none !important; |
| } |
| |
| .preset-row-gradio button:hover { |
| border-color: var(--border-hi) !important; |
| color: var(--text-primary) !important; |
| } |
| |
| .gradio-container .block, |
| .gradio-container .form, |
| .gradio-container .panel { |
| background: transparent !important; |
| border: 0 !important; |
| } |
| |
| .gradio-container label, |
| .gradio-container .label-wrap span { |
| color: var(--text-secondary) !important; |
| font-family: Space Mono, ui-monospace, monospace !important; |
| font-size: 11px !important; |
| font-weight: 500 !important; |
| } |
| |
| textarea, |
| input, |
| .cm-editor { |
| background: var(--bg-raised) !important; |
| border: 0.5px solid var(--border) !important; |
| border-radius: 6px !important; |
| color: var(--text-primary) !important; |
| font-family: Space Mono, ui-monospace, monospace !important; |
| font-size: 13px !important; |
| font-weight: 400 !important; |
| line-height: 1.45 !important; |
| } |
| |
| textarea::placeholder, |
| input::placeholder { |
| color: var(--text-muted) !important; |
| } |
| |
| textarea { |
| min-height: 132px !important; |
| } |
| |
| .section-divider { |
| border-top: 0.5px solid var(--border); |
| margin: 28px 0 0; |
| } |
| |
| .output-shell { |
| background: var(--bg-surface); |
| border: 0.5px solid var(--border); |
| border-radius: 6px; |
| margin-top: 12px; |
| overflow: hidden; |
| } |
| |
| .output-head { |
| align-items: center; |
| background: var(--bg-surface); |
| border-bottom: 0.5px solid var(--border); |
| display: flex; |
| justify-content: space-between; |
| min-height: 34px; |
| padding: 0 12px; |
| } |
| |
| .output-head span:first-child { |
| color: var(--text-secondary); |
| font-size: 11px; |
| font-weight: 500; |
| letter-spacing: 0.08em; |
| text-transform: uppercase; |
| } |
| |
| .validator-detail { |
| color: var(--text-secondary) !important; |
| font-size: 11px; |
| margin-left: 8px; |
| } |
| |
| .output-shell .cm-editor, |
| .output-shell pre, |
| .output-shell code { |
| border: 0 !important; |
| font-size: 12px !important; |
| font-weight: 400 !important; |
| } |
| |
| .message-box { |
| color: var(--text-secondary); |
| font-size: 11px; |
| font-weight: 400; |
| min-height: 24px; |
| padding-top: 8px; |
| } |
| |
| .message-error { |
| color: var(--amber-text); |
| } |
| |
| .message-ok { |
| color: var(--teal); |
| } |
| |
| .loading-overlay { |
| align-items: center; |
| background: var(--overlay-bg); |
| bottom: 0; |
| display: flex; |
| justify-content: center; |
| left: 0; |
| position: fixed; |
| right: 0; |
| top: 0; |
| z-index: 1000; |
| } |
| |
| .loading-overlay.hidden { |
| display: none; |
| } |
| |
| .loading-card { |
| background: var(--bg-surface); |
| border: 0.5px solid var(--border-hi); |
| border-radius: 6px; |
| max-width: 480px; |
| padding: 18px; |
| width: min(90vw, 480px); |
| } |
| |
| .loading-title { |
| color: var(--text-primary); |
| font-size: 13px; |
| font-weight: 500; |
| line-height: 1.35; |
| margin-bottom: 12px; |
| } |
| |
| .loading-line { |
| background: var(--bg-raised); |
| border-radius: 999px; |
| height: 8px; |
| overflow: hidden; |
| } |
| |
| .loading-line span { |
| animation: loadingPulse 1.3s infinite ease-in-out; |
| background: var(--teal); |
| border-radius: inherit; |
| display: block; |
| height: 100%; |
| width: 45%; |
| } |
| |
| .loading-card p { |
| color: var(--text-secondary); |
| font-size: 11px; |
| font-weight: 400; |
| line-height: 1.4; |
| margin: 12px 0 0; |
| } |
| |
| @keyframes loadingPulse { |
| 0% { transform: translateX(-70%); } |
| 50% { transform: translateX(70%); } |
| 100% { transform: translateX(220%); } |
| } |
| |
| @media (max-width: 860px) { |
| .top-panel, |
| .model-grid, |
| .evidence-grid { |
| grid-template-columns: 1fr; |
| } |
| |
| .stats-row { |
| grid-template-columns: repeat(2, minmax(0, 1fr)); |
| } |
| |
| .top-badges { |
| justify-content: flex-start; |
| } |
| |
| .app-shell { |
| padding: 0 14px; |
| } |
| } |
| |
| /* Finalized Lab Workbench direction: dark-first technical UI, source contract always visible. */ |
| @import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap'); |
| |
| :root { |
| color-scheme: dark; |
| --bg-base: #101214; |
| --bg-surface: #1b2027; |
| --bg-raised: #242b35; |
| --border: #2d3642; |
| --border-hi: #3c4654; |
| --text-hi: #edf1f5; |
| --text-mid: #a8b3c1; |
| --text-lo: #8491a2; |
| --text-primary: var(--text-hi); |
| --text-secondary: var(--text-mid); |
| --text-muted: var(--text-lo); |
| --teal: #1d9e75; |
| --teal-soft: #153f34; |
| --teal-text: #87e8c5; |
| --amber-soft: #473111; |
| --amber-text: #f0c878; |
| --overlay-bg: rgba(0, 0, 0, 0.55); |
| --chat-bubble-bot-bg: #242b35; |
| --chat-bubble-user-bg: #1c252f; |
| --chat-pending-bg: #27313d; |
| --chat-pending-text: #edf1f5; |
| --chat-copy-btn-bg: #1b2027; |
| --code-bg: #0b1020; |
| --code-border: #26344d; |
| --code-text: #d7e3f3; |
| --schema-context-bg: #10251f; |
| --disabled-bg: #536171; |
| --font-ui: "IBM Plex Sans", ui-sans-serif, sans-serif; |
| --font-code: "JetBrains Mono", ui-monospace, monospace; |
| } |
| |
| @media (prefers-color-scheme: light) { |
| :root { |
| color-scheme: light; |
| --bg-base: #edf2f7; |
| --bg-surface: #ffffff; |
| --bg-raised: #f4f7fb; |
| --border: #d8e0ea; |
| --border-hi: #c5cfdd; |
| --text-hi: #172033; |
| --text-mid: #667085; |
| --text-lo: #8a95a6; |
| --teal: #0d8b67; |
| --teal-soft: #dff6ec; |
| --teal-text: #076348; |
| --amber-soft: #fff3d8; |
| --amber-text: #7a4700; |
| --overlay-bg: rgba(23, 32, 51, 0.28); |
| --chat-bubble-bot-bg: #f4f7fb; |
| --chat-bubble-user-bg: #edf6ff; |
| --chat-pending-bg: #e8eef8; |
| --chat-pending-text: #172033; |
| --chat-copy-btn-bg: #ffffff; |
| --code-bg: #101828; |
| --code-border: #25344d; |
| --schema-context-bg: #f3fbf8; |
| --disabled-bg: #b7c3cf; |
| } |
| } |
| |
| .gradio-container, |
| .gradio-container .main, |
| .gradio-container .wrap, |
| .gradio-container .contain { |
| background: var(--bg-base) !important; |
| font-family: var(--font-ui) !important; |
| } |
| |
| .app-shell { |
| max-width: 1240px; |
| padding: 0 24px; |
| } |
| |
| .header-wrapper { |
| border: 0 !important; |
| padding: 0 !important; |
| } |
| |
| .theme-style-wrapper { |
| display: none !important; |
| } |
| |
| .html-container:has(.top-panel) { |
| padding: 0 !important; |
| } |
| |
| .top-panel, |
| .model-card, |
| .stats-row, |
| .source-panel, |
| .example-panel, |
| .evidence-panel, |
| .chat-history, |
| .output-shell { |
| background: var(--bg-surface) !important; |
| border: 1px solid var(--border) !important; |
| border-radius: 8px !important; |
| box-shadow: none !important; |
| } |
| |
| .top-panel { |
| padding: 16px !important; |
| } |
| |
| .top-panel h1 { |
| font-family: var(--font-ui) !important; |
| font-size: 20px !important; |
| font-weight: 700 !important; |
| letter-spacing: 0 !important; |
| } |
| |
| .top-panel p { |
| font-family: var(--font-ui) !important; |
| font-size: 15px !important; |
| line-height: 1.45 !important; |
| max-width: 720px; |
| } |
| |
| .badge, |
| .validator-badge, |
| .model-tag, |
| .status-pill, |
| .schema-context code, |
| .preset-row-gradio button, |
| .output-head span:first-child, |
| .panel-heading span, |
| .source-row strong, |
| .example-group code { |
| font-family: var(--font-code) !important; |
| } |
| |
| .badge, |
| .validator-badge, |
| .model-tag { |
| border-radius: 6px !important; |
| font-size: 12px !important; |
| font-weight: 600 !important; |
| min-height: 28px !important; |
| padding: 7px 9px !important; |
| } |
| |
| .workbench-grid { |
| align-items: start !important; |
| display: grid !important; |
| gap: 16px !important; |
| grid-template-columns: minmax(0, 1.6fr) 320px !important; |
| margin-top: 16px !important; |
| } |
| |
| .main-stack, |
| .context-rail { |
| display: grid !important; |
| gap: 10px !important; |
| } |
| |
| .context-rail { |
| align-content: start !important; |
| } |
| |
| .model-side-panel { |
| gap: 6px !important; |
| } |
| |
| .source-panel { |
| position: sticky; |
| top: 16px; |
| z-index: 2; |
| } |
| |
| .mobile-source-panel { |
| display: none !important; |
| } |
| |
| .mobile-source-wrapper { |
| display: none !important; |
| } |
| |
| .panel-heading { |
| align-items: center; |
| border-bottom: 1px solid var(--border); |
| display: flex; |
| gap: 12px; |
| justify-content: space-between; |
| min-height: 36px; |
| padding: 0 12px; |
| } |
| |
| .panel-heading h2 { |
| color: var(--text-primary) !important; |
| flex: 0 0 auto; |
| font-size: 12px; |
| font-weight: 700; |
| letter-spacing: 0.06em; |
| line-height: 1; |
| margin: 0; |
| text-transform: uppercase; |
| white-space: nowrap; |
| } |
| |
| .panel-heading span { |
| color: var(--text-secondary) !important; |
| font-size: 10px; |
| min-width: 0; |
| overflow: hidden; |
| text-align: right; |
| text-overflow: ellipsis; |
| white-space: nowrap; |
| } |
| |
| .source-list, |
| .example-list { |
| display: grid; |
| gap: 6px; |
| padding: 10px 12px 12px; |
| } |
| |
| .example-list { |
| grid-template-columns: repeat(2, minmax(0, 1fr)); |
| } |
| |
| .source-row { |
| border-bottom: 1px solid var(--border); |
| display: grid; |
| gap: 3px; |
| padding-bottom: 6px; |
| } |
| |
| .source-row:last-child { |
| border-bottom: 0; |
| padding-bottom: 0; |
| } |
| |
| .source-row strong { |
| color: var(--text-primary) !important; |
| font-size: 11px; |
| } |
| |
| .source-row span, |
| .example-group span, |
| .evidence-copy p { |
| color: var(--text-secondary) !important; |
| font-size: 11px !important; |
| line-height: 1.3 !important; |
| } |
| |
| .example-group { |
| display: grid; |
| gap: 5px; |
| } |
| |
| .example-group code { |
| background: var(--bg-raised); |
| border: 1px solid var(--border); |
| border-radius: 6px; |
| color: var(--text-primary) !important; |
| font-size: 11px; |
| line-height: 1.35; |
| padding: 7px 8px; |
| white-space: normal; |
| } |
| |
| .model-card { |
| min-height: auto !important; |
| padding: 12px !important; |
| } |
| |
| .model-card code { |
| font-family: var(--font-code) !important; |
| white-space: normal !important; |
| } |
| |
| .model-score span { |
| color: var(--teal) !important; |
| font-size: 24px !important; |
| } |
| |
| .stats-row { |
| gap: 8px !important; |
| grid-template-columns: repeat(2, minmax(0, 1fr)) !important; |
| padding: 12px !important; |
| } |
| |
| .stat-card { |
| background: var(--bg-raised) !important; |
| border: 1px solid var(--border) !important; |
| padding: 10px !important; |
| } |
| |
| #load-button, |
| #generate-button { |
| --button-primary-background-fill: var(--teal) !important; |
| --button-primary-background-fill-hover: #076348 !important; |
| --button-primary-text-color: #ffffff !important; |
| background: var(--teal) !important; |
| background-color: var(--teal) !important; |
| background-image: none !important; |
| border: 0 !important; |
| color: var(--bg-surface) !important; |
| font-family: var(--font-ui) !important; |
| font-size: 13px !important; |
| font-weight: 700 !important; |
| min-height: 44px !important; |
| } |
| |
| #generate-button { |
| background: var(--teal) !important; |
| color: #ffffff !important; |
| } |
| |
| .composer-row #generate-button { |
| height: 42px !important; |
| margin-top: 0 !important; |
| min-height: 42px !important; |
| } |
| |
| .composer-row #generate-button button { |
| height: 42px !important; |
| min-height: 42px !important; |
| } |
| |
| #load-button:hover, |
| #generate-button:hover { |
| opacity: 0.88 !important; |
| } |
| |
| .status-pill { |
| align-items: center !important; |
| background: var(--bg-surface) !important; |
| border: 1px solid var(--border) !important; |
| border-radius: 8px !important; |
| display: flex !important; |
| font-family: var(--font-ui) !important; |
| font-size: 13px !important; |
| line-height: 1.4 !important; |
| margin: 0 !important; |
| min-height: 44px !important; |
| padding: 10px 12px !important; |
| width: 100%; |
| } |
| |
| .model-side-panel { |
| display: grid !important; |
| gap: 12px !important; |
| } |
| |
| .query-section { |
| background: var(--bg-surface); |
| border: 1px solid var(--border); |
| border-radius: 8px; |
| overflow: hidden; |
| scroll-margin-top: 24px; |
| } |
| |
| .chat-section-heading { |
| border-bottom: 1px solid var(--border); |
| } |
| |
| .chat-history { |
| border: 0 !important; |
| border-radius: 0 !important; |
| margin-bottom: 0 !important; |
| } |
| |
| .chat-history .bubble-wrap, |
| .chat-history .message, |
| .chat-history .prose { |
| font-family: var(--font-ui) !important; |
| font-size: 15px !important; |
| line-height: 1.5 !important; |
| } |
| |
| .chat-history .message, |
| .chat-history .bubble-wrap .message { |
| background: var(--chat-bubble-bot-bg) !important; |
| border: 1px solid var(--border) !important; |
| border-radius: 8px !important; |
| } |
| |
| .chat-history [data-testid*="user"] .message, |
| .chat-history [class*="user"] .message { |
| background: var(--chat-bubble-user-bg) !important; |
| } |
| |
| .field-label { |
| border-top: 1px solid var(--border); |
| color: var(--text-secondary) !important; |
| font-family: var(--font-ui) !important; |
| font-size: 13px !important; |
| letter-spacing: 0 !important; |
| margin: 0 !important; |
| padding: 6px 16px 0; |
| text-transform: none !important; |
| } |
| |
| .preset-row-gradio { |
| border-bottom: 1px solid var(--border); |
| gap: 6px !important; |
| margin: 0 !important; |
| padding: 6px 16px !important; |
| } |
| |
| .preset-row-gradio button { |
| background: var(--bg-raised) !important; |
| border: 1px solid var(--border) !important; |
| color: var(--text-secondary) !important; |
| min-height: 28px !important; |
| padding: 2px 10px !important; |
| font-size: 12px !important; |
| } |
| |
| .schema-pill-container { |
| flex: 1 1 auto !important; |
| min-width: 0 !important; |
| overflow: hidden !important; |
| } |
| |
| .schema-context-row { |
| background: var(--schema-context-bg); |
| border-bottom: 1px solid var(--border); |
| margin: 0 !important; |
| padding: 6px 16px !important; |
| } |
| |
| |
| #clear-schema-button { |
| flex: 0 0 auto !important; |
| min-width: 0 !important; |
| } |
| |
| .schema-context-row:has(.schema-context.empty) #clear-schema-button { |
| display: none !important; |
| } |
| |
| #clear-schema-button button { |
| min-height: 28px !important; |
| padding: 0 12px !important; |
| font-size: 12px !important; |
| } |
| |
| .composer-row { |
| border-top: 1px solid var(--border); |
| gap: 6px !important; |
| padding: 8px 16px 14px !important; |
| } |
| |
| .gradio-container label, |
| .gradio-container .label-wrap span { |
| font-family: var(--font-ui) !important; |
| } |
| |
| textarea, |
| input, |
| .cm-editor { |
| background: var(--bg-surface) !important; |
| border: 1px solid var(--border-hi) !important; |
| border-radius: 6px !important; |
| color: var(--text-primary) !important; |
| font-family: var(--font-ui) !important; |
| font-size: 14px !important; |
| } |
| |
| .output-shell { |
| background: var(--code-bg) !important; |
| border-color: var(--code-border) !important; |
| margin-top: 0 !important; |
| } |
| |
| .output-head { |
| background: var(--code-bg) !important; |
| border-bottom: 1px solid var(--code-border) !important; |
| min-height: 44px !important; |
| padding: 0 16px !important; |
| } |
| |
| .output-head span:first-child, |
| .output-head .validator-detail { |
| color: #c3cfdd !important; |
| } |
| |
| .output-shell .cm-editor, |
| .output-shell pre, |
| .output-shell code { |
| background: var(--code-bg) !important; |
| color: var(--code-text) !important; |
| font-family: var(--font-code) !important; |
| font-size: 13px !important; |
| line-height: 1.6 !important; |
| } |
| |
| .message-box { |
| font-family: var(--font-ui) !important; |
| font-size: 13px !important; |
| padding-top: 0 !important; |
| } |
| |
| .message-output-wrapper:has(.message-empty) { |
| display: none !important; |
| } |
| |
| .evidence-panel { |
| margin-top: 0 !important; |
| padding: 0 !important; |
| } |
| |
| .evidence-copy { |
| padding: 14px 16px 0; |
| } |
| |
| .evidence-grid { |
| grid-template-columns: repeat(3, minmax(0, 1fr)) !important; |
| margin-top: 0 !important; |
| padding: 14px 16px 16px; |
| } |
| |
| .evidence-card { |
| background: var(--bg-raised) !important; |
| border: 1px solid var(--border) !important; |
| } |
| |
| @media (max-width: 1024px) { |
| .workbench-grid { |
| grid-template-columns: 1fr !important; |
| } |
| |
| .source-panel { |
| position: static; |
| } |
| |
| .mobile-source-panel { |
| display: block !important; |
| } |
| |
| .mobile-source-wrapper { |
| display: block !important; |
| margin-top: 16px !important; |
| } |
| |
| .context-rail .source-panel { |
| display: none !important; |
| } |
| } |
| |
| @media (max-width: 768px) { |
| .app-shell { |
| padding: 0 16px; |
| } |
| |
| .top-panel { |
| grid-template-columns: 1fr !important; |
| } |
| |
| .top-badges { |
| justify-content: flex-start !important; |
| } |
| |
| .composer-row { |
| align-items: stretch !important; |
| flex-direction: column !important; |
| } |
| |
| #generate-button { |
| align-self: stretch !important; |
| margin-top: 0 !important; |
| transform: none; |
| } |
| |
| #message-input textarea { |
| transform: none !important; |
| } |
| |
| .evidence-grid { |
| grid-template-columns: 1fr !important; |
| } |
| } |
| |
| .metadata-panel { |
| background: var(--bg-surface) !important; |
| border: 1px solid var(--border) !important; |
| border-radius: 8px !important; |
| box-shadow: none !important; |
| } |
| |
| .metadata-body { |
| display: grid; |
| gap: 8px; |
| padding: 10px 12px 12px; |
| } |
| |
| .metadata-source-list { |
| gap: 6px !important; |
| padding: 0 !important; |
| } |
| |
| .metadata-source-list .source-row { |
| padding-bottom: 6px; |
| } |
| |
| .metadata-source-list .source-row strong { |
| font-size: 11px; |
| } |
| |
| .metadata-source-list .source-row span { |
| font-size: 11px !important; |
| line-height: 1.3 !important; |
| } |
| |
| .stats-row-compact { |
| gap: 5px !important; |
| grid-template-columns: repeat(2, minmax(0, 1fr)) !important; |
| margin-top: 0 !important; |
| padding: 0 !important; |
| } |
| |
| .stats-row-compact .stat-card { |
| min-height: 58px; |
| padding: 7px !important; |
| } |
| |
| .stats-row-compact .stat-card strong { |
| display: block; |
| font-size: 12px !important; |
| line-height: 1.15; |
| margin-bottom: 4px; |
| } |
| |
| .stats-row-compact .stat-card span { |
| display: block; |
| font-size: 9px !important; |
| line-height: 1.25 !important; |
| } |
| |
| .model-card { |
| min-height: auto !important; |
| padding: 12px !important; |
| } |
| |
| .model-card code { |
| margin-bottom: 12px !important; |
| } |
| |
| .model-score { |
| margin-bottom: 10px !important; |
| } |
| |
| .model-card-footer { |
| margin-bottom: 10px; |
| } |
| |
| .model-card-evidence { |
| border-top: 1px solid var(--border); |
| display: grid; |
| gap: 6px; |
| margin-top: 8px; |
| padding-top: 8px; |
| } |
| |
| .model-card-evidence-heading { |
| color: var(--text-primary) !important; |
| font-size: 11px; |
| font-weight: 700; |
| letter-spacing: 0.04em; |
| text-transform: uppercase; |
| } |
| |
| .model-card-evidence-grid { |
| display: grid; |
| gap: 4px; |
| grid-template-columns: repeat(3, minmax(0, 1fr)); |
| } |
| |
| .model-card-evidence-chip { |
| background: var(--bg-raised) !important; |
| border: 1px solid var(--border) !important; |
| border-radius: 6px; |
| padding: 7px; |
| } |
| |
| .model-card-evidence-chip.highlighted { |
| border-color: rgba(29, 158, 117, 0.5); |
| } |
| |
| .model-card-evidence-chip strong { |
| color: var(--text-primary) !important; |
| display: block; |
| font-size: 12px; |
| line-height: 1.15; |
| margin-bottom: 3px; |
| } |
| |
| .model-card-evidence-chip small { |
| color: var(--text-secondary) !important; |
| display: block; |
| font-size: 9px; |
| line-height: 1.15; |
| } |
| |
| .context-rail { |
| gap: 10px !important; |
| } |
| |
| .model-side-panel { |
| gap: 6px !important; |
| } |
| |
| .panel-heading { |
| min-height: 36px; |
| padding: 0 12px; |
| } |
| |
| .panel-heading h2 { |
| font-size: 12px; |
| } |
| |
| .panel-heading span { |
| font-size: 10px; |
| } |
| |
| .example-panel .example-list { |
| grid-template-columns: 1fr; |
| padding: 10px 12px 12px; |
| } |
| |
| .example-group code { |
| padding: 7px 8px; |
| } |
| |
| /* Late alignment overrides for Gradio wrapper defaults. */ |
| .gradio-container .header-wrapper { |
| padding: 0 !important; |
| } |
| |
| .gradio-container #load-button, |
| .gradio-container #generate-button { |
| --button-primary-background-fill: var(--teal) !important; |
| --button-primary-background-fill-hover: #076348 !important; |
| --button-primary-text-color: #ffffff !important; |
| background: var(--teal) !important; |
| background-color: var(--teal) !important; |
| background-image: none !important; |
| box-shadow: none !important; |
| color: #ffffff !important; |
| } |
| |
| .gradio-container #generate-button:disabled { |
| background: var(--disabled-bg) !important; |
| color: #ffffff !important; |
| opacity: 1 !important; |
| } |
| |
| .gradio-container .message-output-wrapper:has(.message-empty) { |
| display: none !important; |
| } |
| |
| /* Keep dark as the fallback default; only explicit light devices get the light workbench tokens. */ |
| @media (prefers-color-scheme: light) { |
| :root, |
| .gradio-container, |
| * { |
| color-scheme: light; |
| --bg-base: #edf2f7; |
| --bg-surface: #ffffff; |
| --bg-raised: #f4f7fb; |
| --border: #d8e0ea; |
| --border-hi: #c5cfdd; |
| --text-hi: #172033; |
| --text-mid: #667085; |
| --text-lo: #8a95a6; |
| --text-primary: var(--text-hi); |
| --text-secondary: var(--text-mid); |
| --text-muted: var(--text-lo); |
| --teal: #0d8b67; |
| --teal-soft: #dff6ec; |
| --teal-text: #076348; |
| --amber-soft: #fff3d8; |
| --amber-text: #7a4700; |
| --overlay-bg: rgba(23, 32, 51, 0.28); |
| --chat-bubble-bot-bg: #f4f7fb; |
| --chat-bubble-user-bg: #edf6ff; |
| --chat-pending-bg: #e8eef8; |
| --chat-pending-text: #172033; |
| --chat-copy-btn-bg: #ffffff; |
| --code-bg: #101828; |
| --code-border: #25344d; |
| --code-text: #d7e3f3; |
| --schema-context-bg: #f3fbf8; |
| --disabled-bg: #b7c3cf; |
| } |
| } |
| """ |
|
|
| with gr.Blocks(title="Phi-3 Mini SQL Chatbot") as demo: |
| loaded_key_state = gr.State(value=None) |
| active_schema = gr.State(value="") |
| conversation_state = gr.State(value=chat_core.default_state()) |
| last_user_message = gr.State(value="") |
|
|
| with gr.Column(elem_classes=["app-shell"]): |
| loading_overlay = gr.HTML(render_loading_overlay(visible=False)) |
| gr.HTML(render_theme_detection_style(), elem_classes=["theme-style-wrapper"]) |
| gr.HTML(render_header(), elem_classes=["header-wrapper"]) |
| gr.HTML(render_source_legend("mobile-source-panel"), elem_classes=["mobile-source-wrapper"]) |
|
|
| with gr.Row(elem_classes=["workbench-grid"]): |
| with gr.Column(elem_classes=["main-stack"]): |
| with gr.Column(elem_id="query-section", elem_classes=["query-section"]): |
| gr.HTML( |
| '<div class="panel-heading chat-section-heading">' |
| '<h2>Chat workbench</h2><span>schema + prompt</span></div>' |
| ) |
| chatbot_kwargs = { |
| "label": "", |
| "height": 360, |
| "show_label": False, |
| "elem_classes": ["chat-history"], |
| } |
| if "type" in inspect.signature(gr.Chatbot).parameters: |
| chatbot_kwargs["type"] = "messages" |
| chatbot = gr.Chatbot(**chatbot_kwargs) |
| with gr.Row(elem_classes=["schema-strip"]): |
| gr.HTML('<span class="schema-strip-label">Schema</span>') |
| employees_preset = gr.Button("employees", size="sm") |
| orders_preset = gr.Button("orders", size="sm") |
| students_preset = gr.Button("students", size="sm") |
| products_preset = gr.Button("products", size="sm") |
| sales_preset = gr.Button("sales", size="sm") |
| with gr.Row(elem_classes=["schema-context-row"]): |
| active_schema_pill = gr.HTML(render_schema_context(""), elem_classes=["schema-pill-container"]) |
| clear_schema_button = gr.Button("Clear", size="sm", elem_id="clear-schema-button", scale=0) |
| with gr.Row(elem_classes=["composer-row"]): |
| message_input = gr.Textbox( |
| label="Message", |
| value="", |
| placeholder="Ask a SQL question against the active schema", |
| lines=1, |
| max_lines=5, |
| interactive=True, |
| show_label=True, |
| elem_id="message-input", |
| ) |
| send_button = gr.Button( |
| "Send", |
| variant="primary", |
| interactive=False, |
| elem_id="generate-button", |
| ) |
|
|
| with gr.Column(elem_classes=["output-shell"]): |
| with gr.Row(elem_classes=["output-head"]): |
| gr.HTML("<span>SQL artifact</span>") |
| validator_output = gr.HTML(validate_sql("")) |
| sql_output = gr.Code( |
| label="", |
| language="sql", |
| lines=7, |
| interactive=False, |
| show_label=False, |
| ) |
| error_output = gr.HTML(render_message(), elem_classes=["message-output-wrapper"]) |
|
|
| with gr.Column(elem_classes=["context-rail"]): |
| fine_tuned_model_card = gr.HTML(render_model_card(FINE_TUNED_MODEL_KEY, DEFAULT_MODEL_KEY)) |
| with gr.Column(elem_classes=["model-side-panel"]): |
| load_button = gr.Button("Load fine-tuned model", variant="primary", elem_id="load-button") |
| model_status = gr.HTML(render_status(DEFAULT_MODEL_KEY, None)) |
| model_info = gr.HTML(model_metadata(DEFAULT_MODEL_KEY)) |
| gr.HTML(render_example_prompts()) |
|
|
| model_state_outputs = [ |
| fine_tuned_model_card, |
| model_status, |
| model_info, |
| employees_preset, |
| orders_preset, |
| students_preset, |
| products_preset, |
| sales_preset, |
| clear_schema_button, |
| message_input, |
| send_button, |
| error_output, |
| ] |
|
|
| load_button.click( |
| load_selected_model, |
| inputs=None, |
| outputs=[ |
| loaded_key_state, |
| model_status, |
| loading_overlay, |
| model_info, |
| load_button, |
| employees_preset, |
| orders_preset, |
| students_preset, |
| products_preset, |
| sales_preset, |
| clear_schema_button, |
| message_input, |
| send_button, |
| sql_output, |
| validator_output, |
| error_output, |
| ], |
| js=LOAD_SCROLL_JS, |
| ) |
|
|
| schema_context_outputs = [active_schema, active_schema_pill, clear_schema_button, conversation_state] |
| employees_preset.click(set_preset, inputs=gr.State("employees"), outputs=schema_context_outputs) |
| orders_preset.click(set_preset, inputs=gr.State("orders"), outputs=schema_context_outputs) |
| students_preset.click(set_preset, inputs=gr.State("students"), outputs=schema_context_outputs) |
| products_preset.click(set_preset, inputs=gr.State("products"), outputs=schema_context_outputs) |
| sales_preset.click(set_preset, inputs=gr.State("sales"), outputs=schema_context_outputs) |
| clear_schema_button.click(clear_schema_context, outputs=schema_context_outputs) |
|
|
| chat_generation_outputs = [ |
| chatbot, |
| message_input, |
| active_schema, |
| last_user_message, |
| sql_output, |
| validator_output, |
| error_output, |
| conversation_state, |
| active_schema_pill, |
| clear_schema_button, |
| ] |
| send_button.click( |
| generate_response, |
| inputs=[message_input, chatbot, active_schema, loaded_key_state, conversation_state], |
| outputs=chat_generation_outputs, |
| ) |
| message_input.submit( |
| generate_response, |
| inputs=[message_input, chatbot, active_schema, loaded_key_state, conversation_state], |
| outputs=chat_generation_outputs, |
| ) |
| demo.load( |
| sync_on_load, |
| outputs=[ |
| loaded_key_state, |
| model_status, |
| loading_overlay, |
| model_info, |
| load_button, |
| employees_preset, |
| orders_preset, |
| students_preset, |
| products_preset, |
| sales_preset, |
| clear_schema_button, |
| message_input, |
| send_button, |
| sql_output, |
| validator_output, |
| error_output, |
| ], |
| ) |
|
|
| queue_kwargs = {} |
| if "default_concurrency_limit" in inspect.signature(demo.queue).parameters: |
| queue_kwargs["default_concurrency_limit"] = 1 |
| demo.queue(**queue_kwargs) |
|
|
|
|
| if __name__ == "__main__": |
| demo.launch(css=CSS) |
|
|