Skip to content

anshk8/MindBridgeCompetition2026

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

91 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

MindBridge Γ— Carleton SQL Query Writer Agent (2026)

Author: Ansh Kakkar
Due Date: March 13, 2026

UPDATE MARCH 31, 2026:

  • 1st Place Winner for Macbook Pro Prize

Table of Contents

  1. How to Run
  2. High Level Architecture Overview
  3. Why My Solution Stands Out
  4. Handling Ambiguous & Irrelevant Queries
  5. Agent Design + Techniques
  6. Enabling Multi Conversational Mode
  7. Code Organization
  8. Challenges Faced
  9. About Me

0. How to Run

Shared Ollama client note: The project uses a shared Ollama client instance defined in src/utils/ollamaClient.py.
It reads OLLAMA_MODEL, OLLAMA_REACT_MODEL, OLLAMA_HOST, and OLLAMA_API_KEY from the .env file, so you do not need to construct separate Ollama clients in each agent.

1. Create and activate a virtual environment

python -m venv venv
source venv/bin/activate        # macOS / Linux
# venv\Scripts\activate         # Windows

2. Install dependencies

pip install -r requirements.txt

3. Install and run Ollama (local inference)

Locally:

ollama pull qwen3:8b  # SQL generation + validation
ollama pull llama3.2:latest        # ReAct tool-use loop

Carleton Server:

Create a .env file in the project root (copy from .env.example) and fill in your credentials:

# .env

# Models
OLLAMA_MODEL=qwen3:8b
OLLAMA_REACT_MODEL=llama3.2:latest

# Carleton LLM server
OLLAMA_HOST=<carleton_server_URL>
OLLAMA_API_KEY=<rcs_carleton_API_key>

OLLAMA_HOST defaults to http://localhost:11434 if not set. OLLAMA_API_KEY is only required for the Carleton server β€” leave it unset for a local Ollama instance.

4. Run the interactive agent

python main.py

Note: By default, ambiguous queries return a short hint and the pipeline moves on β€” this is intentional so automated evaluation never hangs on input(). To enable the full interactive clarification loop for ambiguous queries (ask user β†’ reframe β†’ regenerate), see Enabling Multi Conversational Mode.


1. High Level Architecture Overview

My submission uses a two-stage agentic pipeline orchestrated by LangGraph:

  • SQLAgent (generateSqlNode) β€” SQL Generation master. Classifies query intent (Clear / Ambiguous / Irrelevant), uses dynamic few-shot retrieval via semantic similarity to find the most relevant examples, uses step by step Chain-of-Thought reasoning and a ReAct loop using schema-probing tools to verify database information before producing the final SQL.

  • K-Candidate Generation & Validation (kCandidatesNode) β€” Generates multiple SQL candidates at varied temperatures (0.7, 0.3, 1.1) and returns the first one passing execution and semantic validation. ONLY THREE temperatures because we want generation time <= 5 minutes. Having more than 3 temperatures would exceed a reasonable time limit.

  • ValidatorAgent β€” Ensures the final SQL is executable and semantically correct by executing, repairing failures (≀2 rounds), and validating results match user intent.

