Ask questions in plain English. Get answers from your documents and databases.
Smart RAG Engine is a dual-mode AI backend that lets you interact with both unstructured documents (PDFs, CSVs) and structured PostgreSQL databases using plain English — no SQL knowledge required, no manual document search.
It runs entirely on local AI models via Ollama, meaning your data never leaves your machine.
Two independent FastAPI services:
| Service | Entry Point | Purpose |
|---|---|---|
| Database Query API | main.py → port 8000 |
Natural language → SQL → PostgreSQL → summarized answer |
| Document RAG API | src/Rag-Agent/rag.py → port 8001 |
Upload PDF/CSV → vector index → semantic Q&A |
Traditional data interaction forces users to:
- Write SQL queries to interrogate databases
- Manually search through PDF documents
- Handle structured and unstructured data with completely separate tools
Smart RAG Engine provides a unified natural-language interface for both. You ask a question, the system figures out how to retrieve the answer.
graph TD
A[User Question] --> B{Which Service?}
B --> C[Database Query API<br/>main.py :8000]
B --> D[Document RAG API<br/>rag.py :8001]
C --> E[schema.py<br/>Table definitions]
E --> F[services.py<br/>LLM generates SQL]
F --> G[database.py<br/>psycopg2 executes SQL]
G --> H[PostgreSQL]
H --> I[Raw rows returned]
I --> J[services.py<br/>LLM summarizes results]
J --> K[Natural language answer]
D --> L[Upload PDF or CSV]
L --> M[PyPDF2 / pandas<br/>Text extraction]
M --> N[RecursiveCharacterTextSplitter<br/>chunk_size=800, overlap=100]
N --> O[OllamaEmbeddings<br/>nomic-embed-text]
O --> P[FAISS Vector Store<br/>saved to disk per session]
P --> Q[User asks question]
Q --> R[Similarity search<br/>retriever]
R --> S[RetrievalQA chain<br/>phi3:mini]
S --> T[Natural language answer]
Type a question in English. The LLM reads the database schema, generates a valid PostgreSQL query, executes it, and returns a human-readable summary — not raw rows.
Upload a PDF or CSV. The system extracts text, splits it into chunks, embeds them with nomic-embed-text, and stores them in a FAISS index. Ask questions and get answers grounded in the document content.
Both the LLM (phi3:mini) and the embedding model (nomic-embed-text) run locally via Ollama. No API keys, no cloud calls, no data leaving your machine.
The database API maintains per-session ConversationBufferMemory using cookies. The document API creates isolated FAISS vector stores per upload session identified by UUID.
Both services expose clean REST endpoints with Pydantic-validated request/response models and proper HTTP error handling.
| Layer | Technology | Version | Purpose |
|---|---|---|---|
| Language | Python | 3.8+ | Core runtime |
| API Framework | FastAPI | 0.117.1 | REST API layer |
| ASGI Server | Uvicorn | 0.37.0 | Async HTTP server |
| LLM Framework | LangChain | 0.3.27 | RAG + chain orchestration |
| LLM Runtime | Ollama | — | Local model inference |
| Primary LLM | phi3:mini | — | SQL generation + summarization |
| Embedding Model | nomic-embed-text | — | Document vector embeddings |
| Vector Store | FAISS | — | Similarity search + persistence |
| Database | PostgreSQL | — | Structured data storage |
| DB Driver | psycopg2 | 2.9.10 | PostgreSQL connection |
| PDF Parsing | PyPDF2 | — | Text extraction from PDFs |
| CSV Parsing | pandas | — | Tabular data to text |
| Data Validation | Pydantic | 2.11.9 | Request/response schemas |
| Session IDs | uuid | stdlib | Unique session identifiers |
Smart-Rag-Engine/
├── main.py # Database Query API (FastAPI, port 8000)
├── requirements.txt # Python dependencies
├── sample.txt # Example queries for all 4 database tables
├── LICENSE # MIT License
├── .gitignore # Ignores data/, vectorstores/, .env, __pycache__
│
├── src/
│ ├── config/
│ │ └── database.py # psycopg2 connection + run_sql_query()
│ ├── Schema/
│ │ └── schema.py # TABLE_SCHEMAS dict (4 tables, all columns)
│ ├── Services/
│ │ └── services.py # generate_sql_from_question() + generate_summary_from_results()
│ └── Rag-Agent/
│ └── rag.py # Document RAG API (FastAPI, port 8001)
│
├── CSV Files/ # Sample datasets for database import
│ ├── student.csv # Student academic records
│ ├── user.csv # User accounts with signup dates
│ ├── pizza.csv # Pizza restaurant reviews
│ └── products.pdf # Product catalog (PDF format)
│
├── data/ # Runtime: uploaded files stored here (gitignored)
└── vectorstores/ # Runtime: FAISS indexes stored here (gitignored)
- Python 3.8 or higher
- PostgreSQL (any recent version) running locally
- Ollama installed and running — ollama.ai
- Git
1. Clone the repository
git clone https://github.com/SadiqCodex/Smart-Rag-Engine.git
cd Smart-Rag-Engine2. Create and activate a virtual environment
Windows:
python -m venv venv
venv\Scripts\activateLinux / macOS:
python3 -m venv venv
source venv/bin/activate3. Install dependencies
pip install -r requirements.txtInstall Ollama from ollama.ai, then pull the two required models:
# Primary LLM — used for SQL generation and result summarization
ollama pull phi3:mini
# Embedding model — used for document vector search
ollama pull nomic-embed-text
# Start the Ollama server (if not already running)
ollama serveVerify both models are available:
ollama list1. Create the PostgreSQL database and tables
The application expects four tables. Import the sample data from CSV Files/:
-- students table
CREATE TABLE students (
id INTEGER,
name TEXT,
age INTEGER,
gender TEXT,
class INTEGER,
section TEXT,
maths INTEGER,
science INTEGER,
english INTEGER,
hindi INTEGER,
social_studies INTEGER,
grade TEXT,
phone TEXT,
email TEXT,
address TEXT
);
-- products table
CREATE TABLE products (
product_id INTEGER,
product_name TEXT,
category TEXT,
price NUMERIC,
stock_status TEXT
);
-- users table
CREATE TABLE users (
id INTEGER,
username TEXT,
email TEXT,
signup_date DATE,
status TEXT
);
-- pizza_reviews table
CREATE TABLE pizza_reviews (
id INTEGER,
title TEXT,
review TEXT,
rating NUMERIC,
date DATE
);2. Configure the database connection
Edit src/config/database.py with your credentials:
DB_PARAMS = {
"dbname": "your_database",
"user": "your_username",
"password": "your_password",
"host": "localhost",
"port": "5432"
}
⚠️ Never commit real credentials. Addsrc/config/database.pyto.gitignoreor use environment variables for production.
The two services run independently on different ports.
Database Query API (port 8000):
uvicorn main:app --reload --host 0.0.0.0 --port 8000Document RAG API (port 8001):
uvicorn src.Rag-Agent.rag:app --reload --host 0.0.0.0 --port 8001Or run the RAG service directly from its directory:
cd src/Rag-Agent
uvicorn rag:app --reload --port 8001Ask a natural-language question about the PostgreSQL database. The system generates SQL, executes it, and returns a summarized answer.
Request
{
"question": "How many active users are there?"
}Response
{
"answer": "There are 60 active users in the database."
}Error Response (SQL execution failure)
{
"error": "SQL execution error: column 'xyz' does not exist"
}Session state is maintained via a session_id cookie set automatically on the first request.
Upload a PDF or CSV file. Returns a session_id used for subsequent questions.
Request — multipart/form-data
file: [your .pdf or .csv file]
Response
{
"session_id": "351ae27c-0355-416f-9fcd-c5d24342f7b4",
"message": "PDF uploaded & processed successfully"
}Unsupported format response
{
"error": "Only PDF and CSV supported"
}Ask a question about a previously uploaded document.
Request — multipart/form-data
session_id: 351ae27c-0355-416f-9fcd-c5d24342f7b4
question: What products are listed in this catalog?
Response
{
"session_id": "351ae27c-0355-416f-9fcd-c5d24342f7b4",
"question": "What products are listed in this catalog?",
"answer": "The catalog includes electronics, clothing, and home goods..."
}Invalid session response
{
"error": "Invalid or expired session_id"
}Clears all active sessions and deletes all uploaded files and vector stores from disk.
Response
{
"message": "All sessions cleared"
}# Count students in class 10
curl -X POST "http://localhost:8000/ask" \
-H "Content-Type: application/json" \
-d '{"question": "How many students are in class 10?"}'
# Filter by score
curl -X POST "http://localhost:8000/ask" \
-H "Content-Type: application/json" \
-d '{"question": "Show students who scored more than 90 in maths"}'
# Product query
curl -X POST "http://localhost:8000/ask" \
-H "Content-Type: application/json" \
-d '{"question": "List all out-of-stock products"}'
# Review aggregation
curl -X POST "http://localhost:8000/ask" \
-H "Content-Type: application/json" \
-d '{"question": "Show pizza reviews with rating above 4"}'
# User query
curl -X POST "http://localhost:8000/ask" \
-H "Content-Type: application/json" \
-d '{"question": "How many users signed up in 2023?"}'# Step 1: Upload a file
curl -X POST "http://localhost:8001/upload/" \
-F "file=@CSV Files/student.csv"
# Step 2: Ask a question (use the session_id from the upload response)
curl -X POST "http://localhost:8001/ask/" \
-F "session_id=351ae27c-0355-416f-9fcd-c5d24342f7b4" \
-F "question=What grades do most students have?"import requests
BASE_DB = "http://localhost:8000"
BASE_RAG = "http://localhost:8001"
# --- Database query ---
def ask_database(question: str) -> str:
resp = requests.post(f"{BASE_DB}/ask", json={"question": question})
return resp.json().get("answer")
# --- Document RAG ---
def upload_document(file_path: str) -> str:
with open(file_path, "rb") as f:
resp = requests.post(f"{BASE_RAG}/upload/", files={"file": f})
return resp.json()["session_id"]
def ask_document(session_id: str, question: str) -> str:
resp = requests.post(f"{BASE_RAG}/ask/",
data={"session_id": session_id, "question": question})
return resp.json().get("answer")
# Usage
print(ask_database("List all female students in section A"))
sid = upload_document("CSV Files/pizza.csv")
print(ask_document(sid, "What are the most common complaints in the reviews?"))// Database query
async function askDatabase(question) {
const res = await fetch("http://localhost:8000/ask", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ question }),
credentials: "include", // sends session cookie
});
return res.json();
}
// Upload document
async function uploadDocument(file) {
const form = new FormData();
form.append("file", file);
const res = await fetch("http://localhost:8001/upload/", {
method: "POST",
body: form,
});
return res.json(); // { session_id, message }
}
// Ask about document
async function askDocument(sessionId, question) {
const form = new FormData();
form.append("session_id", sessionId);
form.append("question", question);
const res = await fetch("http://localhost:8001/ask/", {
method: "POST",
body: form,
});
return res.json();
}When you upload a document and ask a question, the system follows these steps:
Step 1 — Upload & Save
The file is saved to data/ with a UUID filename to avoid collisions.
Step 2 — Text Extraction
- PDF files →
PyPDF2.PdfReader, iterates all pages - CSV files →
pandas.read_csv(), converts the DataFrame to a string representation
Step 3 — Chunking
RecursiveCharacterTextSplitter splits the text into overlapping chunks:
chunk_size = 800characterschunk_overlap = 100characters
Each chunk becomes a langchain.docstore.document.Document object.
Step 4 — Embedding
OllamaEmbeddings(model="nomic-embed-text") converts each chunk into a dense vector using the locally running nomic-embed-text model.
Step 5 — Vector Store Creation
FAISS.from_documents() builds an in-memory index, then saves it to vectorstores/<session_id>.faiss/ for persistence across requests.
Step 6 — Retrieval
On each question, the FAISS index is loaded from disk, and vectorstore.as_retriever() performs similarity search to find the most relevant chunks.
Step 7 — Answer Generation
RetrievalQA.from_chain_type() with chain_type="stuff" passes the retrieved chunks as context to OllamaLLM(model="phi3:mini"), which generates the final answer.
Step 1 — Schema injection
schema.py defines TABLE_SCHEMAS, a dictionary of all four tables and their columns with types. This is formatted into a string and injected into the LLM prompt.
Step 2 — SQL generation
generate_sql_from_question() in services.py sends a structured prompt to phi3:mini instructing it to return only a valid SQL query — no explanation, no markdown, just SQL.
Step 3 — SQL cleaning
The raw LLM output is cleaned with a regex split on ; to extract the first complete statement.
Step 4 — Execution
run_sql_query() in database.py opens a psycopg2 connection using RealDictCursor, executes the SQL, and returns results as a list of dictionaries.
Step 5 — Serialization
Decimal and datetime types are serialized to JSON-safe formats before being stored in memory or passed to the summarizer.
Step 6 — Summarization
generate_summary_from_results() sends the raw result rows to phi3:mini with a prompt asking for a concise summary (≤100 words).
Step 7 — Session memory
ConversationBufferMemory stores the question/answer pair per session, keyed by a UUID stored in a browser cookie.
The CSV Files/ directory contains sample data ready to import into PostgreSQL:
| File | Table | Description |
|---|---|---|
student.csv |
students |
~100 student records with academic scores across 5 subjects, grades, and contact info |
user.csv |
users |
100 user accounts with signup dates (2023) and active/inactive status |
pizza.csv |
pizza_reviews |
200 pizza restaurant reviews with ratings (1–5) and review text |
products.pdf |
products |
Product catalog in PDF format (also usable with the Document RAG API) |
These files can also be uploaded directly to the Document RAG API for semantic Q&A without a database.
Database API sessions (main.py):
- A UUID
session_idis generated on the first request if no cookie is present ConversationBufferMemoryis stored in an in-memoryconversationsdict keyed bysession_id- The
session_idis set as an HTTP cookie in the response - Sessions persist only for the lifetime of the running process
Document RAG sessions (rag.py):
- A UUID
session_idis generated at upload time - The FAISS vector store is saved to
vectorstores/<session_id>.faiss/on disk - The
sessionsdict mapssession_id→ vector store path - Sessions persist only for the lifetime of the running process; vector stores persist on disk until
/reset/is called
| Scenario | Behavior |
|---|---|
| SQL execution fails | Returns HTTP 400 with {"error": "SQL execution error: ..."} |
| Unsupported file type uploaded | Returns {"error": "Only PDF and CSV supported"} |
Question asked with invalid session_id |
Returns {"error": "Invalid or expired session_id"} |
| No rows returned from SQL | Returns "No relevant data found to summarize." |
| Ollama not running | ConnectionRefusedError raised at request time |
PostgreSQL connection error
- Verify PostgreSQL is running:
pg_isreadyor check your system services - Double-check credentials in
src/config/database.py - Ensure the database and all four tables exist
Ollama model not found
ollama pull phi3:mini
ollama pull nomic-embed-text
ollama serveModuleNotFoundError for Schema.schema
The services.py file imports from Schema.schema import TABLE_SCHEMAS. Run the database API from the project root so Python resolves the path correctly:
# From project root
uvicorn main:app --reloadVector store errors on /ask/
The sessions dict is in-memory. If the server restarts, session state is lost even though .faiss files remain on disk. Re-upload the document to get a new session_id.
Clear all vector stores manually
# Windows
rmdir /s /q vectorstores
mkdir vectorstores
# Linux/macOS
rm -rf vectorstores/*Dependency conflicts
pip install --upgrade -r requirements.txtImplemented:
- Database credentials are isolated in
src/config/database.py data/andvectorstores/are gitignored — uploaded files and indexes are not committed.envis gitignored
Recommended for production:
- Move
DB_PARAMSto environment variables or a.envfile — never hardcode credentials in source files - Validate uploaded file types and sizes before processing
- Restrict PostgreSQL user permissions to
SELECTonly for the query service - Run behind HTTPS with a reverse proxy (nginx, Caddy)
- Add rate limiting to the
/askendpoints - Scope CORS origins explicitly
- SQL generation accuracy depends on the quality of
phi3:mini. Complex multi-join queries or ambiguous questions may produce incorrect SQL. - Document extraction quality depends on the input. Scanned PDFs without OCR will produce poor or empty text.
- Session state is in-memory — restarting the server loses all active sessions.
- No authentication — the APIs are open by default.
- Local inference speed depends on your hardware.
phi3:miniis lightweight but still requires a capable CPU or GPU for reasonable latency. - CSV-to-text conversion uses
df.to_string(), which may not be optimal for very large CSV files.
- Document ingestion (PDF + CSV)
- FAISS vector store with session isolation
- Natural language → SQL → PostgreSQL pipeline
- LLM-powered result summarization
- Conversation memory per session
- Fully local inference via Ollama
- Environment variable configuration (
.envsupport) - Streaming responses for long answers
- Background document processing (async upload)
- Automated test suite
- Authentication / API key middleware
- Rate limiting
- Hybrid retrieval (keyword + semantic)
- Observability / request logging
- Docker Compose setup
Contributions are welcome.
# Fork the repo, then:
git clone https://github.com/SadiqCodex/Smart-Rag-Engine.git
cd Smart-Rag-Engine
git checkout -b feature/your-feature-name- Make your changes
- Test manually against both APIs
- Update this README if your change affects setup or usage
- Commit with a clear message
- Push and open a Pull Request against
main
Please follow PEP 8 and add type hints to new functions.
MIT License — see LICENSE for details.
Copyright (c) 2024 Sadik Mohammad
Sadik Mohammad GitHub: SadiqCodex Repository: SadiqCodex/Smart-Rag-Engine
- FastAPI — the API layer
- LangChain — RAG and chain orchestration
- Ollama — local LLM inference
- FAISS — vector similarity search
- PostgreSQL — structured data storage
Smart RAG Engine demonstrates how a modern LLM application can bridge unstructured documents and structured relational databases through retrieval-augmented generation, FAISS vector search, schema-aware SQL generation, and fully local model inference — without sending a single byte of your data to an external service.