A visual web application built with Django, HTML and CSS that displays Pokémon information via the PokéAPI and lets the user interact with an LLM through OpenRouter to ask questions about the selected Pokémon. Answers are grounded with Retrieval-Augmented Generation (RAG): Pokémon lore is embedded locally and stored in a Qdrant vector database, and the most relevant lore is retrieved and injected into the prompt for every question.
- Overview
- Features
- Tech Stack
- Technical Decisions
- Project Structure
- Prerequisites
- Running with Docker
- Installation and Setup (without Docker)
- Running Locally (without Docker)
- Running Tests
- Environment Variables
The Smart Pokédex is made up of two main pages:
- Pokédex (
/home/) — a visual list of Pokémon with sprite, types and attribute bars, consuming real-time data from the PokéAPI. - Detail (
/pokemon/<id>/) — a full profile of the selected Pokémon, with a query area where the user can ask an LLM questions and see the model's generated answer.
- List of Pokémon with sprite, number, types and mini HP/ATK bars
- Detail page with a full profile: height, weight, ability, base experience and bars for every stat
- Query area to send questions to an LLM via OpenRouter
- Answers are grounded with RAG: each question retrieves the most relevant Pokémon lore from a Qdrant vector database and injects it into the prompt
- The retrieved lore (Pokémon name + similarity score) is shown alongside the model's answer for transparency
- The model's answer is displayed on the same page
- Visual identity inspired by the classic Pokédex, with a CRT terminal aesthetic
| Technology | Role |
|---|---|
| Python 3.x | Main language |
| Django | Web framework (views, urls, templates, sessions) |
| HTML + CSS | Interface and visual identity |
| PokéAPI | Source of Pokémon data |
| OpenRouter API | Access to the LLM that answers questions |
| Python Requests | HTTP requests to the external APIs |
| Django Sessions | Passing data between views via POST/redirect/GET |
| MySQL | Relational database (via PyMySQL) |
| Redis | Caching layer for PokéAPI responses |
| sentence-transformers | Local embedding generation (all-MiniLM-L6-v2), no API cost |
| Qdrant | Vector database storing Pokémon lore embeddings for RAG retrieval |
| Docker + Docker Compose | Containerized app, database, cache and vector store for a one-command setup |
| pytest + pytest-django | Automated test suite |
The project is split into apps with distinct responsibilities, following Django's modularization convention. Each app has its own views.py and urls.py, and the main urls.py delegates routes to each app via include().
Fetching and parsing PokéAPI data lives in a single module (pokedex/services/pokeapi.py), reused by both the home and pokemon_detail apps, instead of being duplicated per view.
Calls to the PokéAPI and OpenRouter are made server-side (in Django views), not in the browser. This avoids exposing the OpenRouter API key on the frontend and centralizes error handling.
The model query form follows the PRG pattern (Post/Redirect/Get): the POST is handled by the pokemon_query view, which saves the answer in the Django session and redirects to pokemon_detail via GET. This avoids resubmitting the form on page reload.
The LLM's answer is temporarily stored in the Django session and consumed with .pop() in the detail view, guaranteeing it's shown only once and cleared automatically afterwards.
The whole interface is built with plain HTML and CSS, with no frontend framework. The only JavaScript present is inline and minimal: a character counter for the textarea and visual feedback on the submit button.
Secrets (Django SECRET_KEY, OPENROUTER_API_KEY) and environment-dependent settings (DEBUG, ALLOWED_HOSTS, database credentials) are read from a local .env file via python-dotenv, never hardcoded in source.
The project uses MySQL as its relational database, connected through PyMySQL — a pure-Python driver. Unlike mysqlclient (the more common choice), it needs no native build tools to install, so it works the same way on every OS and inside Docker without extra system packages. Database connection settings (DB_NAME, DB_USER, DB_PASSWORD, DB_HOST, DB_PORT) come from the .env file.
The home page originally fetched all 151 Pokémon from the PokéAPI sequentially, one HTTP request at a time, on every single page load — around 18 seconds per load in testing. pokedex/services/pokeapi.py now checks Redis (via Django's built-in cache framework, django.core.cache.backends.redis.RedisCache) before making a request, and caches the parsed result for 24 hours (a Pokémon's base stats/types/sprite essentially never change). After the first load, subsequent loads read entirely from Redis — down to tens of milliseconds. Redis connection settings (REDIS_HOST, REDIS_PORT, REDIS_DB) come from the .env file.
Four services run together: web (this Django app), db (MySQL), redis (Redis) and qdrant (Qdrant). db and redis don't publish ports to the host — only web and qdrant do — qdrant publishes 6333/6334 so its dashboard (http://localhost:6333/dashboard) is browsable outside Docker. web waits for db, redis and qdrant to report healthy (via Compose's depends_on: condition: service_healthy) before starting, and entrypoint.sh runs manage.py migrate automatically on every container start, so the schema is always up to date. Host/port values for DB_HOST/REDIS_HOST/QDRANT_HOST are overridden directly in docker-compose.yml (to the service names db/redis/qdrant) rather than taken from .env, since .env's 127.0.0.1 only makes sense for running the app outside Docker.
The LLM query feature is extended with Retrieval-Augmented Generation instead of relying on the model's own (unreliable, unverifiable) knowledge of Pokémon lore. A one-time management command, python manage.py build_rag_index, fetches species flavor text for Pokémon 1–151 from PokéAPI (pokedex/services/pokeapi.py:fetch_pokemon_species), builds one text document per Pokémon (pokemon_rag/documents.py), embeds each document locally with sentence-transformers (all-MiniLM-L6-v2, 384 dimensions, CPU-only, no API cost), and upserts the vectors into a Qdrant collection (pokemon_rag/services/vector_store.py) keyed by Pokémon id — safe to rerun at any time.
At query time (pokemon_rag/retrieval.py), the user's question is combined with the current Pokémon's name ("{pokemon_name}: {question}") before being embedded and searched — this mirrors how the LLM prompt itself is phrased, so the shared name token biases genuine top-k semantic search toward the right Pokémon's document without a hard pokemon_id filter, which would defeat the point of having a vector database. The retrieved snippets are injected into the OpenRouter prompt (pokemon_query/openrouter.py) as reference context, and also shown in the UI under the model's answer for transparency into what the RAG step retrieved.
Known limitation: the MVP corpus is flavor-text only (no moves/type-matchup data), so a question like "why is it strong against water types" will correctly retrieve that Pokémon's document, but the document won't contain an answer — this is a corpus-scope limitation, not a retrieval bug, and would be addressed by extending the corpus to moves/abilities.
pokedex/settings_test.py swaps DATABASES to an in-memory SQLite database and CACHES to Django's LocMemCache, used only when running pytest (wired up in pytest.ini). Since the app has no MySQL-specific SQL and nothing that depends on Redis's actual behavior, this keeps the test suite fast and independent of Docker/MySQL/Redis being up — important both for a quick local feedback loop and for CI. Every PokéAPI and OpenRouter call is mocked (unittest.mock.patch) rather than hitting the real network, so tests can't fail because of an outage or rate limit on either external service.
Smart-Pokedex/
│
├── pokedex/ # Global project configuration
│ ├── settings.py
│ ├── settings_test.py # Settings used only by pytest (SQLite + local-memory cache)
│ ├── urls.py # Main routing
│ ├── wsgi.py / asgi.py
│ └── services/
│ ├── pokeapi.py # Shared PokéAPI client
│ └── test_pokeapi.py
│
├── home/ # Pokédex list app
│ ├── views.py
│ ├── test_views.py
│ └── templates/
│ └── pokedex.html
│
├── pokemon_detail/ # Detail app
│ ├── views.py
│ ├── test_views.py
│ └── templates/
│ └── pokemon_detail.html
│
├── pokemon_query/ # LLM query app
│ ├── views.py
│ ├── test_views.py
│ ├── openrouter.py # OpenRouter API integration
│ └── test_openrouter.py
│
├── pokemon_rag/ # RAG app: embeddings, vector store, retrieval
│ ├── documents.py # Builds one text document per Pokémon
│ ├── test_documents.py
│ ├── retrieval.py # Query-time embed + search orchestration
│ ├── test_retrieval.py
│ ├── services/
│ │ ├── embeddings.py # Local sentence-transformers client
│ │ ├── test_embeddings.py
│ │ ├── vector_store.py # Qdrant client
│ │ └── test_vector_store.py
│ └── management/commands/
│ ├── build_rag_index.py # Builds/rebuilds the Qdrant corpus
│ └── test_build_rag_index.py
│
├── templates/
│ └── static/
│ └── css/
│ ├── pokedex.css
│ └── pokemon_detail.css
│
├── .env # Environment variables (not versioned)
├── .env.example # Template for .env
├── Dockerfile # Image for the web service
├── entrypoint.sh # Runs migrations, then starts the app
├── docker-compose.yml # Wires up web + MySQL + Redis
├── pytest.ini # pytest / pytest-django configuration
├── requirements.txt
├── requirements-dev.txt # requirements.txt + pytest tooling
└── manage.py
- Git
- An account and API key from OpenRouter
- Internet connection (to consume the PokéAPI and OpenRouter)
- To run with Docker (recommended): Docker and Docker Compose
- To run without Docker: Python 3.10+, pip, a running MySQL server, a running Redis server and a running Qdrant instance
This is the fastest way to get everything running — Django, MySQL, Redis and Qdrant — with a single command, no local Python or database setup required.
1. Clone the repository
git clone https://github.com/GMCavalheri/Smart-Pokedex
cd Smart-Pokedex2. Configure environment variables
cp .env.example .envEdit .env and fill in SECRET_KEY, OPENROUTER_API_KEY, and set your own DB_PASSWORD/DB_ROOT_PASSWORD (see Environment Variables). You can leave DB_HOST/REDIS_HOST as-is — docker-compose.yml overrides them automatically for the containers.
3. Build and start everything
docker compose up --buildThis builds the app image, starts db (MySQL), redis and qdrant, waits for all three to be healthy, then starts web — which runs migrations automatically before serving.
Open in your browser: http://127.0.0.1:8000/home/
4. Build the RAG corpus (one-time step, run whenever you want to rebuild it)
docker compose exec web python manage.py build_rag_indexThis fetches flavor text for Pokémon 1–151 from PokéAPI, embeds it locally, and indexes it into Qdrant — takes a minute or two. You can browse the resulting collection at http://localhost:6333/dashboard#/collections/pokemon_docs.
5. Stop everything
docker compose downAdd -v (docker compose down -v) if you also want to wipe the MySQL data volume.
Made a code change? Rebuild the image so it's picked up:
docker compose up --build1. Clone the repository
git clone https://github.com/GMCavalheri/Smart-Pokedex
cd Smart-Pokedex2. Create and activate a virtual environment
python -m venv venv
# Linux/macOS
source venv/bin/activate
# Windows
venv\Scripts\activate3. Install dependencies
pip install -r requirements.txt4. Configure environment variables
Create a .env file in the project root (see the Environment Variables section). Make sure DB_* points to a running MySQL server with a matching database and user already created, REDIS_* points to a running Redis server, and QDRANT_* points to a running Qdrant instance.
5. Run migrations
python manage.py migrate6. Build the RAG corpus (one-time step, run whenever you want to rebuild it)
python manage.py build_rag_indexpython manage.py runserverOpen in your browser: http://127.0.0.1:8000/home/
The test suite uses pytest + pytest-django, and needs no external services — no MySQL, no Redis, no Qdrant, no Docker. Tests run against an in-memory SQLite database and Django's local-memory cache instead (pokedex/settings_test.py), and every call to the PokéAPI, OpenRouter, the embedding model or Qdrant is mocked, so the suite is fast and fully offline.
1. Install the dev dependencies (adds pytest, pytest-django and pytest-cov on top of requirements.txt)
pip install -r requirements-dev.txt2. Run the suite
pytest3. (Optional) Run with a coverage report
pytest --cov=home --cov=pokemon_detail --cov=pokemon_query --cov=pokemon_rag --cov=pokedex/services --cov-report=term-missingTest files live next to the code they cover, named test_*.py:
pokedex/services/test_pokeapi.py— parsing logic, stat percentage math, species flavor-text cleaning, and that a second call for the same Pokémon is served from cache instead of hitting the network again.pokemon_query/test_openrouter.py— prompt building (with and without retrieved context) and the OpenRouter request/response handling.pokemon_rag/test_documents.py,pokemon_rag/test_retrieval.py— corpus document text building, and query-time embed → search → payload-mapping orchestration.pokemon_rag/services/test_embeddings.py,pokemon_rag/services/test_vector_store.py— the sentence-transformers and Qdrant clients, both fully mocked (no real model load, no real Qdrant).pokemon_rag/management/commands/test_build_rag_index.py— the corpus-building management command.home/test_views.py,pokemon_detail/test_views.py,pokemon_query/test_views.py— each view's behavior through Django's test client, including the session-based POST → Redirect → GET flow, the RAG retrieval step, and the error path.
Copy .env.example to .env and fill in your own values:
cp .env.example .envSECRET_KEY=your-django-secret-key-here
DEBUG=True
ALLOWED_HOSTS=localhost,127.0.0.1
OPENROUTER_API_KEY=your-openrouter-api-key-here
# MySQL
DB_NAME=pokedex
DB_USER=pokedex_user
DB_PASSWORD=your-db-password
DB_ROOT_PASSWORD=your-db-root-password
DB_HOST=localhost
DB_PORT=3306
# Redis
REDIS_HOST=localhost
REDIS_PORT=6379
REDIS_DB=0
# Qdrant / RAG
QDRANT_HOST=localhost
QDRANT_PORT=6333
QDRANT_COLLECTION_NAME=pokemon_docs
RAG_EMBEDDING_MODEL=all-MiniLM-L6-v2
RAG_TOP_K=3Warning: never commit the
.envfile. It is already listed in.gitignore.
Note:
DB_ROOT_PASSWORDis only used by the MySQL container itself (its root superuser account, set up indocker-compose.yml) — Django never uses it.DB_HOST/DB_PORT/REDIS_HOST/REDIS_PORT/QDRANT_HOSThere are for running the app without Docker; when running viadocker compose, those five are overridden automatically (see Running with Docker).
To generate a Django SECRET_KEY:
python -c "from django.core.management.utils import get_random_secret_key; print(get_random_secret_key())"