Full Workflow

                                   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                                   β”‚   User Question      β”‚
                                   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                                              β”‚
                                              v
                    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                    β”‚         generateSqlNode: SQLAgent            │◄─────────────┐
                    β”‚         (ReAct Tool-Use Loop)                β”‚              β”‚
                    β”‚                                              β”‚              β”‚
                    β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚              β”‚
                    β”‚  β”‚  Tool-use loop (max 2 rounds):         β”‚  β”‚              β”‚
                    β”‚  β”‚   β€’ search_value(term)                 β”‚  β”‚              β”‚
                    β”‚  β”‚   β€’ get_distinct_values(table, col)    β”‚  β”‚              β”‚
                    β”‚  β”‚   β€’ get_columns(table)                 β”‚  β”‚              β”‚
                    β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚              β”‚
                    β”‚                                              β”‚              β”‚
                    β”‚  Output: SQL + Intent Classification         β”‚              β”‚
                    β”‚  Intents: Clear / Ambiguous / Irrelevant     β”‚              β”‚
                    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜              β”‚
                                        β”‚                                         β”‚
          β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”      β”‚
          β”‚                             β”‚                                  β”‚      β”‚
          v                             v                                  v      β”‚
     Irrelevant                    Ambiguous                             Clear    β”‚
          β”‚                             β”‚                                  β”‚      β”‚
          v                  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                      v      β”‚
     Return                  β”‚                      β”‚                  kCandidatesNode
   Blank / EXIT    multi-Conversational=OFF  multi-Conversational=ON   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                             β”‚                      β”‚                  β”‚ Temp 0.7 β†’ Val?  β”‚
                             v                      v                  β”‚  Exit on pass    β”‚
                        Return Hint        β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”         β”‚ Temp 0.3 β†’ Val?  β”‚
                     Comment / Exit        β”‚clarificationNodeβ”‚         β”‚ Temp 1.1 β†’ Val?  β”‚
                                           β”‚  Ask User for   β”‚         β”‚  ...retry...     β”‚
                                           β”‚  Input, Reframe β”‚         β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                                           β”‚  Question       β”‚                  β”‚
                                           β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜                  v
                                                    β”‚              β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                                                    β”‚              β”‚    ValidatorAgent     β”‚
                                                    β”‚              β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚
                                                    └──────────────►  β”‚  Execute SQL    β”‚  β”‚
                                              (re-runs SQLAgent)   β”‚  β”‚  Repair (≀2x)   β”‚  β”‚
                                                                   β”‚  β”‚  Semantic check β”‚  β”‚
                                                                   β”‚  β”‚  Fix output     β”‚  β”‚
                                                                   β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚
                                                                   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                                                                              β”‚
                                                                           Pass?
                                                                        β”Œβ”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”
                                                                        β”‚            β”‚
                                                                       Yes           No
                                                                        β”‚            β”‚
                                                                        v            v
                                                                    Return       Retry or
                                                                    Final SQL      Fail

2. Why My Solution Stands Out

  • Clean Code Organization β€” clearly separated folders for agents, graph, schemas, utils, and testing. (See Code Organization)

  • Modern LangGraph Workflow β€” entire pipeline with a shared state containing conditional edges, not a chain of if-statements.

  • SQLAgent Techniques β€” ReAct Tool-Use Loop, Chain-of-Thought Prompting, Dynamic Few-Shot Learning, Schema Grounding. (See SQLAgent (Generator))

  • ValidatorAgent β€” two-phase execution + semantic review with self-correction loops. (See ValidatorAgent (Verifier + Fixer))

  • K-Candidate Generation with Temperature Diversity β€” Generates candidates at varied temperatures and exits as soon as the first one passes execution and semantic review. Easy and medium queries almost always succeed on the first attempt (temperature 0.7) and cost exactly one generation. Hard queries benefit from temperature diversity and retry resilience when the first attempt fails.

  • Graceful Query Handling β€” Handle all sorts of user queries, ambiguous and irrelevant queries are detected and handled without crashing the pipeline. (See Handling Ambiguous & Irrelevant Queries)

  • Multi-Conversational Mode β€” (OPTIONAL) Can be turned on and off to prevent interference with automated testing. Allows interactive clarification for ambiguous queries by pausing to ask the user for details, then reframing their answer into a clean, unambiguous question (see Enabling Multi Conversational Mode ) for how to enable).


3. Handling Ambiguous & Irrelevant Queries

Ambiguous Query Handling

If Multi Conversational is enabled (see Enabling Multi Conversational Mode ), the pipeline detects vague terms and pauses to ask the user a clarification question. The user's answer is fed back to an LLM which rewrites the original question into a clean, unambiguous form before re-running SQL generation. See the example below:

Enter your question: Show me the best products

🎯 Intent: Ambiguous
πŸ€” What do you mean by 'best' β€” the most expensive products,
   highest revenue, or something else?
Your answer: highest revenue

πŸ”„ Reframed question: Show me the products with the highest revenue
                      in the bike store database.

Generated SQL:
   SELECT p.product_name,
          SUM(oi.quantity * oi.list_price * (1 - oi.discount)) AS total_revenue
   FROM products p
   JOIN order_items oi ON p.product_id = oi.product_id
   GROUP BY p.product_id, p.product_name
   ORDER BY total_revenue DESC

