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 = 'No SQL yet' CHAT_VALIDATOR = 'Chat response' 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|>", ""): 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 """ """ def render_header(): return """

Phi-3 Mini SQL Chatbot

Text-to-SQL demo with explicit model and guardrail boundaries.

Fine-tuned model: SQL_QUERY Deterministic guardrails CPU lazy load
""" def render_step(number, title): return f"""
{number} — {title}
""" 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"""
{model_def["tag"]}

{model_def["title"]}

{model_def["model_id"]}
{model_def["exact_match"]} exact match
{render_baseline_evidence()}
""" 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 ( '
' f' {SOURCE_FINE_TUNED_MODEL}. Loading {selected_def["short_label"]} model - first load ~3-5 min' "
" ) if loaded_key: loaded_def = model_by_key(loaded_key) return ( '
' f' {SOURCE_FINE_TUNED_MODEL}. {loaded_def["short_label"]} model ready - ~3-5 min per query on CPU' "
" ) return f'
{SOURCE_FINE_TUNED_MODEL}. No model loaded
' def render_loading_overlay(model_key=None, visible=False): if not visible: return '' model_def = model_by_key(model_key or DEFAULT_MODEL_KEY) return ( '
' '
' f'
Loading {model_def["short_label"]} model
' '
' '

First load: ~3-5 min — cached for session

' "
" "
" ) def model_metadata(model_key=None): return f"""

Source contract

SQL_QUERY + create/edit/fallback
{render_source_contract_grid()}
""" def render_source_contract_grid(): return """
SQL_QUERYtemplates first; model only for SELECT/WITH
Createdeterministic CREATE TABLE parser
Editdeterministic schema updates
Fallbackstatic non-SQL response
""" def render_source_legend(extra_class=""): class_name = "source-panel" if extra_class: class_name = f"{class_name} {extra_class}" return f"""

Source contract

always visible
{render_source_contract_grid()}
""" def render_example_prompts(): return """

Example prompts

by source
fine-tuned modelshow employees in Engineering ordered by salary
deterministic SQL templatewhat is the most expensive product?
deterministic schema parsercreate table animals with id name species weight
static fallbackwhat can you do?
""" def render_baseline_evidence(): return """
Offline evidence
Base 2.0%
Fine-tuned 73.5%
Gain +71.5pp
""" 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 keywords that indicate query, not table edit sql_aggregation_terms = {"up", "sum", "total", "count", "average", "avg", "max", "min", "by"} words = message.split() # For add: require target term OR check if it's clearly a column name list # "add up the total" is SQL query; "add email and phone" is table edit add_match = re.search(direct_add_terms, message) has_target = re.search(target_terms, message) if add_match: # Find position after "add" keyword 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 "" # If first word after "add" is aggregation term, it's SQL query, not edit 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 # P2 fix: look for the type as the FINAL token, not the first match # "date DATE" should be interpreted as name="date", type="DATE", not name="" type="date" 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 # Column names that could be confused with SQL types when followed by date/datetime/timestamp # These should be treated as column names, not as part of type specification 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 current could be inferred as a different type, don't pair with date/datetime/timestamp # This preserves "total date" → "total" (inferred NUMERIC) + "date" (type) 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) # Also handle "troca X por Y" pattern 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 '
No active schemaSelect a preset or create a table.
' label = schema_name_by_value(schema) escaped_schema = html.escape(schema) escaped_label = html.escape(label) return ( '
' f'Context: {escaped_label}' f'{escaped_schema}' "
" ) def query_control_updates(can_generate): context_updates = [gr.update(interactive=True) for _ in range(6)] # Keep submit button enabled - model requirement is checked in generate_response return [*context_updates, gr.update(interactive=True), gr.update(interactive=True)] def render_message(message="", kind="error"): if not message: return '
' class_name = "message-ok" if kind == "ok" else "message-error" return f'
{html.escape(str(message))}
' 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: # Timeout reached but cannot truly cancel a running thread. # Wait for the operation to complete naturally to avoid race conditions. # The UI stays in loading state until the operation finishes. 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( '
' '

Chat workbench

schema + prompt
' ) 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('Schema') 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("SQL artifact") 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)