dvwn commited on
Commit
ccb82de
·
1 Parent(s): 0731ede

Update Schema Retrieval using RAG and LangChain

Browse files

1. Packages Requirements:

pip install > langchain-community chromadb langchain-chroma langchain-huggingface sentence-transformer

2. File Changes:
- db_manager.py
- .env
- .gitignore
- requirements.txt

.env CHANGED
@@ -1 +1 @@
1
- HF_TOKEN='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'
 
1
+ HF_TOKEN='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'
.gitignore ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Ignore environment variables / secrets
2
+ .env
3
+
4
+ # Ignore Python virtual environment
5
+ venv/
6
+ env/
7
+
8
+ # Ignore local Chroma database
9
+ chroma_db/
10
+
11
+ # Ignore Python cache files
12
+ __pycache__/
13
+ *.pyc
requirements.txt CHANGED
Binary files a/requirements.txt and b/requirements.txt differ
 
src/database/__pycache__/db_manager.cpython-313.pyc CHANGED
Binary files a/src/database/__pycache__/db_manager.cpython-313.pyc and b/src/database/__pycache__/db_manager.cpython-313.pyc differ
 
src/database/db_manager.py CHANGED
@@ -1,234 +1,87 @@
1
- #"""Database helpers for the NL2SQL project."""
2
-
 
3
  import os
4
- import re
5
  import sqlite3
6
- from typing import Dict, List
7
-
8
-
9
- DB_PATH = os.path.join(os.path.dirname(__file__), "Chinook_Sqlite.sqlite")
10
- STOPWORDS = {
11
- "a",
12
- "all",
13
- "an",
14
- "and",
15
- "are",
16
- "as",
17
- "at",
18
- "by",
19
- "count",
20
- "each",
21
- "find",
22
- "for",
23
- "from",
24
- "give",
25
- "has",
26
- "have",
27
- "how",
28
- "in",
29
- "is",
30
- "list",
31
- "many",
32
- "most",
33
- "name",
34
- "names",
35
- "of",
36
- "on",
37
- "show",
38
- "the",
39
- "their",
40
- "there",
41
- "to",
42
- "total",
43
- "what",
44
- "which",
45
- "who",
46
- "with",
47
- }
48
-
49
-
50
  def get_db_connection():
51
- """Establish a connection to the SQLite database."""
52
  try:
53
- connection = sqlite3.connect(DB_PATH)
54
- connection.row_factory = sqlite3.Row
55
- return connection
56
- except sqlite3.Error as error:
57
- print(f"Error connecting to database: {error}")
58
  return None
59
 