When Multi-Conversational is disabled (e.g. automated evaluation), the pipeline exits cleanly with a hint instead of blocking on input():

Enter your question: Show me the best products

❓ Query was ambiguous β€” try being more specific.
   Hint: What do you mean by 'best' β€” highest revenue, most orders, or something else?

Enter your question: 

Irrelevant Query Detection

Queries that have no connection to a bike store database are identified and skipped before any SQL is generated or validated.

Enter your question: Why am I feeling sick in the store?

  Query has no relevance to the bike store database.

Enter your question: 

Safe Fallback for All Edge Cases

For every edge case such as ambiguous (when Multi Conversational is off), irrelevant, or unanswerablem, generate_query in agent.py intercepts any -- AMBIGUOUS_QUERY, -- IRRELEVANT_QUERY, or -- UNANSWERABLE_QUERY marker and replaces it with:

SELECT 1 WHERE 1=0

This guarantees that all questions always receive a valid, executable SQL statement that returns zero rows for no execution errors, no crashes, and no output that can be misinterpreted.


4. Agent Design + Techniques

This system is built as a multi-agent architecture where each agent has a clearly defined responsibility. The design separates generation, validation, and orchestration to improve reliability and correctness.

1. SQLAgent (Generator)

Role: Converts natural language questions into SQL queries.

Models:

  • qwen3:8b β€” For SQL generation. A general Qwen3 variant which is great at reasoning. I selected this for its balance of speed and accuracy on SQL generation tasks at 8B parameters. I originally wanted to use qwen3:32b, but it was too slow and exceeded 5 minutes for a single generation. qwen3:8b was accurate on my testing and SIGNIFICANTLY faster.
  • llama3.2:latest β€” ReAct tool-use loop. Used separately to keep the tool-use rounds lightweight and fast. llama3.2 handles tool decisions quickly without the overhead of a full coder model call per ReAct round.
  • all-MiniLM-L6-v2 β€” Sentence embedding for dynamic few-shot retrieval (via sentence-transformers).

Techniques Used:

Chain-of-Thought (CoT) Prompting

Encourages step-by-step reasoning (tables β†’ joins β†’ filters β†’ aggregations) before producing SQL. Follows clear steps in the system prompt.

utils/prompts.py

      def buildSystemPrompt() -> str:

         ...
   
        REASONING PROCESS:
        You must think through each query step-by-step using Chain-of-Thought reasoning:
         
        Step 2: What tables are needed?
        Step 3: What columns should be selected?
        Step 4: Are any JOINs needed?
        Step 5: Are any WHERE filters needed?
        Step 6: Are any aggregations needed (COUNT, SUM, AVG, etc.)?
        Step 7: Is GROUP BY needed?
        Step 8: Is ORDER BY needed?
        Step 9: Is a LIMIT needed?
      

NOTE: Step 1 is a part of the intent clarification written above these steps, see src/utils/prompts.py

Dynamic Few-Shot Learning

Embedding model: all-MiniLM-L6-v2 (via sentence-transformers) β€” lightweight, fast, and accurate enough for semantic query matching.

Injects relevant example queries per question to assist LLM. Uses cosine similarity over sentence embeddings to select the top 5 most relevant examples from a curated bank of 20+ patterns covering basic aggregations, multi-table JOINs, self-joins, subqueries, CTEs, window functions, top-per-group problems, and common pitfalls with explicit counter-examples.

src/utils/fewShotExamples.py

