Production-oriented AI backend service built for the Backend & AI Engineering Track assessment.
This project exposes an AI assistant API that can answer natural-language analytics questions by using:
- FastAPI for the HTTP API
- LangGraph for agent orchestration
- LangChain for model and tool integration
- MCP for tool serving
- Ollama for local LLM inference
- Qwen2.5 3B as the local language model
- PostgreSQL for structured analytics data
All AI inference runs locally through Ollama. No external LLM APIs are used.
User
↓
FastAPI API
↓
LangGraph ReAct Agent
↓
Qwen2.5 3B via Ollama
↓
MCP Tool Server
↓
PostgreSQL Database
.
├── main.py
├── mcp_server.py
├── settings.py
├── requirements.txt
├── .env.example
├── README.md
├── agent/
│ ├── __init__.py
│ └── graph.py
└── db/
├── __init__.py
├── session.py
└── schema.sql
Install the following before running the project:
- Python 3.11+
- Docker
- Ollama
- Git
git clone <YOUR_REPOSITORY_URL>
cd <YOUR_REPOSITORY_NAME>python -m venv .venv
source .venv/bin/activatepython -m venv .venv
.venv\Scripts\Activate.ps1pip install -r requirements.txtCopy the example environment file:
cp .env.example .envOn Windows PowerShell:
Copy-Item .env.example .envThe .env file should contain:
OLLAMA_BASE_URL=http://localhost:11434
OLLAMA_MODEL=qwen2.5:3b
DATABASE_URL=postgresql://assess:assess@localhost:5432/analytics
MCP_SERVER_URL=http://localhost:8001/mcpPull the required local model:
ollama pull qwen2.5:3bStart the Ollama server:
ollama serveOllama should now be running at:
http://localhost:11434
You can verify it with:
curl http://localhost:11434/api/tagsStart a PostgreSQL 16 container:
docker run --name pg-assess \
-e POSTGRES_USER=assess \
-e POSTGRES_PASSWORD=assess \
-e POSTGRES_DB=analytics \
-p 5432:5432 \
-d postgres:16If the container already exists, start it with:
docker start pg-assessThe seed script creates the required products and orders tables and inserts sample data.
docker cp db/schema.sql pg-assess:/schema.sql
docker exec -it pg-assess psql -U assess -d analytics -f /schema.sqlVerify that the tables were created:
docker exec -it pg-assess psql -U assess -d analytics -c "\dt"Expected tables:
orders
products
Verify the seed data:
docker exec -it pg-assess psql -U assess -d analytics -c "SELECT COUNT(*) FROM orders;"
docker exec -it pg-assess psql -U assess -d analytics -c "SELECT COUNT(*) FROM products;"Both should return 5.
Open a new terminal and activate the virtual environment.
.venv\Scripts\Activate.ps1
python mcp_server.pyThe MCP server runs at:
http://localhost:8001/mcp
The server may not return a normal browser response because MCP Streamable HTTP expects specific headers. This is normal.
Open another terminal and activate the virtual environment.
.venv\Scripts\Activate.ps1
python -m uvicorn main:app --host 0.0.0.0 --port 8000 --reloadIf Windows blocks uvicorn.exe, use python -m uvicorn as shown above.
FastAPI will run at:
http://localhost:8000
Interactive API documentation:
http://localhost:8000/docs
Start services in this order:
1. Ollama
2. PostgreSQL
3. MCP server
4. FastAPI server
curl http://localhost:8000/healthExample response:
{
"status": "ok",
"ollama": "ok",
"database": "ok",
"mcp": "ok"
}curl http://localhost:8000/toolsThis returns the MCP tools currently available to the agent.
Expected tools:
query_database
get_current_time
date_diff
list_tables
describe_table
curl -X POST http://localhost:8000/chat \
-H "Content-Type: application/json" \
-d '{"question":"How many pending orders do we have?"}'Example response:
{
"answer": "There is 1 pending order.",
"tool_calls": [
{
"tool": "query_database",
"input": {
"sql": "SELECT COUNT(*) AS pending_orders FROM orders WHERE status = 'pending';"
},
"output": {
"ok": true,
"row_count": 1,
"rows": [
{
"pending_orders": 1
}
]
},
"error": null
}
],
"model": "qwen2.5:3b",
"duration_ms": 1240
}curl http://localhost:8000/historyReturns the last 20 chat interactions stored in memory.
Persistence across restarts is not required.
curl -X POST http://localhost:8000/chat \
-H "Content-Type: application/json" \
-d '{"question":"List all products in the Electronics category with their prices."}'Expected result:
Wireless Keyboard - 49.99
USB-C Hub - 34.99
curl -X POST http://localhost:8000/chat \
-H "Content-Type: application/json" \
-d '{"question":"How many days ago was the most recent completed order placed?"}'Expected behavior:
- The agent queries the
orderstable. - The agent uses
get_current_timeordate_diff. - The response explains how long ago the most recent completed order was placed.
curl -X POST http://localhost:8000/chat \
-H "Content-Type: application/json" \
-d '{"question":"What tables are available and what columns does the orders table have?"}'Expected behavior:
- The agent calls
list_tables. - The agent calls
describe_table. - The response summarizes the available tables and the
orderscolumns.
curl -X POST http://localhost:8000/chat \
-H "Content-Type: application/json" \
-d '{"question":"What is the total revenue from completed orders, and what percentage of all orders does that represent?"}'Expected result based on seed data:
Completed orders generated 134.97 in revenue and represent 40% of all orders.
Calculation:
Completed revenue = 99.98 + 34.99 = 134.97
Completed orders = 2
Total orders = 5
Percentage = 2 / 5 * 100 = 40%
The MCP server exposes the following tools:
Executes one read-only PostgreSQL SELECT query and returns results as JSON objects.
Non-SELECT statements are rejected.
Rejected examples:
DROP TABLE products;
DELETE FROM orders;
UPDATE products SET price = 0;Returns:
- Current UTC timestamp
- Day of week
- Human-readable phrase such as
afternoon on a Wednesday
Accepts two ISO-8601 date strings and returns the difference in:
- Days
- Hours
- Minutes
- Total minutes
Returns all user-created tables in the connected PostgreSQL database.
Returns schema details for a specific table:
- Column name
- Data type
- Nullable flag
The application returns structured JSON errors for:
- Invalid requests
- Agent failures
- Database query failures
- Missing or unavailable services
- Invalid date input
- Unsafe SQL statements
- Unknown table names
The API avoids exposing raw stack traces to clients.
Make sure the MCP server is running:
python mcp_server.pyThen restart FastAPI.
Startup order matters:
1. Ollama
2. PostgreSQL
3. MCP server
4. FastAPI
The database is running, but the seed script was not applied.
Run:
docker cp db/schema.sql pg-assess:/schema.sql
docker exec -it pg-assess psql -U assess -d analytics -f /schema.sqlThen restart the MCP and FastAPI servers.
Run Uvicorn as a Python module:
python -m uvicorn main:app --host 0.0.0.0 --port 8000 --reloadIf reload causes issues:
python -m uvicorn main:app --host 0.0.0.0 --port 8000PowerShell aliases curl to Invoke-WebRequest.
The MCP endpoint expects Streamable HTTP requests and may reject simple browser-style requests.
This is normal if the FastAPI /tools endpoint successfully lists the tools.
Use this to verify MCP integration:
curl http://localhost:8000/toolsThe agent uses a ReAct-style LangGraph loop with:
reason
act
should_continue
The loop is capped at 10 iterations to prevent infinite cycles.
Each /chat response includes a structured tool_calls trace showing:
- Tool name
- Tool input
- Tool output
- Tool error, if any
No bonus features implemented.