60
-
61
- def _tokenize(text: str) -> set[str]:
62
- tokens = re.findall(r"[A-Za-z0-9]+", text.lower())
63
- return {token for token in tokens if token not in STOPWORDS}
64
-
65
-
66
- def _quote_identifier(identifier: str) -> str:
67
- escaped_identifier = identifier.replace('"', '""')
68
- return f'"{escaped_identifier}"'
69
-
70
-
71
- def _load_schema_metadata(connection: sqlite3.Connection) -> Dict[str, Dict[str, object]]:
72
- cursor = connection.cursor()
73
- cursor.execute(
74
- """
75
- SELECT name, sql
76
- FROM sqlite_master
77
- WHERE type = 'table' AND name NOT LIKE 'sqlite_%'
78
- ORDER BY name
79
- """
80
- )
81
-
82
- metadata: Dict[str, Dict[str, object]] = {}
83
- for row in cursor.fetchall():
84
- table_name = row["name"]
85
- quoted_table = _quote_identifier(table_name)
86
-
87
- columns = cursor.execute(f"PRAGMA table_info({quoted_table})").fetchall()
88
- foreign_keys = cursor.execute(f"PRAGMA foreign_key_list({quoted_table})").fetchall()
89
-
90
- metadata[table_name] = {
91
- "ddl": row["sql"] or "",
92
- "columns": [
93
- {
94
- "name": column["name"],
95
- "type": column["type"] or "TEXT",
96
- "notnull": bool(column["notnull"]),
97
- "pk": bool(column["pk"]),
98
- }
99
- for column in columns
100
- ],
101
- "foreign_keys": [
102
- {
103
- "from": foreign_key["from"],
104
- "to_table": foreign_key["table"],
105
- "to_column": foreign_key["to"],
106
- }
107
- for foreign_key in foreign_keys
108
- ],
109
- }
110
-
111
- return metadata
112
-
113
-
114
- def _build_table_summary(table_name: str, table_info: Dict[str, object]) -> str:
115
- column_parts = []
116
- for column in table_info["columns"]:
117
- tags = []
118
- if column["pk"]:
119
- tags.append("PK")
120
- if column["notnull"]:
121
- tags.append("NOT NULL")
122
-
123
- tag_suffix = f" [{' '.join(tags)}]" if tags else ""
124
- column_parts.append(f"{column['name']} {column['type']}{tag_suffix}")
125
-
126
- summary = f"Table {table_name}: {', '.join(column_parts)}"
127
- if table_info["foreign_keys"]:
128
- relationships = ", ".join(
129
- f"{table_name}.{foreign_key['from']} -> "
130
- f"{foreign_key['to_table']}.{foreign_key['to_column']}"
131
- for foreign_key in table_info["foreign_keys"]
132
- )
133
- summary = f"{summary}\nRelationships: {relationships}"
134
-
135
- return summary
136
-
137
-
138
- def _rank_tables(
139
- metadata: Dict[str, Dict[str, object]], question: str | None, max_tables: int
140
- ) -> List[str]:
141
- table_names = list(metadata.keys())
142
- if not question:
143
- return table_names
144
-
145
- question_tokens = _tokenize(question)
146
- if not question_tokens:
147
- return table_names
148
-
149
- scored_tables = []
150
- for table_name, table_info in metadata.items():
151
- table_tokens = _tokenize(table_name)
152
- column_tokens = set()
153
- for column in table_info["columns"]:
154
- column_tokens.update(_tokenize(column["name"]))
155
-
156
- score = 0
157
- score += 4 * len(question_tokens & table_tokens)
158
- score += 2 * len(question_tokens & column_tokens)
159
-
160
- singular_name = table_name[:-1].lower() if table_name.lower().endswith("s") else ""
161
- if singular_name and singular_name in question.lower():
162
- score += 2
163
- if table_name.lower() in question.lower():
164
- score += 3
165
-
166
- scored_tables.append((score, table_name))
167
-
168
- scored_tables.sort(key=lambda item: (-item[0], item[1]))
169
- selected = [table_name for score, table_name in scored_tables if score > 0][:max_tables]
170
-
171
- if not selected:
172
- selected = [table_name for _, table_name in scored_tables[:max_tables]]
173
-
174
- # Pull in directly related tables so the model sees valid join paths.
175
- expanded = list(selected)
176
- for table_name in selected:
177
- for foreign_key in metadata[table_name]["foreign_keys"]:
178
- related_table = foreign_key["to_table"]
179
- if related_table in metadata and related_table not in expanded:
180
- expanded.append(related_table)
181
-
182
- for table_name, table_info in metadata.items():
183
- for foreign_key in table_info["foreign_keys"]:
184
- if foreign_key["to_table"] in selected and table_name not in expanded:
185
- expanded.append(table_name)
186
-
187
- return expanded[: max(max_tables, len(expanded))]
188
-
189
-
190
- def get_schema_context(question: str | None = None, max_tables: int = 7) -> str:
191
- """Extract schema information for prompt construction.
192
-
193
- When a question is provided, the returned schema is narrowed to the most
194
- relevant tables plus their immediate relationships. This keeps prompts
195
- smaller while preserving valid join paths.
196
  """
 
 
197
 
198
- connection = get_db_connection()
199
- if not connection:
200
- return "Unable to connect to the database to retrieve schema information."
201
 
 
 
 
 
 
 
202
  try:
203
- metadata = _load_schema_metadata(connection)
204
- finally:
205
- connection.close()
206
-
207
- selected_tables = _rank_tables(metadata, question, max_tables=max_tables)
208
- schema_sections = [_build_table_summary(table_name, metadata[table_name]) for table_name in selected_tables]
209
-
210
- all_relationships = []
211
- for table_name in selected_tables:
212
- for foreign_key in metadata[table_name]["foreign_keys"]:
213
- if foreign_key["to_table"] in selected_tables:
214
- all_relationships.append(
215
- f"{table_name}.{foreign_key['from']} = "
216
- f"{foreign_key['to_table']}.{foreign_key['to_column']}"
217
- )
218
-
219
- if all_relationships:
220
- schema_sections.append("Join paths:\n" + "\n".join(sorted(set(all_relationships))))
221
-
222
- return "\n\n".join(schema_sections)
223
-
224
-
225
  if __name__ == "__main__":
226
- connection = get_db_connection()
227
- if connection:
228
- print("Database connection successful!")
229
- cursor = connection.cursor()
230
- cursor.execute("SELECT name FROM sqlite_master WHERE type='table';")
231
- print("Tables in the database:", [row[0] for row in cursor.fetchall()])
232
- connection.close()
233
- else:
234
- print("Failed to connect to the database.")
 