FEW_SHOT_EXAMPLES = [
    FewShotExample(
        question="How many products are in each category?",
        sql="SELECT c.category_name, COUNT(p.product_id) FROM categories c LEFT JOIN products p ON c.category_id = p.category_id GROUP BY c.category_name",
        explanation="JOIN with GROUP BY aggregation"
    ),
    FewShotExample(
        question="For each store, show the most expensive product in stock",
        sql="SELECT s.store_name, p.product_name, p.list_price FROM stores s JOIN stocks st ON s.store_id = st.store_id JOIN products p ON st.product_id = p.product_id WHERE (s.store_id, p.list_price) IN (SELECT st2.store_id, MAX(p2.list_price) FROM stocks st2 JOIN products p2 ON st2.product_id = p2.product_id GROUP BY st2.store_id)",
        explanation="Top-1 per group pattern: use subquery to find MAX per group."
    ),
    FewShotExample(
        question="Which staff members manage other staff?",
        sql="SELECT m.first_name, m.last_name, COUNT(s.staff_id) FROM staffs m JOIN staffs s ON m.staff_id = CAST(s.manager_id AS BIGINT) GROUP BY m.staff_id, m.first_name, m.last_name",
        explanation="Self-join hierarchy pattern: alias table twice and join ON manager_id."
    ),
    # ... 20 more patterns
]

ReAct Tool-Use Loop (Reasoning + Acting)

Model: llama3.2:latest β€” chosen for its native function-calling support, which makes tool usage reliable and deterministic.

Instead of hoping the LLM remembers every column name and value correctly, the SQLAgent wraps its generation in a ReAct loop where the model can reason about the question, decide it needs to verify something, call a tool, get real data back and enrich its information before generating the SQL.

The loop runs up to 2 tool rounds before requiring a final answer. If the LLM decides it doesn't need any tools, it skips straight to SQL generation, so we can reduce overhead for simple queries.

Three read only database tools are available:

Tool What it does When the model uses it
get_distinct_values(table, column) Returns up to 20 distinct values from a column Verifying exact string casing for WHERE filters (e.g. is it 'Trek' or 'trek'?)
search_value(term) Fuzzy-searches all VARCHAR columns across all tables Finding which table/column contains a value the user mentioned
get_columns(table) Returns all column names and types for a table Confirming exact column names before SELECT or WHERE
Example USAGE: User asks "Show me orders with Electra bikes"

ReAct Round 0:
  LLM thinks: "User mentioned 'Electra' β€” I should verify where this value lives."
  Tool call:  {"action": "tool_call", "tool": "search_value", "term": "Electra"}
  Result:     brands.brand_name: ['Electra']
              products.product_name: ['Electra Townie Original 21D - 2016', ...]

# ... Now the call for SQL Generation after ReAct loop has more context!

If all tool rounds complete without the model calling a tool, the agent proceeds directly to the final structured call with a strict Pydantic schema constraint (format=SQLResult.model_json_schema()) β€” so it never crashes.

src/agents/tools/tools.py        # Tool implementations (get_distinct_values, search_value, get_columns)
src/agents/tools/toolHelpers.py  # getTools() schema definitions + executeTool() dispatcher
src/agents/SQLAgent.py           # ReAct loop in generate()
src/utils/prompts.py             # System + user prompt builders

Schema Grounding

Provides context to the LLM, including the table names and actual sample column data, to reduce hallucinations.

utils/prompts.py
   #When calling chat completion we build a prompt that combines Schema Context which includes row examples along with our FewShotExamples.
   def buildUserPrompt(question: str, schemaContext: str, fewShotContext: str) -> str:

2. ValidatorAgent (Verifier + Fixer)

Role:
Ensures that generated SQL is both executable and semantically correct before returning to the user.

Model: qwen3:8b β€” same model as SQL generation, reused here for execution repair and semantic review since it already has strong code reasoning and schema familiarity from the generation phase.

Techniques Used

Execution and Semantic Review

Unlike other validator agents, this agent combines TWO important aspects, execution and semantics. The agent is given context to perform both reviews as accurately as possible.

Self-Correction Loops (≀ 2 Rounds)

If execution fails, the agent performs 2 repair attempts. If execution works, the agent will obtain sample rows, the user's original query and perform a semantic review. If the agent determines that the SQL results do not match the user's query, it will try to fix it 2 times to match the user's request.


5. Enabling Multi Conversational Feature

Multi-Conversational Mode

By default, ambiguous queries return a hint comment (❓ Query was ambiguous β€” try being more specific.) so the pipeline never blocks on input() during automated evaluations. To enable the interactive clarification loop, set the flag in agent.py:

# agent.py β€” QueryWriter.__init__
self.multi_conversational_enabled = True   # ask user to clarify ambiguous queries

