fix: unsupported data mutation routing
Browse files- README.md +2 -1
- app.py +23 -0
- intent.py +4 -0
- sql_tools.py +65 -1
- tests/test_chatbot_behavior.py +87 -1
- tests/test_chatbot_core.py +46 -0
README.md
CHANGED
|
@@ -51,6 +51,7 @@ Reported gain: **+71.5 percentage points** over the base model.
|
|
| 51 |
Known limits:
|
| 52 |
|
| 53 |
- The training data is mostly `SELECT`; `INSERT`, `UPDATE`, and `DELETE` are not a reliable model capability.
|
|
|
|
| 54 |
- PT-BR input is not trained model capability. The app handles selected PT-BR cases through deterministic normalization/templates.
|
| 55 |
- Conversational chat and JSON schema proposal are out of scope and use fallback behavior instead of model prompts.
|
| 56 |
- Exact match does not prove semantic perfection. It is useful evidence, not a substitute for qualitative review.
|
|
@@ -126,7 +127,7 @@ The probe prints JSON with pass/fail checks for static fallback, deterministic C
|
|
| 126 |
set PYTHONPATH=. && pytest tests/test_chatbot_core.py tests/test_chatbot_behavior.py -q
|
| 127 |
```
|
| 128 |
|
| 129 |
-
Current unit suite: **
|
| 130 |
|
| 131 |
## Run Locally
|
| 132 |
|
|
|
|
| 51 |
Known limits:
|
| 52 |
|
| 53 |
- The training data is mostly `SELECT`; `INSERT`, `UPDATE`, and `DELETE` are not a reliable model capability.
|
| 54 |
+
- Unsupported DML requests are not generated by the model and must not be disguised as schema edits. For example, `delete species` can remove a schema column, but `delete all animals` returns a static unsupported-scope fallback.
|
| 55 |
- PT-BR input is not trained model capability. The app handles selected PT-BR cases through deterministic normalization/templates.
|
| 56 |
- Conversational chat and JSON schema proposal are out of scope and use fallback behavior instead of model prompts.
|
| 57 |
- Exact match does not prove semantic perfection. It is useful evidence, not a substitute for qualitative review.
|
|
|
|
| 127 |
set PYTHONPATH=. && pytest tests/test_chatbot_core.py tests/test_chatbot_behavior.py -q
|
| 128 |
```
|
| 129 |
|
| 130 |
+
Current unit suite: **127 tests**. These tests avoid loading the 3.8B model and focus on routing, deterministic tools, prompt construction, model-output rejection, SQL validation, UI schema-context synchronization, and error handling.
|
| 131 |
|
| 132 |
## Run Locally
|
| 133 |
|
app.py
CHANGED
|
@@ -28,6 +28,10 @@ FALLBACK_RESPONSE = (
|
|
| 28 |
"Example: 'what is the most expensive product?' or "
|
| 29 |
"'create table products with id name price'."
|
| 30 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 31 |
SOURCE_FINE_TUNED_MODEL = "Source: fine-tuned model"
|
| 32 |
SOURCE_DETERMINISTIC_SQL_TEMPLATE = "Source: deterministic SQL template"
|
| 33 |
SOURCE_DETERMINISTIC_SCHEMA_PARSER = "Source: deterministic schema parser"
|
|
@@ -1175,6 +1179,14 @@ def generate_response(message, chat_history, active_schema, loaded_key, conversa
|
|
| 1175 |
sql_text=edited_table,
|
| 1176 |
validator=sql_core.validate_sql(edited_table),
|
| 1177 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1178 |
return _empty_generation_response(
|
| 1179 |
chat_history,
|
| 1180 |
message,
|
|
@@ -1204,6 +1216,17 @@ def generate_response(message, chat_history, active_schema, loaded_key, conversa
|
|
| 1204 |
source_label=SOURCE_DETERMINISTIC_SCHEMA_PARSER,
|
| 1205 |
)
|
| 1206 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1207 |
if intent_result.intent in {intent_core.SMALLTALK, intent_core.UNKNOWN}:
|
| 1208 |
return _response_tuple(
|
| 1209 |
chat_history,
|
|
|
|
| 28 |
"Example: 'what is the most expensive product?' or "
|
| 29 |
"'create table products with id name price'."
|
| 30 |
)
|
| 31 |
+
UNSUPPORTED_MUTATION_RESPONSE = (
|
| 32 |
+
"This demo does not generate INSERT, UPDATE, DELETE, or DROP statements. "
|
| 33 |
+
"It only supports SELECT/WITH model SQL plus deterministic CREATE TABLE tools."
|
| 34 |
+
)
|
| 35 |
SOURCE_FINE_TUNED_MODEL = "Source: fine-tuned model"
|
| 36 |
SOURCE_DETERMINISTIC_SQL_TEMPLATE = "Source: deterministic SQL template"
|
| 37 |
SOURCE_DETERMINISTIC_SCHEMA_PARSER = "Source: deterministic schema parser"
|
|
|
|
| 1179 |
sql_text=edited_table,
|
| 1180 |
validator=sql_core.validate_sql(edited_table),
|
| 1181 |
)
|
| 1182 |
+
if sql_core.last_create_table_from_history(chat_history) or sql_core.create_table_from_schema(state.active_schema):
|
| 1183 |
+
return _empty_generation_response(
|
| 1184 |
+
chat_history,
|
| 1185 |
+
message,
|
| 1186 |
+
state,
|
| 1187 |
+
"No matching schema column was changed.",
|
| 1188 |
+
source_label=SOURCE_DETERMINISTIC_SCHEMA_PARSER,
|
| 1189 |
+
)
|
| 1190 |
return _empty_generation_response(
|
| 1191 |
chat_history,
|
| 1192 |
message,
|
|
|
|
| 1216 |
source_label=SOURCE_DETERMINISTIC_SCHEMA_PARSER,
|
| 1217 |
)
|
| 1218 |
|
| 1219 |
+
if intent_result.intent == intent_core.UNKNOWN and intent_result.reason == "unsupported_data_mutation":
|
| 1220 |
+
return _response_tuple(
|
| 1221 |
+
chat_history,
|
| 1222 |
+
message,
|
| 1223 |
+
state,
|
| 1224 |
+
UNSUPPORTED_MUTATION_RESPONSE,
|
| 1225 |
+
f"{SOURCE_STATIC_FALLBACK}. Unsupported data mutation - no model call.",
|
| 1226 |
+
sql_text="",
|
| 1227 |
+
validator=EMPTY_VALIDATOR,
|
| 1228 |
+
)
|
| 1229 |
+
|
| 1230 |
if intent_result.intent in {intent_core.SMALLTALK, intent_core.UNKNOWN}:
|
| 1231 |
return _response_tuple(
|
| 1232 |
chat_history,
|
intent.py
CHANGED
|
@@ -55,6 +55,10 @@ def classify_intent(message, state=None, chat_history=None):
|
|
| 55 |
if _is_smalltalk(message):
|
| 56 |
return IntentResult(SMALLTALK, 0.95, "smalltalk_phrase")
|
| 57 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 58 |
edited_table = sql_tools.edit_create_table_from_message(message, chat_history, state.active_schema)
|
| 59 |
if edited_table or sql_tools.is_table_edit_intent(message):
|
| 60 |
return IntentResult(
|
|
|
|
| 55 |
if _is_smalltalk(message):
|
| 56 |
return IntentResult(SMALLTALK, 0.95, "smalltalk_phrase")
|
| 57 |
|
| 58 |
+
schema_context = sql_tools.last_create_table_from_history(chat_history) or state.active_schema
|
| 59 |
+
if sql_tools.is_unsupported_data_mutation_for_schema(message, schema_context):
|
| 60 |
+
return IntentResult(UNKNOWN, 0.9, "unsupported_data_mutation")
|
| 61 |
+
|
| 62 |
edited_table = sql_tools.edit_create_table_from_message(message, chat_history, state.active_schema)
|
| 63 |
if edited_table or sql_tools.is_table_edit_intent(message):
|
| 64 |
return IntentResult(
|
sql_tools.py
CHANGED
|
@@ -284,6 +284,67 @@ def is_table_edit_intent(message):
|
|
| 284 |
)
|
| 285 |
|
| 286 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 287 |
def infer_column_type(column_name):
|
| 288 |
name = column_name.strip().lower()
|
| 289 |
if name == "id" or name.endswith("_id") or name in {"quantity", "quantidade", "stock", "estoque", "year"}:
|
|
@@ -795,7 +856,10 @@ def edit_create_table_from_message(message, chat_history, active_schema):
|
|
| 795 |
for col_name, col_type in existing_columns
|
| 796 |
if col_name not in removed_set
|
| 797 |
]
|
| 798 |
-
|
|
|
|
|
|
|
|
|
|
| 799 |
|
| 800 |
|
| 801 |
def create_table_from_suggestion(suggestion):
|
|
|
|
| 284 |
)
|
| 285 |
|
| 286 |
|
| 287 |
+
def is_unsupported_data_mutation_intent(message):
|
| 288 |
+
message = normalize_text(message)
|
| 289 |
+
if not message:
|
| 290 |
+
return False
|
| 291 |
+
if is_create_table_intent(message):
|
| 292 |
+
return False
|
| 293 |
+
explicit_dml = (
|
| 294 |
+
r"\b(?:insert\s+into|update\b.+\bset\b|delete\s+from|"
|
| 295 |
+
r"drop\s+table|drop\s+(?:the\s+|a\s+|an\s+)?\w+\s+table|truncate\s+table)\b"
|
| 296 |
+
)
|
| 297 |
+
if re.search(explicit_dml, message):
|
| 298 |
+
return True
|
| 299 |
+
if re.search(r"\binsert\b", message) and not re.search(r"\b(column|field|coluna|campo)\b", message):
|
| 300 |
+
return True
|
| 301 |
+
row_terms = r"(?:row|rows|record|records|linha|linhas|registro|registros)"
|
| 302 |
+
row_mutation = (
|
| 303 |
+
rf"\b(?:add|create|insert|include|remove|delete|drop|"
|
| 304 |
+
rf"adicionar|adicione|criar|crie|incluir|inclua|remover|remova|deletar|excluir|exclua|apagar|apague)\b"
|
| 305 |
+
rf".*\b{row_terms}\b"
|
| 306 |
+
)
|
| 307 |
+
if re.search(row_mutation, message):
|
| 308 |
+
return True
|
| 309 |
+
destructive_all = (
|
| 310 |
+
r"\b(?:delete|drop|remove|deletar|exclua|excluir|apaga|apagar|apague|remova|remover)\b"
|
| 311 |
+
r"\s+(?:all|every|rows?|records?|table|todos|todas|linhas?|registros?|tabela)\b"
|
| 312 |
+
)
|
| 313 |
+
return bool(re.search(destructive_all, message))
|
| 314 |
+
|
| 315 |
+
|
| 316 |
+
def _table_name_variants(table_name):
|
| 317 |
+
base = normalize_text(table_name)
|
| 318 |
+
if not base:
|
| 319 |
+
return set()
|
| 320 |
+
variants = {base}
|
| 321 |
+
if base.endswith("ies") and len(base) > 3:
|
| 322 |
+
variants.add(f"{base[:-3]}y")
|
| 323 |
+
elif base.endswith("s") and len(base) > 1:
|
| 324 |
+
variants.add(base[:-1])
|
| 325 |
+
else:
|
| 326 |
+
variants.add(f"{base}s")
|
| 327 |
+
return {variant for variant in variants if variant}
|
| 328 |
+
|
| 329 |
+
|
| 330 |
+
def is_unsupported_data_mutation_for_schema(message, active_schema=""):
|
| 331 |
+
if is_unsupported_data_mutation_intent(message):
|
| 332 |
+
return True
|
| 333 |
+
table_name, _columns = parse_create_table_schema(create_table_from_schema(active_schema))
|
| 334 |
+
if not table_name:
|
| 335 |
+
return False
|
| 336 |
+
message = normalize_text(message)
|
| 337 |
+
table_pattern = "|".join(
|
| 338 |
+
re.escape(variant)
|
| 339 |
+
for variant in sorted(_table_name_variants(table_name), key=len, reverse=True)
|
| 340 |
+
)
|
| 341 |
+
table_mutation = (
|
| 342 |
+
rf"\b(?:delete|drop|remove|deletar|excluir|exclua|remover|remova)\b"
|
| 343 |
+
rf"\s+(?:the\s+|a\s+|an\s+)?(?:table\s+)?(?:{table_pattern})\b(?:\s+table)?"
|
| 344 |
+
)
|
| 345 |
+
return bool(re.search(table_mutation, message))
|
| 346 |
+
|
| 347 |
+
|
| 348 |
def infer_column_type(column_name):
|
| 349 |
name = column_name.strip().lower()
|
| 350 |
if name == "id" or name.endswith("_id") or name in {"quantity", "quantidade", "stock", "estoque", "year"}:
|
|
|
|
| 856 |
for col_name, col_type in existing_columns
|
| 857 |
if col_name not in removed_set
|
| 858 |
]
|
| 859 |
+
updated_columns = [*kept_columns, *added_columns]
|
| 860 |
+
if updated_columns == existing_columns:
|
| 861 |
+
return ""
|
| 862 |
+
return format_create_table(table_name, updated_columns)
|
| 863 |
|
| 864 |
|
| 865 |
def create_table_from_suggestion(suggestion):
|
tests/test_chatbot_behavior.py
CHANGED
|
@@ -148,7 +148,7 @@ def test_add_column_variants(message, expected_col, monkeypatch):
|
|
| 148 |
[
|
| 149 |
("remova salario", "salario"),
|
| 150 |
("remove nome", "nome"),
|
| 151 |
-
("delete
|
| 152 |
("drop coluna id", "id"),
|
| 153 |
],
|
| 154 |
)
|
|
@@ -674,6 +674,92 @@ def test_edit_without_existing_table_returns_error():
|
|
| 674 |
assert app.SOURCE_DETERMINISTIC_SCHEMA_PARSER in status_html(result)
|
| 675 |
|
| 676 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 677 |
def test_non_template_sql_intent_without_model_returns_load_error():
|
| 678 |
result = app.generate_response(
|
| 679 |
"find employees named Alice", [], app.PRESETS["employees"], None, None
|
|
|
|
| 148 |
[
|
| 149 |
("remova salario", "salario"),
|
| 150 |
("remove nome", "nome"),
|
| 151 |
+
("delete salario", "salario"),
|
| 152 |
("drop coluna id", "id"),
|
| 153 |
],
|
| 154 |
)
|
|
|
|
| 674 |
assert app.SOURCE_DETERMINISTIC_SCHEMA_PARSER in status_html(result)
|
| 675 |
|
| 676 |
|
| 677 |
+
@pytest.mark.parametrize(
|
| 678 |
+
"message",
|
| 679 |
+
[
|
| 680 |
+
"delete all animals",
|
| 681 |
+
"delete animals",
|
| 682 |
+
"DELETE FROM animals",
|
| 683 |
+
"drop table animals",
|
| 684 |
+
"drop animals table",
|
| 685 |
+
"drop the animals table",
|
| 686 |
+
"drop animals",
|
| 687 |
+
"update animals set weight = 0",
|
| 688 |
+
"insert into animals values (1)",
|
| 689 |
+
"insert animal",
|
| 690 |
+
"insert row into animals",
|
| 691 |
+
"add row to animals",
|
| 692 |
+
"add animal record",
|
| 693 |
+
],
|
| 694 |
+
)
|
| 695 |
+
def test_unsupported_data_mutation_returns_static_fallback(message, monkeypatch):
|
| 696 |
+
monkeypatch.setattr(app, "_run_generation", lambda *a, **k: pytest.fail("model should not run"))
|
| 697 |
+
|
| 698 |
+
result = app.generate_response(
|
| 699 |
+
message,
|
| 700 |
+
[],
|
| 701 |
+
"CREATE TABLE animals (id INTEGER, name TEXT, weight NUMERIC)",
|
| 702 |
+
None,
|
| 703 |
+
None,
|
| 704 |
+
)
|
| 705 |
+
|
| 706 |
+
assert sql_output(result) == ""
|
| 707 |
+
assert result[5] == app.EMPTY_VALIDATOR
|
| 708 |
+
assert app.UNSUPPORTED_MUTATION_RESPONSE in assistant_text(result)
|
| 709 |
+
assert app.SOURCE_STATIC_FALLBACK in status_html(result)
|
| 710 |
+
assert app.SOURCE_DETERMINISTIC_SCHEMA_PARSER not in status_html(result)
|
| 711 |
+
|
| 712 |
+
|
| 713 |
+
def test_remove_nonexistent_columns_does_not_return_unchanged_schema():
|
| 714 |
+
schema = "CREATE TABLE animals (id INTEGER, name TEXT, species TEXT);"
|
| 715 |
+
|
| 716 |
+
assert app.sql_core.edit_create_table_from_message("delete all animals", [], schema) == ""
|
| 717 |
+
|
| 718 |
+
|
| 719 |
+
def test_noop_schema_edit_reports_no_matching_column(monkeypatch):
|
| 720 |
+
monkeypatch.setattr(app, "_run_generation", lambda *a, **k: pytest.fail("model should not run"))
|
| 721 |
+
|
| 722 |
+
result = app.generate_response(
|
| 723 |
+
"delete salary",
|
| 724 |
+
[],
|
| 725 |
+
"CREATE TABLE animals (id INTEGER, name TEXT, species TEXT)",
|
| 726 |
+
None,
|
| 727 |
+
None,
|
| 728 |
+
)
|
| 729 |
+
|
| 730 |
+
assert sql_output(result) == ""
|
| 731 |
+
assert "No matching schema column was changed." in status_html(result)
|
| 732 |
+
assert app.SOURCE_DETERMINISTIC_SCHEMA_PARSER in status_html(result)
|
| 733 |
+
|
| 734 |
+
|
| 735 |
+
def test_data_mutation_uses_schema_from_history_when_active_schema_empty(monkeypatch):
|
| 736 |
+
monkeypatch.setattr(app, "_run_generation", lambda *a, **k: pytest.fail("model should not run"))
|
| 737 |
+
history = [
|
| 738 |
+
{
|
| 739 |
+
"role": "assistant",
|
| 740 |
+
"content": "```sql\nCREATE TABLE animals (id INTEGER, species TEXT);\n```",
|
| 741 |
+
}
|
| 742 |
+
]
|
| 743 |
+
|
| 744 |
+
result = app.generate_response("delete animals", history, "", None, None)
|
| 745 |
+
|
| 746 |
+
assert sql_output(result) == ""
|
| 747 |
+
assert app.UNSUPPORTED_MUTATION_RESPONSE in assistant_text(result)
|
| 748 |
+
assert app.SOURCE_STATIC_FALLBACK in status_html(result)
|
| 749 |
+
|
| 750 |
+
|
| 751 |
+
@pytest.mark.parametrize("message", ["create table rows with id name", "create table records with id name"])
|
| 752 |
+
def test_create_table_names_that_overlap_row_terms_still_create_schema(message, monkeypatch):
|
| 753 |
+
monkeypatch.setattr(app, "_run_generation", lambda *a, **k: pytest.fail("model should not run"))
|
| 754 |
+
|
| 755 |
+
result = app.generate_response(message, [], "", None, None)
|
| 756 |
+
|
| 757 |
+
assert "CREATE TABLE" in sql_output(result)
|
| 758 |
+
assert "id INTEGER" in sql_output(result)
|
| 759 |
+
assert "name TEXT" in sql_output(result)
|
| 760 |
+
assert app.SOURCE_DETERMINISTIC_SCHEMA_PARSER in status_html(result)
|
| 761 |
+
|
| 762 |
+
|
| 763 |
def test_non_template_sql_intent_without_model_returns_load_error():
|
| 764 |
result = app.generate_response(
|
| 765 |
"find employees named Alice", [], app.PRESETS["employees"], None, None
|
tests/test_chatbot_core.py
CHANGED
|
@@ -46,6 +46,52 @@ def test_intent_create_edit_and_sql_query():
|
|
| 46 |
assert query.intent == SQL_QUERY
|
| 47 |
|
| 48 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 49 |
def test_zoologico_transcript_with_mocked_sql_model(monkeypatch):
|
| 50 |
app._model = types.SimpleNamespace(generation_config=types.SimpleNamespace(eos_token_id=0))
|
| 51 |
app._tokenizer = types.SimpleNamespace(eos_token_id=0, pad_token_id=0)
|
|
|
|
| 46 |
assert query.intent == SQL_QUERY
|
| 47 |
|
| 48 |
|
| 49 |
+
def test_destructive_data_mutation_is_unknown_with_active_schema():
|
| 50 |
+
state = ConversationState(active_schema="CREATE TABLE animals (id INTEGER, name TEXT, species TEXT)")
|
| 51 |
+
|
| 52 |
+
assert classify_intent("delete all animals", state).intent == UNKNOWN
|
| 53 |
+
assert classify_intent("DELETE FROM animals", state).intent == UNKNOWN
|
| 54 |
+
assert classify_intent("UPDATE animals SET name = 'x'", state).intent == UNKNOWN
|
| 55 |
+
assert classify_intent("INSERT INTO animals VALUES (1)", state).intent == UNKNOWN
|
| 56 |
+
assert classify_intent("insert animal", state).intent == UNKNOWN
|
| 57 |
+
assert classify_intent("insert row into animals", state).intent == UNKNOWN
|
| 58 |
+
assert classify_intent("add row to animals", state).intent == UNKNOWN
|
| 59 |
+
assert classify_intent("add animal record", state).intent == UNKNOWN
|
| 60 |
+
assert classify_intent("drop animals table", state).intent == UNKNOWN
|
| 61 |
+
assert classify_intent("drop the animals table", state).intent == UNKNOWN
|
| 62 |
+
assert classify_intent("delete animals", state).intent == UNKNOWN
|
| 63 |
+
assert classify_intent("drop animals", state).intent == UNKNOWN
|
| 64 |
+
|
| 65 |
+
singular_state = ConversationState(active_schema="CREATE TABLE animal (id INTEGER, species TEXT)")
|
| 66 |
+
assert classify_intent("delete animals", singular_state).intent == UNKNOWN
|
| 67 |
+
assert classify_intent("drop animals", singular_state).intent == UNKNOWN
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
def test_data_mutation_uses_schema_from_history_when_active_schema_empty():
|
| 71 |
+
history = [
|
| 72 |
+
{
|
| 73 |
+
"role": "assistant",
|
| 74 |
+
"content": "```sql\nCREATE TABLE animals (id INTEGER, species TEXT);\n```",
|
| 75 |
+
}
|
| 76 |
+
]
|
| 77 |
+
|
| 78 |
+
assert classify_intent("delete animals", ConversationState(), history).intent == UNKNOWN
|
| 79 |
+
assert classify_intent("drop animals", ConversationState(), history).intent == UNKNOWN
|
| 80 |
+
|
| 81 |
+
|
| 82 |
+
def test_delete_existing_column_stays_schema_edit():
|
| 83 |
+
state = ConversationState(active_schema="CREATE TABLE animals (id INTEGER, species TEXT)")
|
| 84 |
+
|
| 85 |
+
assert classify_intent("delete species", state).intent == EDIT_TABLE
|
| 86 |
+
assert classify_intent("drop coluna id", state).intent == EDIT_TABLE
|
| 87 |
+
assert classify_intent("add habitat", state).intent == EDIT_TABLE
|
| 88 |
+
|
| 89 |
+
|
| 90 |
+
def test_create_tables_named_rows_or_records_are_not_data_mutation():
|
| 91 |
+
assert classify_intent("create table rows with id name", ConversationState()).intent == CREATE_TABLE
|
| 92 |
+
assert classify_intent("create table records with id name", ConversationState()).intent == CREATE_TABLE
|
| 93 |
+
|
| 94 |
+
|
| 95 |
def test_zoologico_transcript_with_mocked_sql_model(monkeypatch):
|
| 96 |
app._model = types.SimpleNamespace(generation_config=types.SimpleNamespace(eos_token_id=0))
|
| 97 |
app._tokenizer = types.SimpleNamespace(eos_token_id=0, pad_token_id=0)
|