1
+ # Path: src/database/db_manager.py
2
+ # This module provides a function to establish a connection to the SQLite database.
3
+ # Include a RAG-based dynamic schema retrieval.
4
  import os
 
5
  import sqlite3
6
+ from dotenv import load_dotenv
7
+ from langchain_community.utilities import SQLDatabase
8
+ from langchain_chroma import Chroma
9
+ from langchain_huggingface import HuggingFaceEmbeddings
10
+ from langchain_core.documents import Document
11
+
12
+ load_dotenv()
13
+
14
+ # The path to SQLite database
15
+ DB_PATH = os.getenv("SQLITE_DB_PATH", "src/database/Chinook_Sqlite.sqlite")
16
+ DB_URI = f"sqlite:///{DB_PATH}"
17
+
18
+ # Define RAG table description as knowledge base
19
+ TABLE_DESCRIPTIONS = [
20
+ Document(page_content="Contains music albums associated with artists", metadata={"table_name": "Album"}),
21
+ Document(page_content="Contains information about artists", metadata={"table_name": "Artist"}),
22
+ Document(page_content="Contains customer information like name, address, phone, and email", metadata={"table_name": "Customer"}),
23
+ Document(page_content="Contains employee information such as name, title, hire date, and manager", metadata={"table_name": "Employee"}),
24
+ Document(page_content="Contains musical genres like Rock, Jazz, or Metal", metadata={"table_name": "Genre"}),
25
+ Document(page_content="Contains details of invoices including billing information and total amount", metadata={"table_name": "Invoice"}),
26
+ Document(page_content="Contains line items for each invoice, linking to the purchased tracks.", metadata={"table_name": "InvoiceLine"}),
27
+ Document(page_content="Contains media types like MPEG audio or AAC audio.", metadata={"table_name": "MediaType"}),
28
+ Document(page_content="Contains custom playlists created by users.", metadata={"table_name": "Playlist"}),
29
+ Document(page_content="Mapping table linking tracks to playlists.", metadata={"table_name": "PlaylistTrack"}),
30
+ Document(page_content="Contains details of music tracks including name, album, genre, and composer", metadata={"table_name": "Track"}),
31
+ ]
32
+
33
+ # Initialize Chroma vector store with HuggingFace embeddings
34
+ embedding_model = HuggingFaceEmbeddings(model_name="all-MiniLM-L6-v2")
35
+
36
+ # Initialize Chroma vector store with a persistent directory for storing embeddings
37
+ vector_store = Chroma.from_documents(
38
+ documents = TABLE_DESCRIPTIONS,
39
+ embedding = embedding_model,
40
+ persist_directory = "chroma_db"
41
+ )
42
+
43
+ # Get a connection to the SQLite database
 
 
 
 
 
 
44
  def get_db_connection():
 
45
  try:
46
+ return sqlite3.connect(DB_PATH)
47
+ except sqlite3.Error as e:
48
+ print(f"Error connecting to database: {e}")
 
 
49
  return None
50
 
51
+ # Retrieve relevant schema context based on the user's question
52
+ def get_relevant_tables(question: str, top_k: int = 4) -> list:
53
+ """
54
+ RAG function: Retrieves the most relevant table descriptions based on the user's question.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
55
  """
56
+ # Perform a similarity search in vector store
57
+ docs = vector_store.similarity_search(question, k=top_k)
58
 
59
+ # Extract table names from the retrieved documents
60
+ relevant_tables = [doc.metadata["table_name"] for doc in docs]
61
+ return relevant_tables
62
 
63
+ # Get database schema context
64
+ def get_schema_context(question=None):
65
+ """
66
+ Returns the DDL and sample rows.
67
+ If a question is provided, it retrieves the most relevant tables based on the question and returns their schema information.
68
+ """
69
  try:
70
+ tables_to_include = None
71
+
72
+ # Trigger RAG retrieval if a question is provided
73
+ if question:
74
+ tables_to_include = get_relevant_tables(question)
75
+ print(f"RAG selected tables for question '{question}': {tables_to_include}")
76
+
77
+ # Connect to the database and retrieve schema information
78
+ db = SQLDatabase.from_uri(DB_URI, include_tables = tables_to_include, sample_rows_in_table_info=5)
79
+ return db.get_table_info()
80
+ except Exception as e:
81
+ return f"Error retrieving database schema: {e}"
82
+
 
 
 
 
 
 
 
 
 
83
  if __name__ == "__main__":
84
+ # Example usage: Get schema context for a specific question
85
+ question = "What tables contain information about music tracks and their genres?"
86
+ schema_context = get_schema_context(question)
87
+ print(schema_context)