Skip to content

Commit de966bb

Browse files
author
Shrijeeth-Suresh
committed
feat: add SQL table schema extractor and describer agent with auto-generation script
1 parent 84999ee commit de966bb

17 files changed

Lines changed: 447 additions & 59 deletions

File tree

Makefile

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ format:
1313
upsert-data:
1414
cd backend && python -m scripts.clear_vector_db && python -m scripts.add_sample_queries_qdrant && python -m scripts.add_table_schemas
1515

16+
generate-data:
17+
cd backend && python -m scripts.generate_tables_json
18+
1619
run-backend:
1720
cd backend && uvicorn main:app --port 8000
1821

README.md

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,11 @@ QueryGPT-ADK is an open-source, multi-agent system for natural language to SQL q
2121

2222
- Natural Language to SQL: Converts user questions into SQL queries using LLM-based agents.
2323
- Multi-Agent Architecture: Modular agents for query formation, explanation, table/column selection, and validation.
24+
- **Automatic Schema Extraction & Table Description:**
25+
- New agent (`sql_table_describer_agent`) for extracting and describing SQL table schemas, including sub-agents for table-level tasks.
26+
- Includes a script to auto-generate JSON documentation of all tables, views, and enums from your database (`generate_tables_json.py`).
27+
- Robust SQL Query Validation: Ensures only valid SELECT statements are executed, supporting both MySQL and PostgreSQL.
28+
2429
- Vector Search Integration: Uses Qdrant for semantic search over sample queries and table schemas.
2530
- Validation: Ensures only valid SELECT queries are generated and executed.
2631
- **Production-Ready Security:**
@@ -44,6 +49,12 @@ QueryGPT-ADK is an open-source, multi-agent system for natural language to SQL q
4449

4550
## Changelog
4651

52+
### 2025-05-23
53+
- Added `sql_table_describer_agent` for automated SQL table schema extraction and description.
54+
- Introduced `backend/scripts/generate_tables_json.py` for generating up-to-date JSON documentation of all database tables, views, and enums.
55+
- Enhanced SQL query validation tools to support both MySQL and PostgreSQL, with improved error handling.
56+
- Updated configuration and helper utilities for multi-database and LLM support.
57+
4758
### 2025-05-20
4859

4960
- **PostgreSQL Support**: Added support for PostgreSQL alongside MySQL for query validation.
@@ -92,7 +103,16 @@ POSTGRES_DB_URL=postgresql+asyncpg://user:password@localhost:5432/querygpt
92103
REDIS_URL=redis://localhost:6379/1
93104
```
94105

95-
4. Set up environment variables (Frontend):
106+
4. Generate up-to-date table and enum documentation (Backend):
107+
108+
```bash
109+
cd backend
110+
python -m scripts.generate_tables_json
111+
```
112+
113+
This command will connect to your configured database and auto-generate the latest tables, views, and enums documentation in `backend/scripts/data/tables.json`. Before running this command, make sure your database is up and running and the connection string in `.env` is correct for validation database. You also need LLM provider and API key for this command to work. Set `SYNTHETIC_DATA_LLM_MODEL` and `SYNTHETIC_DATA_LLM_API_KEY` in `.env` for this to work.
114+
115+
5. Set up environment variables (Frontend):
96116

97117
```bash
98118
cd frontend
@@ -105,7 +125,7 @@ Edit `.env` and set your API base URL as needed. Example:
105125
NEXT_PUBLIC_API_BASE_URL=http://localhost:8000
106126
```
107127

108-
5. Set up the Frontend (Next.js UI):
128+
6. Set up the Frontend (Next.js UI):
109129

110130
```bash
111131
cd frontend
@@ -116,22 +136,22 @@ npm run dev
116136