When enabled, the pipeline will pause on ambiguous queries, ask a targeted question, reframe the answer into a clean unambiguous question using an LLM call, and re-run generation.

Must be False during automated evaluation to avoid hanging on input().


6. Code Organization

Organized to be readable and scalable. I use Pydantic schemas to reduce errors and provide the LLM with a consistent output format. The LangGraph workflow, agents, utils (with helpers and prompts) and tests I used are all organized in their own respective folders.

carleton_competition_winter_2026/
β”‚
β”œβ”€β”€ agent.py                        # Competition interface (QueryWriter)
β”œβ”€β”€ main.py                         # Interactive CLI entry point
β”‚
└── src/
    β”œβ”€β”€ agents/
    β”‚   β”œβ”€β”€ SQLAgent.py             # SQL generation with CoT + Few-Shot + ReAct tool-use loop
    β”‚   β”œβ”€β”€ ValidatorAgent.py       # Execution + semantic review with self-correction
    β”‚   └── tools/
    β”‚       β”œβ”€β”€ tools.py            # get_distinct_values, search_value, get_columns
    β”‚       └── toolHelpers.py      # getTools() definitions + executeTool() to call tool
    β”‚
    β”œβ”€β”€ graph/
    β”‚   β”œβ”€β”€ GraphWorkflow.py        # LangGraph workflow definition & conditional edges
    β”‚   β”œβ”€β”€ Nodes.py                # Node functions (generateSqlNode, kCandidatesNode, ...)
    β”‚   β”œβ”€β”€ State.py                # Typed shared state schema
    |   |
    β”‚   └── visualization/
    β”‚       β”œβ”€β”€ visualize_graph.py  # Not submission relevant β€” visualize graph for fun
    β”‚       └── graph.png           # Generated LangGraph workflow visualization
    β”‚
    β”œβ”€β”€ schemas/                    # Pydantic output schemas for SQLAgent + ValidatorAgent
    β”‚
    β”œβ”€β”€ utils/
    β”‚   β”œβ”€β”€ ollamaClient.py         # Shared Ollama client instance (imported by all agents)
    β”‚   β”œβ”€β”€ helpers.py              # loadSchema, buildSchemaContext, executeSQL
    β”‚   β”œβ”€β”€ prompts.py              # System + user prompt builders
    β”‚   β”œβ”€β”€ fewShotExamples.py      # Bank of few-shot examples for SQLAgent
    β”‚   └── constants.py            # Constants
    β”‚
    └── testing/                    # I used an automated script to help improve my submission

Testing Folder

The src/testing folder contains the local evaluation method I used during development. There is a queriesToTest.py containing easy, medium, hard, ambiguous, and irrelevant prompts. The testAgentPipeline.py invokes my Langgraph workflow to mimic the evaluation environment for the competition. These tests help me iterate on validation accuracy and edge-case handling before my submission.


7. Challenges Faced

I went through lots of trial and error to solve this problem. Originally, I had 3 agents: a QuestionDecomposerAgent, a SchemaExpertAgent, and an SQLGeneration Agent. Although this type of architecture may seem advanced, it was inaccurate and error-prone. After some research, I realized that a multi-agent workflow of that format is not useful for this problem. A degree of error is carried over from each agent, and if the first agent misunderstood the question even slightly, the whole workflow is ruined. I also played around with a RAG (Retrieval Augmented Generation) setup to add more context, but for this dataset, the schema is small enough to fit directly in the prompt. RAG added extra complexity, latency and didn’t help enough to justify it.

After that, I pivoted to my more reliable setup one "MASTER" SQL agent that focuses on understanding the question and generating SQL supported by a separate ValidatorAgent that executes the query, fixes obvious failures, and semantic checks that the output actually matches the question. This approach produced much more accurate results and let me focus on tightening the system’s reliability (instead of debugging a long chain of agents).


8. About Me

Hi! I’m Ansh Kakkar, a 3rd-year Computer Science student at Carleton University. I love building things, joining hackathons and learn by making personal projects in my free time.


About

1st Place Winner (Macbook Pro Prize) for Agentic Text2SQL using Ollama for Mindbridge X Carleton University Challenge 2026.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages