DEV Community

Cover image for I Built an AI-Powered Pokedex with Python & Streamlit (And It's Free!) πŸš€
Hoang Manh Cam
Hoang Manh Cam

Posted on

I Built an AI-Powered Pokedex with Python & Streamlit (And It's Free!) πŸš€

πŸ”΄ Introduction

Remember the original Pokedex? It was a handheld encyclopedia that gave you information about every Pokemon you encountered. Fast forward to 2025, and I thought: "What if the Pokedex could actually talk back?"

In this article, I'll take you on a deep technical dive into how I built a "Minimal Pokedex" web app. This isn't just a "hello world" tutorial; I'll show you how I handled real-world challenges like API caching, complex UI states, recursive data structures, and finally, integrating a Large Language Model (LLM) for real-time chat.

Best of all? It's built entirely in Python, hosted for free, and the AI costs $0 to run!

πŸ”— Live Demo: https://pokedex-ai.streamlit.app/

πŸ‘¨β€πŸ’» GitHub Repo: cam-hm/pokedex-ai

Demo


πŸ› οΈ The Tech Stack

I chose this stack for speed and simplicity:

  • Frontend: Streamlit - Turns Python scripts into shareable web apps in minutes.
  • Data: PokeAPI - The gold standard for Pokemon data.
  • AI: Groq API - Running Llama-3.3-70b, which is incredibly fast and free for developers.
  • Hosting: Streamlit Cloud - One-click deployment from GitHub.

✨ What We're Building (Feature Overview)

Before we dive into the code, let's look at what this app actually does. It's not just a list of names; it's a full-featured companion.

1. 🏠 The Animated Grid

Instead of static images, the home page features a responsive grid of Animated Sprites (using Showdown GIFs). It feels alive and nostalgic.

2. πŸ“Š Deep Detail View

Clicking a Pokemon reveals:

  • Official Artwork (High-res)
  • Base Stats visualized with colored progress bars.
  • Evolution Chain navigation.
  • Type Effectiveness calculator.

3. πŸ€– The AI Expert

The killer feature. You can chat with the Pokedex!

  • "How do I evolve Eevee into Umbreon?"
  • "Is this Pokemon good against Water types?" The AI answers using real game data, not just generic knowledge.

πŸ“± Phase 1: The "Monolith" (Building the MVP)

I started exactly how you should start a prototype: One single file (app.py).

My goal was to get something on the screen fast. I didn't worry about folder structures or architecture. I just wanted to fetch Pokemon and show them.

1. Fetching Data (The Naive Way)

I wrote a simple function right inside app.py to hit the PokeAPI. To prevent the app from being slow, I used Streamlit's caching.

# app.py

import streamlit as st
import requests

@st.cache_data
def get_pokemon_list(limit=100):
    url = f"https://pokeapi.co/api/v2/pokemon?limit={limit}"
    return requests.get(url).json()['results']

pokemon_list = get_pokemon_list()
Enter fullscreen mode Exit fullscreen mode

2. The Grid Layout

I iterated through the list and displayed them. I wanted animated sprites, so I added some logic to check for the "Showdown" GIF first.

# app.py (continued)

cols = st.columns(4)
for i, pokemon in enumerate(pokemon_list):
    with cols[i % 4]:
        # Logic to find the best sprite URL
        sprite_url = get_sprite_url(pokemon['id']) 
        st.image(sprite_url)
Enter fullscreen mode Exit fullscreen mode

3. Adding Complexity (The Detail View)

Then I added the detail view. This is where things got tricky.

The Tricky Part: Evolution Chains (Recursion!) 🀯

Displaying "Charmander β†’ Charmeleon β†’ Charizard" sounds easy, but the API returns a deeply nested JSON tree (chain -> evolves_to -> evolves_to...).

I had to write a recursive function inside app.py to flatten this tree:

# app.py (still the same file!)

def parse_evolution_chain(chain_link, evo_list=None):
    if evo_list is None: evo_list = []

    species_name = chain_link['species']['name']
    evo_list.append(species_name)

    # Recursion magic happens here
    for evolution in chain_link['evolves_to']:
        parse_evolution_chain(evolution, evo_list)

    return evo_list
Enter fullscreen mode Exit fullscreen mode

Visualizing Stats

I also wanted those cool colored progress bars for stats (Green for high, Red for low). Streamlit's default bar wasn't enough, so I injected custom HTML/CSS directly into the Python script.

# app.py

st.markdown(f"""
<div style="width: {percentage}%; background-color: {color};"></div>
""", unsafe_allow_html=True)
Enter fullscreen mode Exit fullscreen mode

All of this logicβ€”API calls, recursion, HTML generationβ€”was stuffed into app.py.

By the time I finished the core features, app.py had grown to 500+ lines. It was a mix of everything.

It worked, but it was becoming unmanageable.

πŸ—οΈ Phase 2: Refactoring for Scale (The "Spaghetti" Problem)

As I added more featuresβ€”Shiny Toggle, Type Effectiveness Modal, Audio Criesβ€”my app.py started to look like a disaster zone. It was a 500-line monolithic script where UI code was mixed with API logic and state management.

The Problem:

  • Readability: I couldn't find anything.
  • State Management: Variables were being overwritten.
  • Scalability: Adding AI to this mess would be a nightmare.

The Solution: Modular Architecture
I stopped coding features and spent an hour refactoring. I split the app into 4 distinct layers:

pokedex_app/
β”œβ”€β”€ app.py                  # Entry point (Main router - only 20 lines!)
β”œβ”€β”€ src/
β”‚   β”œβ”€β”€ config/             # Constants (Type colors, Gen limits)
β”‚   β”œβ”€β”€ api/                # API Client (Network layer, Caching)
β”‚   β”œβ”€β”€ services/           # Business Logic (Data processing, AI)
β”‚   └── ui/                 # Presentation Layer
β”‚       β”œβ”€β”€ components/     # Reusable widgets (Modals, Cards)
β”‚       β”œβ”€β”€ home.py         # Home Page Logic
β”‚       └── detail.py       # Detail Page Logic
Enter fullscreen mode Exit fullscreen mode

Why this matters:
By moving the Pokemon logic to src/services/pokemon_service.py and the UI to src/ui/, I created a clean slot for the AI. When I was ready to add the chatbot, I didn't have to touch the existing UI codeβ€”I just created a new src/services/ai_service.py.


πŸ€– Phase 3: The AI Brain (Groq + Llama 3)

Now, the killer feature: Conversational AI.

I didn't want a generic chatbot that says "I am an AI model." I wanted a Pokemon Expert that knows exactly which Pokemon you are looking at.

1. The Challenge: Hallucinations

If you ask ChatGPT "How do I beat Charizard?", it gives a generic answer. But what if you're playing a specific ROM hack where Charizard is a Water type? The AI wouldn't know.

2. The Solution: Context Injection (RAG-lite)

I used a technique similar to RAG (Retrieval Augmented Generation). Instead of searching a vector database, I inject the Ground Truth (the exact stats of the current Pokemon) directly into the system prompt.

Here is the exact prompt engineering strategy:

# src/services/ai_service.py

class PokemonChatbot:
    def chat(self, pokemon_name, pokemon_data, user_message):
        # 1. Extract relevant data
        stats = pokemon_data['stats']
        types = [t['type']['name'] for t in pokemon_data['types']]
        abilities = [a['ability']['name'] for a in pokemon_data['abilities']]

        # 2. Construct the Context String
        # This is what the AI "sees" before it answers
        context = f"""
        You are analyzing {pokemon_name}.
        Type: {', '.join(types)} 
        Abilities: {', '.join(abilities)}
        Base Stats: {stats}
        Height: {pokemon_data['height']} | Weight: {pokemon_data['weight']}
        """

        # 3. System Prompt
        system_prompt = f"""
        You are a Pokemon Expert. Use the data below to answer the user's question.

        DATA CONTEXT:
        {context}

        GUIDELINES:
        - Be concise, strategic, and friendly.
        - Use emojis (πŸ”₯, ⚑, πŸ›‘οΈ).
        - If asked about battles, refer to the specific Stats provided.
        - Do NOT make up information not in the data.
        """

        # 4. Call Groq API
        return self.client.chat.completions.create(
            model="llama-3.3-70b-versatile",
            messages=[{"role": "system", "content": system_prompt}, ...]
        )
Enter fullscreen mode Exit fullscreen mode

3. Why Groq? (Speed is a Feature)

For a chat interface, latency is everything.

  • OpenAI GPT-4: Great quality, but can be slow (2-5s) and expensive.
  • Groq Llama 3: Instant (<0.5s) and Free for developers.

Groq's LPU (Language Processing Unit) inference engine delivers responses so fast it feels like the AI is pre-generating them. For a real-time companion app, this speed difference is the difference between "cool" and "usable."


πŸš€ Deployment

directly with GitHub.

The Secret Sauce:
To keep my API keys safe, I used Streamlit's Secrets management.

  1. Local: .streamlit/secrets.toml (Git ignored)
  2. Production: Streamlit Dashboard -> App Settings -> Secrets
# .streamlit/secrets.toml
GROQ_API_KEY = "gsk_..."
Enter fullscreen mode Exit fullscreen mode

πŸ’‘ Key Takeaways

  1. UX Matters: Small details like animated sprites, colored stat bars, and loading states make a "toy app" feel professional.
  2. Architecture Saves Time: Refactoring early allowed me to add the AI feature in just 30 minutes.
  3. AI Needs Context: The chatbot is only smart because I programmatically feed it the correct data. Without context, it's just ChatGPT; with context, it's a Pokedex Expert.

πŸ‘‹ Try it out!

Go ahead, ask the AI for a competitive moveset for Magikarp (spoiler: it's Flail).

πŸ‘‰ App: pokedex-ai.streamlit.app

πŸ‘‰ Code: github.com/cam-hm/pokedex-ai

If you enjoyed this write-up, drop a star on the repo! ⭐

python #streamlit #ai #coding #pokemon

Top comments (0)