117137
This will start the frontend at [http://localhost:3000](http://localhost:3000).
118138

119-
6. Clear Qdrant database (Optional):
139+
7. Clear Qdrant database (Optional):
120140

121141
```bash
122142
cd backend
123143
python scripts/clear_vector_db.py
124144
```
125145

126-
7. Load db data into Qdrant:
146+
8. Load db data into Qdrant:
127147

128148
```bash
129149
cd backend
130150
python scripts/add_table_schemas.py
131151
python scripts/add_sample_queries_qdrant.py
132152
```
133153

134-
8. (Optional) Use Your Own Data:
154+
9. (Optional) Use Your Own Data:
135155

136156
To use QueryGPT-ADK with your own data, replace the provided `sample_queries.json` and `tables.json` files in the `backend/scripts/data/` directory with your own files. Make sure your files follow the same structure as the samples. Then, rerun the data loading scripts:
137157

@@ -388,7 +408,7 @@ curl -X POST "http://localhost:8000/query" \
388408
- Enable schema and collection import for semantic search and retrieval.
389409
- Ensure integration of custom vector DBs with agentic workflow and query tools.
390410

391-
### Generate Synthetic Data for a Database (To Do)
411+
### Generate Synthetic Data for a Database (In Progress)
392412

393413
- Add support for generating synthetic data for a given database.
394414
- Provide a UI and/or CLI for users to generate synthetic data for their own database.

backend/agents/query_agent/sub_agents/query_validation_agent/tools.py

Lines changed: 21 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,58 +1,39 @@
1-
import mysql.connector
2-
import psycopg2
31
from config import get_settings
42
from google.adk.tools import FunctionTool
53
from mysql.connector import Error as MySQLError
64
from psycopg2 import Error as PostgresError
5+
from utils.helpers import (
6+
get_validate_db_mysql_connection,
7+
get_validate_db_postgres_connection,
8+
)
79

810

9-
def validate_sql_query_mysql(query: str) -> dict:
10-
conn = mysql.connector.connect(
11-
host=get_settings().VALIDATE_DB_HOST,
12-
port=get_settings().VALIDATE_DB_PORT,
13-
user=get_settings().VALIDATE_DB_USER,
14-
password=get_settings().VALIDATE_DB_PASSWORD,
15-
database=get_settings().VALIDATE_DB_DATABASE,
16-
autocommit=False,
17-
)
18-
cursor = conn.cursor()
11+
def validate_sql_query_mysql(query: str) -> tuple[dict, object]:
12+
conn, cursor = get_validate_db_mysql_connection()
1913
query_type = query.strip().split()[0].upper()
2014

2115
if query_type == "SELECT":
2216
try:
2317
cursor.execute(f"EXPLAIN {query}")
24-
conn.close()
25-
return {"valid": "true", "error": ""}
18+
return {"valid": "true", "error": ""}, conn
2619
except MySQLError as e:
27-
conn.close()
28-
return {"valid": "false", "error": str(e)}
20+
return {"valid": "false", "error": str(e)}, conn
2921
else:
30-
conn.close()
31-
return {"valid": "false", "error": "Only SELECT statements are allowed"}
22+
return {"valid": "false", "error": "Only SELECT statements are allowed"}, conn
3223

3324

34-
def validate_sql_query_postgres(query: str) -> dict:
35-
conn = psycopg2.connect(
36-
host=get_settings().VALIDATE_DB_HOST,
37-
port=get_settings().VALIDATE_DB_PORT,
38-
user=get_settings().VALIDATE_DB_USER,
39-
password=get_settings().VALIDATE_DB_PASSWORD,
40-
database=get_settings().VALIDATE_DB_DATABASE,
41-
)
42-
cursor = conn.cursor()
25+
def validate_sql_query_postgres(query: str) -> tuple[dict, object]:
26+
conn, cursor = get_validate_db_postgres_connection()
4327
query_type = query.strip().split()[0].upper()
4428

4529
if query_type == "SELECT":
4630
try:
4731
cursor.execute(f"EXPLAIN {query}")
48-
conn.close()
49-
return {"valid": "true", "error": ""}
32+
return {"valid": "true", "error": ""}, conn
5033
except PostgresError as e:
51-
conn.close()
52-
return {"valid": "false", "error": str(e)}
34+
return {"valid": "false", "error": str(e)}, conn
5335
else:
54-
conn.close()
55-
return {"valid": "false", "error": "Only SELECT statements are allowed"}
36+
return {"valid": "false", "error": "Only SELECT statements are allowed"}, conn
5637

5738

5839
def validate_sql_query(query: str) -> dict:
@@ -67,15 +48,20 @@ def validate_sql_query(query: str) -> dict:
6748
Returns:
6849
dict: A dictionary containing the validation result (valid: bool) and error message (error: str)
6950
"""
51+
conn = None
7052
try:
7153
if get_settings().VALIDATE_DB_TYPE == "mysql":
72-
return validate_sql_query_mysql(query)
54+
result, conn = validate_sql_query_mysql(query)
7355
elif get_settings().VALIDATE_DB_TYPE == "postgresql":
74-
return validate_sql_query_postgres(query)
56+
result, conn = validate_sql_query_postgres(query)
7557
else:
7658
return {"valid": "false", "error": "Unsupported database type"}
59+
return result
7760
except Exception as e:
7861
return {"valid": "false", "error": str(e)}
62+
finally:
63+
if conn:
64+
conn.close()
7965

8066

8167
validate_sql_query_tool = FunctionTool(
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
from . import agent
2+
3+
__all__ = ["agent"]
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
from google.adk.agents import LoopAgent
2+
3+
from .sub_agents import table_count_check_agent, table_description_agent
4+
5+
root_agent = LoopAgent(
6+
name="sql_table_describer_agent",
7+
sub_agents=[
8+
table_description_agent,
9+
table_count_check_agent,
10+
],
11+
max_iterations=3,
12+
)
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
from .table_count_check_agent.agent import root_agent as table_count_check_agent
2+
from .table_description_agent.agent import root_agent as table_description_agent
3+
4+
__all__ = ["table_count_check_agent", "table_description_agent"]

backend/agents/sql_table_describer_agent/sub_agents/table_count_check_agent/__init__.py

Whitespace-only changes.
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
from typing import AsyncGenerator
2+
3+
from google.adk.agents import BaseAgent
4+
from google.adk.agents.invocation_context import InvocationContext
5+
from google.adk.events import Event, EventActions
6+
7+
8+
class ValidateTableCount(BaseAgent):
9+
async def _run_async_impl(
10+
self, ctx: InvocationContext
11+
) -> AsyncGenerator[Event, None]:
12+
try:
13+
table_descriptions = ctx.session.state.get("sql_table_description", "")
14+
input_query = ctx.session.events[-1].content.parts[0].text
15+
input_tables_count = len(input_query.strip().split(";"))
16+
table_descriptions_count = len(table_descriptions["table_descriptions"])
17+
should_stop = input_tables_count == table_descriptions_count
18+
yield Event(
19+
author=self.name,
20+
actions=EventActions(
21+
escalate=should_stop,
22+
),
23+
)
24+
except Exception as e:
25+
print("Exception: ", e)
26+
yield Event(
27+
author=self.name,
28+
actions=EventActions(
29+
escalate=False,
30+
),
31+
)
32+
33+
34+
root_agent = ValidateTableCount(name="validate_table_count")

backend/agents/sql_table_describer_agent/sub_agents/table_description_agent/__init__.py

Whitespace-only changes.
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
from config import get_settings
2+
from google.adk.agents import LlmAgent
3+
from google.adk.models.lite_llm import LiteLlm
4+
from google.genai import types
5+
6+
from .prompts import agent_description, agent_instruction
7+
from .types import TableDescriptionSchema
8+
9+
AGENT_MODEL = LiteLlm(
10+
model=get_settings().SYNTHETIC_DATA_LLM_MODEL,
11+
api_key=get_settings().SYNTHETIC_DATA_LLM_API_KEY,
12+
response_format=TableDescriptionSchema,
13+
)
14+
15+
16+
root_agent = LlmAgent(
17+
name="sql_table_describer_agent",
18+
model=AGENT_MODEL,
19+
description=agent_description(version=1),
20+
instruction=agent_instruction(version=1),
21+
output_key="sql_table_description",
22+
generate_content_config=types.GenerateContentConfig(
23+
temperature=get_settings().SYNTHETIC_DATA_LLM_TEMPERATURE,
24+
),
25+
output_schema=TableDescriptionSchema,
26+
)

0 commit comments

Comments
 (0)