# -*- coding: utf-8 -*- """ Utils and helpers for performing sql querys. Referenced from https://github.com/BeachWang/DAIL-SQL. """ import sqlite3 import json from typing import Union from sqlite3 import Connection import os import numpy as np def query_sqlite( queries: Union[list[str], str], path_db: str = None, cur: str = None, ) -> Union[list, str]: """Execute queries and return results.""" assert not ( path_db is None and cur is None ), "path_db and cur cannot be NoneType at the same time" con: Connection close_in_func = False if cur is None: con = sqlite3.connect(path_db) cur = con.cursor() close_in_func = True if isinstance(queries, str): results = cur.execute(queries).fetchall() elif isinstance(queries, list): results = [] for query in queries: res = cur.execute(query).fetchall() results.append(res) else: raise TypeError(f"queries cannot be {type(queries)}") # close the connection if needed if close_in_func: con.close() return results def create_sqlite_db_from_schema(schema_path: str, db_path: str) -> None: """Create a SQLite database file from a schema SQL file. Args: schema_path: The file path to the schema SQL file. db_path: The file path for the SQLite database to be created. """ if os.path.exists(db_path): print( f"Database file '{db_path}' already exists. ", ) return conn = sqlite3.connect(db_path) with open(schema_path, "r", encoding="utf-8") as schema_file: schema_sql = schema_file.read() cursor = conn.cursor() cursor.executescript(schema_sql) conn.commit() conn.close() def get_table_names(path_db: str = None, cur: str = None) -> list: """Get names of all tables within the database, and reuse cur if it's not None""" table_names = query_sqlite( queries="SELECT name FROM sqlite_master WHERE type='table'", path_db=path_db, cur=cur, ) table_names = [_[0] for _ in table_names] return table_names def get_sql_for_database(path_db: str = None, cur: str = None) -> list: """ Get sql table from database """ close_in_func = False if cur is None: con = sqlite3.connect(path_db) cur = con.cursor() close_in_func = True table_names = get_table_names(path_db, cur) queries = [ f"SELECT sql FROM sqlite_master WHERE tbl_name='{name}'" for name in table_names ] sqls = query_sqlite(queries, path_db, cur) if close_in_func: cur.close() return [_[0][0] for _ in sqls] class SQLPrompt: """SQL prompt helper""" def __init__(self) -> None: self.template_info = "/* Given the following database schema: */\n{}" self.template_question = "/* Answer the following: {} */" self.template_agent_prompt = ( "You are a helpful agent that preform" "SQL querys base on natual language instructions." "Please describe the database schema provided" "in a simple and understandable manner. " ) self.is_sql_prompt = ( "Please read the user's question below and " "determine whether the question is an appropriate " "query for the given SQL schema. \n" "If the question is indeed a query pertaining to the SQL schema, " 'respond with "YES". ' "If the question is not a query related to the SQL schema, " "provide a brief explanation to the user explaining why their " "question does not correspond to a SQL query within the " "context of the schema. " "The user's question is: " ) def format_target(self, example: dict) -> str: """Format sql prompt""" return self.format_question(example) + "\nSELECT " def format_question(self, example: dict) -> str: """Format question""" sqls = get_sql_for_database(example["path_db"]) prompt_info = self.template_info.format("\n\n".join(sqls)) prompt_question = self.template_question.format(example["question"]) prompt_components = [prompt_info, prompt_question] prompt = "\n\n".join(prompt_components) return prompt def describe_schema(self, db_path: str) -> str: """Describe SQL schema""" sqls = get_sql_for_database(db_path) prompt_info = self.template_info.format("\n\n".join(sqls)) prompt_components = [ self.template_agent_prompt, "DB schema info: ", prompt_info, ] prompt = "\n\n".join(prompt_components) return prompt def is_sql_question(self, example: dict) -> str: """whether the input is a sql question or not""" sqls = get_sql_for_database(example["path_db"]) prompt_info = self.template_info.format("\n\n".join(sqls)) prompt_components = [ prompt_info, self.is_sql_prompt, example["question"], ] prompt = "\n".join(prompt_components) return prompt class QuestionSqlExampleStyle: """Provide QA pair as examples""" def get_example_prefix(self) -> str: """get example prefix""" return ( "/* Some SQL examples are provided based on similar problems: */\n" ) def format_example(self, example: dict) -> str: """format example""" template_qa = "/* Answer the following: {} */\n{}" return template_qa.format(example["question"], example["query"]) class EuclideanDistanceExampleSelector: """Select similar sample question""" def __init__(self) -> None: self.train_json_path = "./database/train.json" with open(self.train_json_path, "r", encoding="utf-8") as file: self.train_json = json.load(file) self.train_questions = [_["question"] for _ in self.train_json] self.SELECT_MODEL = "sentence-transformers/all-mpnet-base-v2" from sentence_transformers import SentenceTransformer self.bert_model = SentenceTransformer(self.SELECT_MODEL, device="cpu") try: self.train_embeddings = np.load("./.cache/train_embeddings.npy") except FileNotFoundError: self.train_embeddings = self.bert_model.encode( self.train_questions, ) np.save("./.cache/train_embeddings.npy", self.train_embeddings) def get_examples( self, target: dict, num_example: int, ) -> list: """Get similar question examples for few shot""" target_embedding = self.bert_model.encode([target["question"]]) # find the most similar question in train dataset from sklearn.metrics.pairwise import euclidean_distances distances = np.squeeze( euclidean_distances(target_embedding, self.train_embeddings), ).tolist() pairs = list(zip(distances, range(len(distances)))) train_json = self.train_json pairs_sorted = sorted(pairs, key=lambda x: x[0]) top_pairs = [] for d, index in pairs_sorted: top_pairs.append((index, d)) if len(top_pairs) >= num_example: break return [train_json[index] for (index, d) in top_pairs] class DailSQLPromptGenerator: """Generate prompt given the dataset and question""" def __init__( self, db_path: str, ) -> None: self.db_path = db_path self.sql_prompt = SQLPrompt() self.question_selector = EuclideanDistanceExampleSelector() self.question_style = QuestionSqlExampleStyle() self.SEP_EXAMPLE = "\n\n" self.scope_factor = 100 self.NUM_EXAMPLE = 9 self.cross_domain = False def describe_schema(self) -> str: """Describe the sql""" return self.sql_prompt.describe_schema(self.db_path) def is_sql_question_prompt(self, question: str) -> str: """ prompt for LLM to judge whether the question is appropriate """ target = { "path_db": self.db_path, "question": question, } return self.sql_prompt.is_sql_question(target) def generate_prompt(self, x: dict = None) -> dict: """ Generate prompt given input question """ question = x["content"] target = { "path_db": self.db_path, "question": question, } prompt_target = self.sql_prompt.format_target(target) if self.NUM_EXAMPLE != 0: examples = self.question_selector.get_examples( target, self.NUM_EXAMPLE * self.scope_factor, ) prompt_example = [] question = target["question"] example_prefix = self.question_style.get_example_prefix() for example in examples: example_format = self.question_style.format_example(example) prompt_example.append(example_format) if len(prompt_example) >= self.NUM_EXAMPLE: break n_valid_example = len(prompt_example) if len(prompt_example) > 0: prompt = example_prefix + self.SEP_EXAMPLE.join( prompt_example + [prompt_target], ) else: prompt = self.SEP_EXAMPLE.join( prompt_example + [prompt_target], ) else: n_valid_example = 0 prompt = prompt_target return { "prompt": prompt, "n_examples": n_valid_example, }