Spaces:
Sleeping
Sleeping
api code changes for schema reader
Browse files- backend/app/api/endpoints.py +63 -2
- backend/app/db/db_connector.py +10 -3
- backend/app/db/schema_reader.py +0 -24
- backend/app/services/query_executor.py +6 -1
- backend/app/services/sql_generator.py +88 -27
- backend/app/services/utility.py +78 -47
- backend/requirements.txt +5 -1
backend/app/api/endpoints.py
CHANGED
|
@@ -23,7 +23,7 @@ class QueryRequest(BaseModel):
|
|
| 23 |
from fastapi import HTTPException
|
| 24 |
|
| 25 |
@router.post("/process-text")
|
| 26 |
-
def
|
| 27 |
try:
|
| 28 |
sql = generate_sql_query(req.question)
|
| 29 |
|
|
@@ -34,6 +34,7 @@ def ask_query(req: QueryRequest):
|
|
| 34 |
|
| 35 |
# Use utility method to check if the string is a valid SQL query
|
| 36 |
formattedSqlQuery = UtilityClass.is_valid_sql_query(sql)
|
|
|
|
| 37 |
if not formattedSqlQuery:
|
| 38 |
return {"message": str(sql) if sql else "No SQL query could be generated for your question."}
|
| 39 |
|
|
@@ -50,13 +51,73 @@ def ask_query(req: QueryRequest):
|
|
| 50 |
|
| 51 |
# Prepare heading and records JSON using LLM
|
| 52 |
result_json = UtilityClass.prepare_llm_heading_and_records(req.question, df)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 53 |
|
| 54 |
return {
|
| 55 |
"sql": sql,
|
| 56 |
"rows": result_json["records"],
|
| 57 |
-
"heading":
|
|
|
|
| 58 |
"chart": chart
|
| 59 |
}
|
| 60 |
except Exception as e:
|
| 61 |
raise HTTPException(status_code=400, detail=f"An error occurred: {str(e)}")
|
| 62 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 23 |
from fastapi import HTTPException
|
| 24 |
|
| 25 |
@router.post("/process-text")
|
| 26 |
+
def process_text(req: QueryRequest):
|
| 27 |
try:
|
| 28 |
sql = generate_sql_query(req.question)
|
| 29 |
|
|
|
|
| 34 |
|
| 35 |
# Use utility method to check if the string is a valid SQL query
|
| 36 |
formattedSqlQuery = UtilityClass.is_valid_sql_query(sql)
|
| 37 |
+
print(f"Formatted SQL Query: {formattedSqlQuery}")
|
| 38 |
if not formattedSqlQuery:
|
| 39 |
return {"message": str(sql) if sql else "No SQL query could be generated for your question."}
|
| 40 |
|
|
|
|
| 51 |
|
| 52 |
# Prepare heading and records JSON using LLM
|
| 53 |
result_json = UtilityClass.prepare_llm_heading_and_records(req.question, df)
|
| 54 |
+
|
| 55 |
+
# Parse the heading if it's a JSON string containing heading and summary
|
| 56 |
+
heading_text = result_json["heading"]
|
| 57 |
+
summary_text = ""
|
| 58 |
+
|
| 59 |
+
if isinstance(heading_text, str):
|
| 60 |
+
try:
|
| 61 |
+
import json
|
| 62 |
+
# Check if it's wrapped in markdown code block
|
| 63 |
+
if heading_text.strip().startswith('```json') and heading_text.strip().endswith('```'):
|
| 64 |
+
# Extract JSON from markdown code block
|
| 65 |
+
json_content = heading_text.strip()
|
| 66 |
+
# Remove ```json from start and ``` from end
|
| 67 |
+
json_content = json_content[7:-3].strip() # Remove ```json and ```
|
| 68 |
+
heading_data = json.loads(json_content)
|
| 69 |
+
else:
|
| 70 |
+
# Try to parse directly
|
| 71 |
+
heading_data = json.loads(heading_text)
|
| 72 |
+
|
| 73 |
+
if isinstance(heading_data, dict):
|
| 74 |
+
heading_text = heading_data.get("heading", heading_text)
|
| 75 |
+
summary_text = heading_data.get("summary", summary_text)
|
| 76 |
+
except (json.JSONDecodeError, ValueError):
|
| 77 |
+
# If not JSON, use as-is
|
| 78 |
+
pass
|
| 79 |
|
| 80 |
return {
|
| 81 |
"sql": sql,
|
| 82 |
"rows": result_json["records"],
|
| 83 |
+
"heading": heading_text, # Send just the heading text
|
| 84 |
+
"summary": summary_text, # Send just the summary text
|
| 85 |
"chart": chart
|
| 86 |
}
|
| 87 |
except Exception as e:
|
| 88 |
raise HTTPException(status_code=400, detail=f"An error occurred: {str(e)}")
|
| 89 |
|
| 90 |
+
|
| 91 |
+
@router.get("/health")
|
| 92 |
+
def health_check():
|
| 93 |
+
"""
|
| 94 |
+
Health check endpoint to verify API and system status.
|
| 95 |
+
|
| 96 |
+
Returns:
|
| 97 |
+
dict: Health status information including system checks and timestamp
|
| 98 |
+
"""
|
| 99 |
+
try:
|
| 100 |
+
health_status = UtilityClass.get_health_status()
|
| 101 |
+
|
| 102 |
+
# Return appropriate HTTP status based on health
|
| 103 |
+
if health_status["status"] == "healthy":
|
| 104 |
+
return health_status
|
| 105 |
+
else:
|
| 106 |
+
# Return 503 Service Unavailable if any critical component is unhealthy
|
| 107 |
+
from fastapi import Response
|
| 108 |
+
import json
|
| 109 |
+
return Response(
|
| 110 |
+
content=json.dumps(health_status),
|
| 111 |
+
media_type="application/json",
|
| 112 |
+
status_code=503
|
| 113 |
+
)
|
| 114 |
+
|
| 115 |
+
except Exception as e:
|
| 116 |
+
# Return 503 if health check itself fails
|
| 117 |
+
from datetime import datetime
|
| 118 |
+
error_response = {
|
| 119 |
+
"status": "unhealthy",
|
| 120 |
+
"message": f"Health check failed: {str(e)}",
|
| 121 |
+
"timestamp": datetime.utcnow().isoformat()
|
| 122 |
+
}
|
| 123 |
+
raise HTTPException(status_code=503, detail=error_response)
|
backend/app/db/db_connector.py
CHANGED
|
@@ -24,16 +24,23 @@ def get_connection():
|
|
| 24 |
if not os.path.isfile(db_path):
|
| 25 |
raise FileNotFoundError(f"Database file not found at {db_path}. Set DB_PATH env variable or .env to the correct location.")
|
| 26 |
try:
|
| 27 |
-
|
|
|
|
|
|
|
| 28 |
conn.row_factory = sqlite3.Row # access columns by name
|
| 29 |
return conn
|
| 30 |
except sqlite3.OperationalError as e:
|
| 31 |
raise Exception(f"Error connecting to DB at {db_path}: {e}")
|
| 32 |
|
| 33 |
-
# Create engine
|
| 34 |
engine = create_engine(
|
| 35 |
DATABASE_URL,
|
| 36 |
-
connect_args={
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 37 |
)
|
| 38 |
|
| 39 |
# Session factory
|
|
|
|
| 24 |
if not os.path.isfile(db_path):
|
| 25 |
raise FileNotFoundError(f"Database file not found at {db_path}. Set DB_PATH env variable or .env to the correct location.")
|
| 26 |
try:
|
| 27 |
+
# Remove timeout restriction to allow long-running queries
|
| 28 |
+
# Complex AI-generated queries may take time and should not be interrupted
|
| 29 |
+
conn = sqlite3.connect(db_path, check_same_thread=False)
|
| 30 |
conn.row_factory = sqlite3.Row # access columns by name
|
| 31 |
return conn
|
| 32 |
except sqlite3.OperationalError as e:
|
| 33 |
raise Exception(f"Error connecting to DB at {db_path}: {e}")
|
| 34 |
|
| 35 |
+
# Create engine with no timeout restrictions for complex queries
|
| 36 |
engine = create_engine(
|
| 37 |
DATABASE_URL,
|
| 38 |
+
connect_args={
|
| 39 |
+
"check_same_thread": False, # Needed for SQLite threading
|
| 40 |
+
"timeout": 0 # No timeout - wait indefinitely for query completion
|
| 41 |
+
},
|
| 42 |
+
pool_timeout=None, # No pool timeout
|
| 43 |
+
pool_recycle=-1 # No connection recycling timeout
|
| 44 |
)
|
| 45 |
|
| 46 |
# Session factory
|
backend/app/db/schema_reader.py
CHANGED
|
@@ -24,27 +24,3 @@ def get_schema():
|
|
| 24 |
cursor.close()
|
| 25 |
conn.close()
|
| 26 |
|
| 27 |
-
def get_schema2():
|
| 28 |
-
"""Fetch database schema information (tables & columns)."""
|
| 29 |
-
engine = get_db_engine()
|
| 30 |
-
inspector = inspect(engine)
|
| 31 |
-
|
| 32 |
-
schema = {}
|
| 33 |
-
for table_name in inspector.get_table_names():
|
| 34 |
-
columns = [
|
| 35 |
-
{"name": col["name"], "type": str(col["type"])}
|
| 36 |
-
for col in inspector.get_columns(table_name)
|
| 37 |
-
]
|
| 38 |
-
schema[table_name] = columns
|
| 39 |
-
|
| 40 |
-
return schema
|
| 41 |
-
|
| 42 |
-
# def get_schema():
|
| 43 |
-
# engine = get_db_engine()
|
| 44 |
-
# inspector = inspect(engine)
|
| 45 |
-
|
| 46 |
-
# schema_info = {}
|
| 47 |
-
# for table in inspector.get_table_names():
|
| 48 |
-
# columns = [col["name"] for col in inspector.get_columns(table)]
|
| 49 |
-
# schema_info[table] = columns
|
| 50 |
-
# return schema_info
|
|
|
|
| 24 |
cursor.close()
|
| 25 |
conn.close()
|
| 26 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
backend/app/services/query_executor.py
CHANGED
|
@@ -23,7 +23,12 @@ def run_and_handle_sql_query(sql_query: str, user_question: str):
|
|
| 23 |
try:
|
| 24 |
rows = execute_sql_query(sql_query)
|
| 25 |
if not rows:
|
| 26 |
-
llm = ChatOpenAI(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 27 |
no_result_prompt = (
|
| 28 |
f"The following SQL query was generated for the user's question, but it returned no results. "
|
| 29 |
f"User question: {user_question}\nSQL query: {sql_query}\n"
|
|
|
|
| 23 |
try:
|
| 24 |
rows = execute_sql_query(sql_query)
|
| 25 |
if not rows:
|
| 26 |
+
llm = ChatOpenAI(
|
| 27 |
+
model="gpt-4o-mini",
|
| 28 |
+
temperature=0,
|
| 29 |
+
api_key=settings.OPENAI_API_KEY,
|
| 30 |
+
request_timeout=None # No timeout for API requests
|
| 31 |
+
)
|
| 32 |
no_result_prompt = (
|
| 33 |
f"The following SQL query was generated for the user's question, but it returned no results. "
|
| 34 |
f"User question: {user_question}\nSQL query: {sql_query}\n"
|
backend/app/services/sql_generator.py
CHANGED
|
@@ -6,55 +6,116 @@ from langchain.prompts import PromptTemplate, ChatPromptTemplate
|
|
| 6 |
from app.db.schema_reader import get_schema
|
| 7 |
from app.db.db_connector import get_db_engine
|
| 8 |
from app.db.db_connector import get_connection
|
|
|
|
| 9 |
|
| 10 |
-
# llm = ChatOpenAI(model="gpt-
|
| 11 |
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0, api_key=settings.OPENAI_API_KEY)
|
| 12 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
|
| 14 |
def generate_sql_query(user_question: str):
|
| 15 |
schema = get_schema()
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
)
|
| 19 |
|
| 20 |
prompt = ChatPromptTemplate.from_template("""
|
| 21 |
-
You are an expert SQLite query generator.
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 31 |
|
| 32 |
User question: {question}
|
| 33 |
""")
|
| 34 |
|
| 35 |
-
messages = prompt.format_messages(
|
| 36 |
sql_query = llm.invoke(messages).content.strip()
|
| 37 |
|
| 38 |
# If the LLM says it cannot generate a query, treat as normal chat
|
| 39 |
if sql_query.lower().startswith("i cannot generate this query"):
|
| 40 |
# Use the LLM as a chatbot for normal conversation
|
| 41 |
-
chat_prompt =
|
|
|
|
|
|
|
|
|
|
|
|
|
| 42 |
chat_response = llm.invoke([{"role": "user", "content": chat_prompt}]).content.strip()
|
| 43 |
return {"chat_message": chat_response}
|
| 44 |
|
| 45 |
return sql_query
|
| 46 |
|
| 47 |
-
def generate_sql_query2(natural_language_query: str) -> str:
|
| 48 |
-
"""Generate SQL query from natural language using LangChain."""
|
| 49 |
-
engine = get_db_engine()
|
| 50 |
-
db = SQLDatabase(engine)
|
| 51 |
|
| 52 |
-
# You can swap ChatOpenAI with LLaMA wrapper
|
| 53 |
-
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0, api_key=settings.OPENAI_API_KEY)
|
| 54 |
-
|
| 55 |
-
sql_chain = create_sql_query_chain(llm, db)
|
| 56 |
-
|
| 57 |
-
sql_query = sql_chain.invoke({"question": natural_language_query})
|
| 58 |
-
return sql_query
|
| 59 |
|
| 60 |
|
|
|
|
| 6 |
from app.db.schema_reader import get_schema
|
| 7 |
from app.db.db_connector import get_db_engine
|
| 8 |
from app.db.db_connector import get_connection
|
| 9 |
+
import re
|
| 10 |
|
| 11 |
+
# llm = ChatOpenAI(model="gpt-4o-mini", temperature=0, api_key=settings.OPENAI_API_KEY)
|
| 12 |
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0, api_key=settings.OPENAI_API_KEY)
|
| 13 |
|
| 14 |
+
def detect_foreign_key_relationships_only(schema):
|
| 15 |
+
"""Only detect foreign key relationships without any assumptions about data types"""
|
| 16 |
+
relationships = []
|
| 17 |
+
|
| 18 |
+
for table_name, columns in schema.items():
|
| 19 |
+
for column in columns:
|
| 20 |
+
# Look for foreign key patterns: *_id columns
|
| 21 |
+
if column.endswith('_id') and column != 'id':
|
| 22 |
+
# Extract potential referenced table name
|
| 23 |
+
ref_name = column[:-3] # Remove '_id'
|
| 24 |
+
|
| 25 |
+
# Check for exact match or plural/singular variations
|
| 26 |
+
possible_refs = [ref_name, ref_name + 's', ref_name[:-1] if ref_name.endswith('s') else ref_name + 's']
|
| 27 |
+
|
| 28 |
+
for possible_ref in possible_refs:
|
| 29 |
+
if possible_ref in schema:
|
| 30 |
+
relationships.append({
|
| 31 |
+
'from_table': table_name,
|
| 32 |
+
'from_column': column,
|
| 33 |
+
'to_table': possible_ref,
|
| 34 |
+
'to_column': 'id'
|
| 35 |
+
})
|
| 36 |
+
break
|
| 37 |
+
|
| 38 |
+
return relationships
|
| 39 |
+
|
| 40 |
+
def generate_pure_schema_context(schema):
|
| 41 |
+
"""Generate schema context without any assumptions - let AI figure out everything"""
|
| 42 |
+
|
| 43 |
+
# Just provide the raw schema structure
|
| 44 |
+
context = "DATABASE SCHEMA:\n\n"
|
| 45 |
+
|
| 46 |
+
# List all tables and their columns
|
| 47 |
+
context += "TABLES AND COLUMNS:\n"
|
| 48 |
+
for table_name, columns in schema.items():
|
| 49 |
+
context += f"- {table_name}: {', '.join(columns)}\n"
|
| 50 |
+
|
| 51 |
+
# Detect only foreign key relationships (no assumptions about data types)
|
| 52 |
+
relationships = detect_foreign_key_relationships_only(schema)
|
| 53 |
+
|
| 54 |
+
# Show detected relationships
|
| 55 |
+
if relationships:
|
| 56 |
+
context += "\nDETECTED FOREIGN KEY RELATIONSHIPS:\n"
|
| 57 |
+
for rel in relationships:
|
| 58 |
+
context += f"- {rel['from_table']}.{rel['from_column']} → {rel['to_table']}.{rel['to_column']}\n"
|
| 59 |
+
|
| 60 |
+
return context
|
| 61 |
+
|
| 62 |
|
| 63 |
def generate_sql_query(user_question: str):
|
| 64 |
schema = get_schema()
|
| 65 |
+
|
| 66 |
+
# Generate pure schema context - no assumptions, let AI figure it out
|
| 67 |
+
schema_context = generate_pure_schema_context(schema)
|
| 68 |
|
| 69 |
prompt = ChatPromptTemplate.from_template("""
|
| 70 |
+
You are an expert SQLite query generator with advanced analytical capabilities.
|
| 71 |
+
|
| 72 |
+
{schema_context}
|
| 73 |
+
|
| 74 |
+
INTELLIGENT ANALYSIS INSTRUCTIONS:
|
| 75 |
+
1. EXAMINE the schema above - look at table names and column names to understand what data each table contains
|
| 76 |
+
2. ANALYZE the column names to infer what type of information is stored (names, addresses, amounts, dates, etc.)
|
| 77 |
+
3. USE the detected foreign key relationships to understand how tables connect
|
| 78 |
+
4. For queries needing data from multiple tables, follow the relationship paths and use JOINs
|
| 79 |
+
5. INFER from context - if user asks about location and you see city/address columns, use those tables
|
| 80 |
+
6. THINK logically about which tables would contain the requested information
|
| 81 |
+
|
| 82 |
+
QUERY GENERATION STRATEGY:
|
| 83 |
+
- Read the user's question carefully
|
| 84 |
+
- Identify what data they want (customers, transactions, locations, etc.)
|
| 85 |
+
- Look at the schema to find which tables likely contain that data
|
| 86 |
+
- Check if multiple tables are needed and use the foreign key relationships
|
| 87 |
+
- Generate appropriate SQL with JOINs if data spans multiple tables
|
| 88 |
+
|
| 89 |
+
RESPONSE FORMAT:
|
| 90 |
+
- Return ONLY valid SQLite syntax
|
| 91 |
+
- No explanations, comments, or extra text
|
| 92 |
+
- Use proper JOIN syntax when needed
|
| 93 |
+
- If you cannot determine how to query the available schema, respond: "I cannot generate this query."
|
| 94 |
+
|
| 95 |
+
Example thinking process:
|
| 96 |
+
- User asks "customers in New York" → Look for tables with customer info AND location info → Use relationships to JOIN them
|
| 97 |
+
- User asks "transactions over $1000" → Look for tables with transaction/amount data
|
| 98 |
+
- User asks "branch managers" → Look for tables with employee/staff info and management roles
|
| 99 |
|
| 100 |
User question: {question}
|
| 101 |
""")
|
| 102 |
|
| 103 |
+
messages = prompt.format_messages(schema_context=schema_context, question=user_question)
|
| 104 |
sql_query = llm.invoke(messages).content.strip()
|
| 105 |
|
| 106 |
# If the LLM says it cannot generate a query, treat as normal chat
|
| 107 |
if sql_query.lower().startswith("i cannot generate this query"):
|
| 108 |
# Use the LLM as a chatbot for normal conversation
|
| 109 |
+
chat_prompt = (
|
| 110 |
+
"You are an AI assistant whose primary role is to generate SQL queries from natural language questions. "
|
| 111 |
+
"If the user's question is not related to SQL or databases, respond conversationally as a helpful assistant."
|
| 112 |
+
f"\n\nUser question: {user_question}"
|
| 113 |
+
)
|
| 114 |
chat_response = llm.invoke([{"role": "user", "content": chat_prompt}]).content.strip()
|
| 115 |
return {"chat_message": chat_response}
|
| 116 |
|
| 117 |
return sql_query
|
| 118 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 119 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 120 |
|
| 121 |
|
backend/app/services/utility.py
CHANGED
|
@@ -1,11 +1,10 @@
|
|
| 1 |
-
import dropbox
|
| 2 |
import openai
|
| 3 |
from config.settings import settings
|
| 4 |
from langchain_openai import ChatOpenAI
|
| 5 |
import sqlparse
|
| 6 |
import re
|
| 7 |
import os
|
| 8 |
-
import tempfile
|
| 9 |
class UtilityClass:
|
| 10 |
"""
|
| 11 |
Classifies user input as 'sql' (SQL/data context) or 'chat' (normal conversation) using an LLM.
|
|
@@ -52,35 +51,12 @@ class UtilityClass:
|
|
| 52 |
heading = llm.invoke([{"role": "user", "content": heading_prompt}]).content.strip()
|
| 53 |
return {"heading": heading, "records": rows}
|
| 54 |
|
| 55 |
-
@staticmethod
|
| 56 |
-
def is_valid_sql_query33(sql: str) -> bool:
|
| 57 |
-
"""
|
| 58 |
-
Returns True if the string is a valid SQL statement (not just a keyword in text).
|
| 59 |
-
Uses sqlparse to check for a valid statement structure.
|
| 60 |
-
Accepts queries starting with 'sql ' followed by a valid SQL statement.
|
| 61 |
-
"""
|
| 62 |
-
if not sql or not isinstance(sql, str):
|
| 63 |
-
return False
|
| 64 |
-
sql_strip = sql.strip()
|
| 65 |
-
# Remove leading 'sql' if present
|
| 66 |
-
if sql_strip.lower().startswith('sql'):
|
| 67 |
-
sql_strip = sql_strip[4:].lstrip()
|
| 68 |
-
parsed = sqlparse.parse(sql_strip)
|
| 69 |
-
if not parsed or not parsed[0].tokens:
|
| 70 |
-
return False
|
| 71 |
-
stmt = parsed[0]
|
| 72 |
-
first_token = stmt.token_first(skip_cm=True, skip_ws=True)
|
| 73 |
-
if first_token is None:
|
| 74 |
-
return False
|
| 75 |
-
# Accept only if the first token is a SQL keyword and there is more than one token
|
| 76 |
-
return first_token.ttype in sqlparse.tokens.Keyword.DML and len(stmt.tokens) > 1
|
| 77 |
-
|
| 78 |
@staticmethod
|
| 79 |
def is_valid_sql_query(text: str):
|
| 80 |
if not text or not text.strip():
|
| 81 |
return False
|
| 82 |
|
| 83 |
-
# Remove markdown formatting like ```sql ... ```
|
| 84 |
text = re.sub(r"```sql|```", "", text, flags=re.IGNORECASE).strip()
|
| 85 |
|
| 86 |
# Collapse multiple spaces & line breaks into a single space
|
|
@@ -91,10 +67,7 @@ class UtilityClass:
|
|
| 91 |
r"^SELECT\s+.+\s+FROM\s+.+", # SELECT ... FROM ...
|
| 92 |
r"^INSERT\s+INTO\s+.+\s+VALUES\s*\(", # INSERT INTO ... VALUES (...)
|
| 93 |
r"^UPDATE\s+.+\s+SET\s+.+", # UPDATE ... SET ...
|
| 94 |
-
r"^DELETE\s+FROM\s+.+", # DELETE FROM ...
|
| 95 |
r"^CREATE\s+(TABLE|DATABASE)\s+.+", # CREATE TABLE/DATABASE ...
|
| 96 |
-
r"^DROP\s+(TABLE|DATABASE)\s+.+", # DROP TABLE/DATABASE ...
|
| 97 |
-
r"^ALTER\s+TABLE\s+.+", # ALTER TABLE ...
|
| 98 |
r"^WITH\s+.+\s+AS\s*\(.+\)" # WITH ... AS (...)
|
| 99 |
]
|
| 100 |
|
|
@@ -106,23 +79,81 @@ class UtilityClass:
|
|
| 106 |
return False
|
| 107 |
|
| 108 |
@staticmethod
|
| 109 |
-
def
|
| 110 |
"""
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
:
|
| 114 |
-
|
| 115 |
-
:return: Local file path of the downloaded DB.
|
| 116 |
"""
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# import dropbox
|
| 2 |
import openai
|
| 3 |
from config.settings import settings
|
| 4 |
from langchain_openai import ChatOpenAI
|
| 5 |
import sqlparse
|
| 6 |
import re
|
| 7 |
import os
|
|
|
|
| 8 |
class UtilityClass:
|
| 9 |
"""
|
| 10 |
Classifies user input as 'sql' (SQL/data context) or 'chat' (normal conversation) using an LLM.
|
|
|
|
| 51 |
heading = llm.invoke([{"role": "user", "content": heading_prompt}]).content.strip()
|
| 52 |
return {"heading": heading, "records": rows}
|
| 53 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 54 |
@staticmethod
|
| 55 |
def is_valid_sql_query(text: str):
|
| 56 |
if not text or not text.strip():
|
| 57 |
return False
|
| 58 |
|
| 59 |
+
# Remove markdown formatting like ```sql ... ```
|
| 60 |
text = re.sub(r"```sql|```", "", text, flags=re.IGNORECASE).strip()
|
| 61 |
|
| 62 |
# Collapse multiple spaces & line breaks into a single space
|
|
|
|
| 67 |
r"^SELECT\s+.+\s+FROM\s+.+", # SELECT ... FROM ...
|
| 68 |
r"^INSERT\s+INTO\s+.+\s+VALUES\s*\(", # INSERT INTO ... VALUES (...)
|
| 69 |
r"^UPDATE\s+.+\s+SET\s+.+", # UPDATE ... SET ...
|
|
|
|
| 70 |
r"^CREATE\s+(TABLE|DATABASE)\s+.+", # CREATE TABLE/DATABASE ...
|
|
|
|
|
|
|
| 71 |
r"^WITH\s+.+\s+AS\s*\(.+\)" # WITH ... AS (...)
|
| 72 |
]
|
| 73 |
|
|
|
|
| 79 |
return False
|
| 80 |
|
| 81 |
@staticmethod
|
| 82 |
+
def get_health_status() -> dict:
|
| 83 |
"""
|
| 84 |
+
Get the health status of the application and its dependencies.
|
| 85 |
+
|
| 86 |
+
Returns:
|
| 87 |
+
dict: Health status information including API status, database connectivity, and timestamp
|
|
|
|
| 88 |
"""
|
| 89 |
+
import time
|
| 90 |
+
from datetime import datetime
|
| 91 |
+
from app.db.db_connector import get_connection
|
| 92 |
+
|
| 93 |
+
health_data = {
|
| 94 |
+
"status": "healthy",
|
| 95 |
+
"timestamp": datetime.utcnow().isoformat(),
|
| 96 |
+
"version": "1.0.0",
|
| 97 |
+
"checks": {}
|
| 98 |
+
}
|
| 99 |
+
|
| 100 |
+
# Check database connectivity
|
| 101 |
+
try:
|
| 102 |
+
with get_connection() as conn:
|
| 103 |
+
cursor = conn.cursor()
|
| 104 |
+
cursor.execute("SELECT 1")
|
| 105 |
+
result = cursor.fetchone()
|
| 106 |
+
if result:
|
| 107 |
+
health_data["checks"]["database"] = {
|
| 108 |
+
"status": "healthy",
|
| 109 |
+
"message": "Database connection successful"
|
| 110 |
+
}
|
| 111 |
+
else:
|
| 112 |
+
health_data["checks"]["database"] = {
|
| 113 |
+
"status": "unhealthy",
|
| 114 |
+
"message": "Database query failed"
|
| 115 |
+
}
|
| 116 |
+
health_data["status"] = "unhealthy"
|
| 117 |
+
except Exception as e:
|
| 118 |
+
health_data["checks"]["database"] = {
|
| 119 |
+
"status": "unhealthy",
|
| 120 |
+
"message": f"Database connection failed: {str(e)}"
|
| 121 |
+
}
|
| 122 |
+
health_data["status"] = "unhealthy"
|
| 123 |
+
|
| 124 |
+
# Check OpenAI API (if configured)
|
| 125 |
+
try:
|
| 126 |
+
if hasattr(settings, 'OPENAI_API_KEY') and settings.OPENAI_API_KEY:
|
| 127 |
+
health_data["checks"]["openai_api"] = {
|
| 128 |
+
"status": "configured",
|
| 129 |
+
"message": "OpenAI API key is configured"
|
| 130 |
+
}
|
| 131 |
+
else:
|
| 132 |
+
health_data["checks"]["openai_api"] = {
|
| 133 |
+
"status": "warning",
|
| 134 |
+
"message": "OpenAI API key not configured"
|
| 135 |
+
}
|
| 136 |
+
except Exception as e:
|
| 137 |
+
health_data["checks"]["openai_api"] = {
|
| 138 |
+
"status": "error",
|
| 139 |
+
"message": f"OpenAI API check failed: {str(e)}"
|
| 140 |
+
}
|
| 141 |
+
|
| 142 |
+
return health_data
|
| 143 |
+
|
| 144 |
+
# @staticmethod
|
| 145 |
+
# def download_sqlite_db_from_dropbox(dropbox_token: str, dropbox_db_path: str, local_db_path: str = None) -> str:
|
| 146 |
+
# """
|
| 147 |
+
# Downloads a SQLite DB file from Dropbox and returns the local file path.
|
| 148 |
+
# :param dropbox_token: Dropbox API access token.
|
| 149 |
+
# :param dropbox_db_path: Path to the DB file in Dropbox (e.g., '/folder/mydb.db').
|
| 150 |
+
# :param local_db_path: Local path to save the file. If None, saves to './downloaded_db.db'.
|
| 151 |
+
# :return: Local file path of the downloaded DB.
|
| 152 |
+
# """
|
| 153 |
+
# if local_db_path is None:
|
| 154 |
+
# local_db_path = os.path.join(os.getcwd(), "downloaded_db.db")
|
| 155 |
+
# dbx = dropbox.Dropbox(dropbox_token)
|
| 156 |
+
# with open(local_db_path, 'wb') as f:
|
| 157 |
+
# metadata, res = dbx.files_download(path=dropbox_db_path)
|
| 158 |
+
# f.write(res.content)
|
| 159 |
+
# return local_db_path
|
backend/requirements.txt
CHANGED
|
@@ -12,4 +12,8 @@ faiss-cpu # optional for embeddings
|
|
| 12 |
pandas
|
| 13 |
matplotlib
|
| 14 |
python-dotenv
|
| 15 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12 |
pandas
|
| 13 |
matplotlib
|
| 14 |
python-dotenv
|
| 15 |
+
pytest==8.4.1
|
| 16 |
+
pytest-cov==6.2.1
|
| 17 |
+
pytest-mock==3.14.1
|
| 18 |
+
coverage==7.10.6
|
| 19 |
+
# dropbox
|