Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 17 additions & 7 deletions app/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
from sqlalchemy import text
from sqlalchemy import select
from app.datatables import datatables_response, init_app, query_columns
from app.nl_query import run_nl_query
from app.nl_query import generate_sql, generate_sql_modification
from werkzeug.middleware.proxy_fix import ProxyFix


Expand Down Expand Up @@ -222,14 +222,24 @@ def api_query():
@app.route("/api/nl_query", methods=["POST"])
def api_nl_query():
"""Translate a natural language question into SQL and execute it."""
question = request.form.get("query")
question = request.form.get("prompt")
starting_query = request.form.get("query")

if not question:
return {"error": "No query provided"}, 400
try:
return run_nl_query(db.session, question), 200
except Exception as e:
print(f"Error executing NL query: {e}")
return {"error": "Query execution failed"}, 500

if not starting_query:
try:
return generate_sql(question), 200
except Exception as e:
print(f"Error creating NL query: {e}")
return {"error": "Query generation failed"}, 500
else:
try:
return generate_sql_modification(question, starting_query), 200
Comment on lines +233 to +239

Copilot AI Aug 6, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The API returns inconsistent response formats. When successful, it returns just the SQL string, but on error it returns a dictionary with an 'error' key. Consider wrapping the successful response in a consistent format like {'query': generate_sql(question)}.

Copilot uses AI. Check for mistakes.
Comment on lines +233 to +239

Copilot AI Aug 6, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The API returns inconsistent response formats. When successful, it returns just the SQL string, but on error it returns a dictionary with an 'error' key. Consider wrapping the successful response in a consistent format like {'query': generate_sql_modification(question, starting_query)}.

Copilot uses AI. Check for mistakes.
except Exception as e:
print(f"Error creating NL query with starting query: {e}")
return {"error": "Query modification failed"}, 500


@app.route("/api", methods=["POST"])
Expand Down
104 changes: 85 additions & 19 deletions app/nl_query.py
Original file line number Diff line number Diff line change
@@ -1,28 +1,94 @@
"""Utility functions for natural language to SQL queries."""
from typing_extensions import Annotated, TypedDict
from langchain import hub
from langgraph.graph import START, StateGraph
from langchain_openai import ChatOpenAI
from sqlalchemy.dialects import sqlite
from sqlalchemy.schema import CreateTable

from typing import Dict, List
# Import SQLAlchemy models installed via requirements
from marc_db.models import Base

from sqlalchemy import text
from sqlalchemy.orm import Session

class QueryOutput(TypedDict):
"""Generated SQL query."""

def _to_sql(question: str) -> str:
"""Naively convert a natural language question to SQL.
query: Annotated[str, ..., "Syntactically valid SQL query."]

This is a placeholder implementation meant to demonstrate how the
translation could be structured. Only a very small set of questions is
supported.
"""

q = question.lower()
if "how many isolates" in q:
return "SELECT COUNT(*) AS count FROM isolates"
raise ValueError("Unsupported question")
class State(TypedDict):
question: str
query: str


def run_nl_query(session: Session, question: str) -> Dict[str, List[Dict[str, int]]]:
"""Execute a natural language query against the database."""
# Build schema from SQLAlchemy models
SCHEMA = "\n\n".join(
str(CreateTable(table).compile(dialect=sqlite.dialect()))
for table in Base.metadata.sorted_tables
)

sql = _to_sql(question)
result = session.execute(text(sql)).fetchall()
return {"sql": sql, "result": [dict(row._mapping) for row in result]}
llm = ChatOpenAI(

Copilot AI Aug 6, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The OpenAI API key should be configured through environment variables or secure configuration rather than relying on default authentication. Consider explicitly setting the api_key parameter or ensuring OPENAI_API_KEY environment variable is properly configured.

Copilot uses AI. Check for mistakes.
model="gpt-4o",
temperature=0,
max_tokens=None,
timeout=None,
max_retries=2,
)
query_prompt_template = hub.pull("langchain-ai/sql-query-system-prompt")


def write_query(state: State) -> State:
prompt = query_prompt_template.invoke(
{
"dialect": "sqlite",
"top_k": 10,
"table_info": SCHEMA,
"input": state["question"],
}
)
structured_llm = llm.with_structured_output(QueryOutput)
result = structured_llm.invoke(prompt)
return {"query": result["query"]}


def modify_query(state: State) -> State:
prompt = query_prompt_template.invoke(
{
"dialect": "sqlite",
"top_k": 10,
"table_info": SCHEMA,
"input": state["question"],
"query": state["query"],
}
)
structured_llm = llm.with_structured_output(QueryOutput)
result = structured_llm.invoke(prompt)
return {"query": result["query"]}


def generate_sql(question: str) -> str:
graph_builder = StateGraph(State)
graph_builder.add_node("write_query", write_query)
graph_builder.add_edge(START, "write_query")
graph = graph_builder.compile()
result = graph.invoke({"question": question})
return result["query"]


def generate_sql_modification(question: str, starting_query: str) -> str:
graph_builder = StateGraph(State)
graph_builder.add_node("modify_query", write_query)

Copilot AI Aug 6, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The modify_query node is incorrectly calling write_query function instead of modify_query function. This should be modify_query to properly handle query modifications.

Suggested change
graph_builder.add_node("modify_query", write_query)
graph_builder.add_node("modify_query", modify_query)

Copilot uses AI. Check for mistakes.
graph_builder.add_edge(START, "modify_query")
graph = graph_builder.compile()
result = graph.invoke({"question": question, "query": starting_query})
return result["query"]


if __name__ == "__main__":
import argparse

parser = argparse.ArgumentParser(
description="Generate an SQL query for a natural language question."
)
parser.add_argument("question", help="User question to answer with SQL.")
args = parser.parse_args()
print(generate_sql(args.question))
3 changes: 3 additions & 0 deletions app/static/query_editor.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,15 @@ function initQueryEditor(containerId, hiddenInputId, tables) {
keywords.push(f);
}
}
console.log("Keywords:", keywords);
console.log("Tables:", tables);
Comment on lines +10 to +11

Copilot AI Aug 6, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Debug console.log statements should be removed from production code as they can clutter the browser console and potentially expose sensitive information.

Suggested change
console.log("Keywords:", keywords);
console.log("Tables:", tables);

Copilot uses AI. Check for mistakes.
Comment on lines +10 to +11

Copilot AI Aug 6, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Debug console.log statements should be removed from production code as they can clutter the browser console and potentially expose sensitive information.

Suggested change
console.log("Keywords:", keywords);
console.log("Tables:", tables);

Copilot uses AI. Check for mistakes.
const editor = CodeMirror(document.getElementById(containerId), {
value: input.value || "",
mode: "text/x-sql",
lineNumbers: true,
extraKeys: { "Ctrl-Space": "autocomplete" },
hintOptions: { tables: tables, keywords: keywords },
lineWrapping: true,
});

editor.on("inputRead", function (cm, change) {
Expand Down
25 changes: 19 additions & 6 deletions app/templates/query.html
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,25 @@
{% block body %}
<div class="container py-2">
<div class="d-flex flex-row">
<form class="d-flex flex-column w-100" method="POST" action="{{ url_for('query') }}">
<label class="p-2">Custom Query</label>
<input type="hidden" id="query-input" name="query" value="{{ query }}">
<div id="query-editor" class="w-100 border" style="height: 300px;"></div>
<button type="submit" id="submit" class="my-2 p-2">Submit</button>
</form>
<div class="d-flex flex-column">
<form class="d-flex flex-column w-100" method="POST" action="{{ url_for('query') }}">
<label class="p-2">Custom Query</label>
<input type="hidden" id="query-input" name="query" value="{{ query }}">
<div id="query-editor" class="w-100 border" style="height: 300px; max-width: 300;"></div>

Copilot AI Aug 6, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The max-width CSS property is missing a unit (px, %, em, etc.). It should be 'max-width: 300px;' or another appropriate unit.

Suggested change
<div id="query-editor" class="w-100 border" style="height: 300px; max-width: 300;"></div>
<div id="query-editor" class="w-100 border" style="height: 300px; max-width: 300px;"></div>

Copilot uses AI. Check for mistakes.
<button type="submit" id="submit" class="my-2 p-2">Submit</button>
</form>

<form class="d-flex flex-column w-50 p-5 m-5" method="POST" action="{{ url_for('api_nl_query') }}">
<label for="prompt" class="form-label">
Natural Language Query
<i class="bi bi-question-circle" tabindex="0" data-bs-toggle="popover"
data-bs-content="Ask a question about the data. The system will generate an SQL query to answer it."></i>
</label>
<textarea id="prompt" name="prompt" class="form-control mb-3" rows="3">{{ prompt }}</textarea>
<input type="hidden" name="query" value="{{ query }}">
<button type="submit" class="btn btn-primary">Submit</button>
</form>
</div>

<div class="d-flex flex-column w-50 p-5 m-5">
<label for="model-select" class="form-label">
Expand Down
Loading