{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "6751831d-9b08-434f-829b-d0052a3b119f",
   "metadata": {},
   "source": [
    "# How to deal with large databases when doing SQL question-answering\n",
    "\n",
    "In order to write valid queries against a database, we need to feed the model the table names, table schemas, and feature values for it to query over. When there are many tables, columns, and/or high-cardinality columns, it becomes impossible for us to dump the full information about our database in every prompt. Instead, we must find ways to dynamically insert into the prompt only the most relevant information.\n",
    "\n",
    "In this guide we demonstrate methods for identifying such relevant information, and feeding this into a query-generation step. We will cover:\n",
    "\n",
    "1. Identifying a relevant subset of tables;\n",
    "2. Identifying a relevant subset of column values.\n",
    "\n",
    "\n",
    "## Setup\n",
    "\n",
    "First, get required packages and set environment variables:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "afd5c20e-c705-4ef4-b33b-71fa819215ce",
   "metadata": {},
   "outputs": [],
   "source": [
    "%pip install --upgrade --quiet  langchain langchain-community langchain-openai"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "592e0c93-5396-44ec-92f0-1635ddd59a42",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Uncomment the below to use LangSmith. Not required.\n",
    "# import os\n",
    "# os.environ[\"LANGCHAIN_API_KEY\"] = getpass.getpass()\n",
    "# os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\""
   ]
  },
  {
   "cell_type": "markdown",
   "id": "590ee096-db88-42af-90d4-99b8149df753",
   "metadata": {},
   "source": [
    "The below example will use a SQLite connection with Chinook database. Follow [these installation steps](https://database.guide/2-sample-databases-sqlite/) to create `Chinook.db` in the same directory as this notebook:\n",
    "\n",
    "* Save [this file](https://raw.githubusercontent.com/lerocha/chinook-database/master/ChinookDatabase/DataSources/Chinook_Sqlite.sql) as `Chinook_Sqlite.sql`\n",
    "* Run `sqlite3 Chinook.db`\n",
    "* Run `.read Chinook_Sqlite.sql`\n",
    "* Test `SELECT * FROM Artist LIMIT 10;`\n",
    "\n",
    "Now, `Chinhook.db` is in our directory and we can interface with it using the SQLAlchemy-driven [SQLDatabase](https://api.python.langchain.com/en/latest/utilities/langchain_community.utilities.sql_database.SQLDatabase.html) class:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "id": "cebd3915-f58f-4e73-8459-265630ae8cd4",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "sqlite\n",
      "['Album', 'Artist', 'Customer', 'Employee', 'Genre', 'Invoice', 'InvoiceLine', 'MediaType', 'Playlist', 'PlaylistTrack', 'Track']\n",
      "[(1, 'AC/DC'), (2, 'Accept'), (3, 'Aerosmith'), (4, 'Alanis Morissette'), (5, 'Alice In Chains'), (6, 'Antônio Carlos Jobim'), (7, 'Apocalyptica'), (8, 'Audioslave'), (9, 'BackBeat'), (10, 'Billy Cobham')]\n"
     ]
    }
   ],
   "source": [
    "from langchain_community.utilities import SQLDatabase\n",
    "\n",
    "db = SQLDatabase.from_uri(\"sqlite:///Chinook.db\")\n",
    "print(db.dialect)\n",
    "print(db.get_usable_table_names())\n",
    "print(db.run(\"SELECT * FROM Artist LIMIT 10;\"))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "2e572e1f-99b5-46a2-9023-76d1e6256c0a",
   "metadata": {},
   "source": [
    "## Many tables\n",
    "\n",
    "One of the main pieces of information we need to include in our prompt is the schemas of the relevant tables. When we have very many tables, we can't fit all of the schemas in a single prompt. What we can do in such cases is first extract the names of the tables related to the user input, and then include only their schemas.\n",
    "\n",
    "One easy and reliable way to do this is using [tool-calling](/docs/how_to/tool_calling). Below, we show how we can use this feature to obtain output conforming to a desired format (in this case, a list of table names). We use the chat model's `.bind_tools` method to bind a tool in Pydantic format, and feed this into an output parser to reconstruct the object from the model's response.\n",
    "\n",
    "```{=mdx}\n",
    "import ChatModelTabs from \"@theme/ChatModelTabs\";\n",
    "\n",
    "<ChatModelTabs customVarName=\"llm\" />\n",
    "```"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "id": "d278de7e-9228-4265-abf2-7a5e214a7dd7",
   "metadata": {},
   "outputs": [],
   "source": [
    "# | output: false\n",
    "# | echo: false\n",
    "\n",
    "from langchain_openai import ChatOpenAI\n",
    "\n",
    "llm = ChatOpenAI()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "id": "dbfc94bb-1c64-4f77-9e65-fb2468f55a58",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "[Table(name='Genre')]"
      ]
     },
     "execution_count": 4,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "from langchain_core.output_parsers.openai_tools import PydanticToolsParser\n",
    "from langchain_core.prompts import ChatPromptTemplate\n",
    "from langchain_core.pydantic_v1 import BaseModel, Field\n",
    "\n",
    "\n",
    "class Table(BaseModel):\n",
    "    \"\"\"Table in SQL database.\"\"\"\n",
    "\n",
    "    name: str = Field(description=\"Name of table in SQL database.\")\n",
    "\n",
    "\n",
    "table_names = \"\\n\".join(db.get_usable_table_names())\n",
    "system = f\"\"\"Return the names of ALL the SQL tables that MIGHT be relevant to the user question. \\\n",
    "The tables are:\n",
    "\n",
    "{table_names}\n",
    "\n",
    "Remember to include ALL POTENTIALLY RELEVANT tables, even if you're not sure that they're needed.\"\"\"\n",
    "\n",
    "prompt = ChatPromptTemplate.from_messages(\n",
    "    [\n",
    "        (\"system\", system),\n",
    "        (\"human\", \"{input}\"),\n",
    "    ]\n",
    ")\n",
    "llm_with_tools = llm.bind_tools([Table])\n",
    "output_parser = PydanticToolsParser(tools=[Table])\n",
    "\n",
    "table_chain = prompt | llm_with_tools | output_parser\n",
    "\n",
    "table_chain.invoke({\"input\": \"What are all the genres of Alanis Morisette songs\"})"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "1641dbba-d359-4cb2-ac52-82dfae99f392",
   "metadata": {},
   "source": [
    "This works pretty well! Except, as we'll see below, we actually need a few other tables as well. This would be pretty difficult for the model to know based just on the user question. In this case, we might think to simplify our model's job by grouping the tables together. We'll just ask the model to choose between categories \"Music\" and \"Business\", and then take care of selecting all the relevant tables from there:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "id": "0ccb0bf5-c580-428f-9cde-a58772ae784e",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "[Table(name='Music'), Table(name='Business')]"
      ]
     },
     "execution_count": 5,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "system = \"\"\"Return the names of any SQL tables that are relevant to the user question.\n",
    "The tables are:\n",
    "\n",
    "Music\n",
    "Business\n",
    "\"\"\"\n",
    "\n",
    "prompt = ChatPromptTemplate.from_messages(\n",
    "    [\n",
    "        (\"system\", system),\n",
    "        (\"human\", \"{input}\"),\n",
    "    ]\n",
    ")\n",
    "\n",
    "category_chain = prompt | llm_with_tools | output_parser\n",
    "category_chain.invoke({\"input\": \"What are all the genres of Alanis Morisette songs\"})"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "id": "883eda7a-7d0f-4012-9658-6a1010c7cda9",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "['Album',\n",
       " 'Artist',\n",
       " 'Genre',\n",
       " 'MediaType',\n",
       " 'Playlist',\n",
       " 'PlaylistTrack',\n",
       " 'Track',\n",
       " 'Customer',\n",
       " 'Employee',\n",
       " 'Invoice',\n",
       " 'InvoiceLine']"
      ]
     },
     "execution_count": 6,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "from typing import List\n",
    "\n",
    "\n",
    "def get_tables(categories: List[Table]) -> List[str]:\n",
    "    tables = []\n",
    "    for category in categories:\n",
    "        if category.name == \"Music\":\n",
    "            tables.extend(\n",
    "                [\n",
    "                    \"Album\",\n",
    "                    \"Artist\",\n",
    "                    \"Genre\",\n",
    "                    \"MediaType\",\n",
    "                    \"Playlist\",\n",
    "                    \"PlaylistTrack\",\n",
    "                    \"Track\",\n",
    "                ]\n",
    "            )\n",
    "        elif category.name == \"Business\":\n",
    "            tables.extend([\"Customer\", \"Employee\", \"Invoice\", \"InvoiceLine\"])\n",
    "    return tables\n",
    "\n",
    "\n",
    "table_chain = category_chain | get_tables\n",
    "table_chain.invoke({\"input\": \"What are all the genres of Alanis Morisette songs\"})"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "04d52d01-1ccf-4753-b34a-0dcbc4921f78",
   "metadata": {},
   "source": [
    "Now that we've got a chain that can output the relevant tables for any query we can combine this with our [create_sql_query_chain](https://api.python.langchain.com/en/latest/chains/langchain.chains.sql_database.query.create_sql_query_chain.html), which can accept a list of `table_names_to_use` to determine which table schemas are included in the prompt:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "id": "79f2a5a2-eb99-47e3-9c2b-e5751a800174",
   "metadata": {},
   "outputs": [],
   "source": [
    "from operator import itemgetter\n",
    "\n",
    "from langchain.chains import create_sql_query_chain\n",
    "from langchain_core.runnables import RunnablePassthrough\n",
    "\n",
    "query_chain = create_sql_query_chain(llm, db)\n",
    "# Convert \"question\" key to the \"input\" key expected by current table_chain.\n",
    "table_chain = {\"input\": itemgetter(\"question\")} | table_chain\n",
    "# Set table_names_to_use using table_chain.\n",
    "full_chain = RunnablePassthrough.assign(table_names_to_use=table_chain) | query_chain"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "id": "3c74b418-aa9a-4eb5-89dd-6e1a99a21344",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "SELECT DISTINCT \"g\".\"Name\"\n",
      "FROM \"Genre\" g\n",
      "JOIN \"Track\" t ON \"g\".\"GenreId\" = \"t\".\"GenreId\"\n",
      "JOIN \"Album\" a ON \"t\".\"AlbumId\" = \"a\".\"AlbumId\"\n",
      "JOIN \"Artist\" ar ON \"a\".\"ArtistId\" = \"ar\".\"ArtistId\"\n",
      "WHERE \"ar\".\"Name\" = 'Alanis Morissette'\n",
      "LIMIT 5;\n"
     ]
    }
   ],
   "source": [
    "query = full_chain.invoke(\n",
    "    {\"question\": \"What are all the genres of Alanis Morisette songs\"}\n",
    ")\n",
    "print(query)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "id": "ace7bdbf-728e-4f7b-a361-b44dae404481",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "\"[('Rock',)]\""
      ]
     },
     "execution_count": 9,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "db.run(query)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "7a717020-84c2-40f3-ba84-6624138d8e0c",
   "metadata": {},
   "source": [
    "We can see the LangSmith trace for this run [here](https://smith.langchain.com/public/4fbad408-3554-4f33-ab47-1e510a1b52a3/r).\n",
    "\n",
    "We've seen how to dynamically include a subset of table schemas in a prompt within a chain. Another possible approach to this problem is to let an Agent decide for itself when to look up tables by giving it a Tool to do so. You can see an example of this in the [SQL: Agents](/docs/tutorials/agents) guide."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cb9e54fd-64ca-4ed5-847c-afc635aae4f5",
   "metadata": {},
   "source": [
    "## High-cardinality columns\n",
    "\n",
    "In order to filter columns that contain proper nouns such as addresses, song names or artists, we first need to double-check the spelling in order to filter the data correctly. \n",
    "\n",
    "One naive strategy it to create a vector store with all the distinct proper nouns that exist in the database. We can then query that vector store each user input and inject the most relevant proper nouns into the prompt.\n",
    "\n",
    "First we need the unique values for each entity we want, for which we define a function that parses the result into a list of elements:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "id": "dc97e6ba-6055-4677-b0cc-c5425d5d4c81",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "['AC/DC', 'Accept', 'Aerosmith', 'Alanis Morissette', 'Alice In Chains']"
      ]
     },
     "execution_count": 10,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "import ast\n",
    "import re\n",
    "\n",
    "\n",
    "def query_as_list(db, query):\n",
    "    res = db.run(query)\n",
    "    res = [el for sub in ast.literal_eval(res) for el in sub if el]\n",
    "    res = [re.sub(r\"\\b\\d+\\b\", \"\", string).strip() for string in res]\n",
    "    return res\n",
    "\n",
    "\n",
    "proper_nouns = query_as_list(db, \"SELECT Name FROM Artist\")\n",
    "proper_nouns += query_as_list(db, \"SELECT Title FROM Album\")\n",
    "proper_nouns += query_as_list(db, \"SELECT Name FROM Genre\")\n",
    "len(proper_nouns)\n",
    "proper_nouns[:5]"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "22efa968-1879-4d7a-858f-7899dfa57454",
   "metadata": {},
   "source": [
    "Now we can embed and store all of our values in a vector database:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "id": "ea50abce-545a-4dc3-8795-8d364f7d142a",
   "metadata": {},
   "outputs": [],
   "source": [
    "from langchain_community.vectorstores import FAISS\n",
    "from langchain_openai import OpenAIEmbeddings\n",
    "\n",
    "vector_db = FAISS.from_texts(proper_nouns, OpenAIEmbeddings())\n",
    "retriever = vector_db.as_retriever(search_kwargs={\"k\": 15})"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a5d1d5c0-0928-40a4-b961-f1afe03cd5d3",
   "metadata": {},
   "source": [
    "And put together a query construction chain that first retrieves values from the database and inserts them into the prompt:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "id": "006b2955-4c06-4597-9c1d-442f77cd0261",
   "metadata": {},
   "outputs": [],
   "source": [
    "from operator import itemgetter\n",
    "\n",
    "from langchain_core.prompts import ChatPromptTemplate\n",
    "from langchain_core.runnables import RunnablePassthrough\n",
    "\n",
    "system = \"\"\"You are a SQLite expert. Given an input question, create a syntactically\n",
    "correct SQLite query to run. Unless otherwise specificed, do not return more than\n",
    "{top_k} rows.\n",
    "\n",
    "Only return the SQL query with no markup or explanation.\n",
    "\n",
    "Here is the relevant table info: {table_info}\n",
    "\n",
    "Here is a non-exhaustive list of possible feature values. If filtering on a feature\n",
    "value make sure to check its spelling against this list first:\n",
    "\n",
    "{proper_nouns}\n",
    "\"\"\"\n",
    "\n",
    "prompt = ChatPromptTemplate.from_messages([(\"system\", system), (\"human\", \"{input}\")])\n",
    "\n",
    "query_chain = create_sql_query_chain(llm, db, prompt=prompt)\n",
    "retriever_chain = (\n",
    "    itemgetter(\"question\")\n",
    "    | retriever\n",
    "    | (lambda docs: \"\\n\".join(doc.page_content for doc in docs))\n",
    ")\n",
    "chain = RunnablePassthrough.assign(proper_nouns=retriever_chain) | query_chain"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "12b0ed60-2536-4f82-85df-e096a272072a",
   "metadata": {},
   "source": [
    "To try out our chain, let's see what happens when we try filtering on \"elenis moriset\", a misspelling of Alanis Morissette, without and with retrieval:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "id": "5ba81336-0853-43da-8b07-5a256b8ba0b6",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "SELECT DISTINCT g.Name \n",
      "FROM Track t\n",
      "JOIN Album a ON t.AlbumId = a.AlbumId\n",
      "JOIN Artist ar ON a.ArtistId = ar.ArtistId\n",
      "JOIN Genre g ON t.GenreId = g.GenreId\n",
      "WHERE ar.Name = 'Elenis Moriset';\n"
     ]
    },
    {
     "data": {
      "text/plain": [
       "''"
      ]
     },
     "execution_count": 13,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# Without retrieval\n",
    "query = query_chain.invoke(\n",
    "    {\"question\": \"What are all the genres of elenis moriset songs\", \"proper_nouns\": \"\"}\n",
    ")\n",
    "print(query)\n",
    "db.run(query)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 15,
   "id": "fcdd8432-07a4-4609-8214-b1591dd94950",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "SELECT DISTINCT Genre.Name\n",
      "FROM Genre\n",
      "JOIN Track ON Genre.GenreId = Track.GenreId\n",
      "JOIN Album ON Track.AlbumId = Album.AlbumId\n",
      "JOIN Artist ON Album.ArtistId = Artist.ArtistId\n",
      "WHERE Artist.Name = 'Elenis Moriset'\n"
     ]
    },
    {
     "data": {
      "text/plain": [
       "''"
      ]
     },
     "execution_count": 15,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# Without retrieval\n",
    "query = query_chain.invoke(\n",
    "    {\"question\": \"What are all the genres of elenis moriset songs\", \"proper_nouns\": \"\"}\n",
    ")\n",
    "print(query)\n",
    "db.run(query)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "id": "cbbff7cc-c616-41eb-bbf5-08bd42c6808e",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "SELECT DISTINCT g.Name\n",
      "FROM Genre g\n",
      "JOIN Track t ON g.GenreId = t.GenreId\n",
      "JOIN Album a ON t.AlbumId = a.AlbumId\n",
      "JOIN Artist ar ON a.ArtistId = ar.ArtistId\n",
      "WHERE ar.Name = 'Alanis Morissette';\n"
     ]
    },
    {
     "data": {
      "text/plain": [
       "\"[('Rock',)]\""
      ]
     },
     "execution_count": 14,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# With retrieval\n",
    "query = chain.invoke({\"question\": \"What are all the genres of elenis moriset songs\"})\n",
    "print(query)\n",
    "db.run(query)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "7f99181b-a75c-4ff3-b37b-33f99a506581",
   "metadata": {},
   "source": [
    "We can see that with retrieval we're able to correct the spelling from \"Elenis Moriset\" to \"Alanis Morissette\" and get back a valid result.\n",
    "\n",
    "Another possible approach to this problem is to let an Agent decide for itself when to look up proper nouns. You can see an example of this in the [SQL: Agents](/docs/tutorials/agents) guide."
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3 (ipykernel)",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.10.4"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}