DEV Community

Cover image for Pet Recommendation App
Tom Jacobson
Tom Jacobson

Posted on

Pet Recommendation App

Building an AI-Powered Dog Breed Recommender with Flask, Nyckel, and Google Gemini

Overview

This application takes a photo or image URL of a dog and passes it to an external API to determine the breed. Once the breed is identified, a custom prompt is sent to Google Gemini and the results are returned to the user as a tailored list of care recommendations for that specific breed.

Tech stack: Python, Flask, Nyckel API, Google Gemini 1.5 Flash, Docker


Screenshots

Architecture

The application follows a simple two-step pipeline:

User Input (image/URL + optional fields)
        ↓
Nyckel Dog Breed Classifier API
        ↓
Google Gemini 1.5 Flash (LLM)
        ↓
Results Page (breed + care recommendations)
Enter fullscreen mode Exit fullscreen mode

The Flask app exposes two routes:

  • / — renders the input form and handles POST submissions (Query view)
  • /results — renders the breed and recommendations (Results view)

Project Structure

├── app.py          # Flask entry point, URL routing
├── query.py        # Query view: image processing, API calls
├── results.py      # Results view: parses and renders recommendations
├── templates/
│   ├── layout.html
│   ├── query.html  # Input form
│   └── results.html
├── static/css/
├── requirements.txt
└── Dockerfile
Enter fullscreen mode Exit fullscreen mode

How It Works

1. Handling User Input

The Query view accepts two input types — a file upload or an image URL — along with optional fields for age, activity level, and free-text notes.

url = request.form['url']
image_file = request.files.get('image file')
age = request.form['age']
activity = request.form['activity_level']
additional_info = request.form['additional_info']
Enter fullscreen mode Exit fullscreen mode

Uploaded files are saved to static/uploads/ with a timestamped filename to avoid conflicts:

cleaned_filename = f"{int(time.time())}_{random.randint(0, 1000)}_user_upload.jpg"
Enter fullscreen mode Exit fullscreen mode

2. Breed Classification — Nyckel API

The app sends the image to Nyckel's pretrained dog breed identifier. File uploads are sent as multipart form data; URLs are sent as JSON.

classifier = 'https://www.nyckel.com/v1/functions/dog-breed-identifier/invoke'

# File upload
with open(filename, 'rb') as img:
    result = requests.post(classifier, files={"data": img})

# URL
result = requests.post(classifier, json={"data": url})

breed = json.loads(result.text)["labelName"]
Enter fullscreen mode Exit fullscreen mode

Nyckel returns a labelName field containing the identified breed, which is passed to Gemini.

3. Generating Recommendations — Google Gemini

The generate_recommendations method builds a prompt using the detected breed and any optional fields the user provided, then calls Gemini 1.5 Flash.

prompt = f"""I am thinking about adopting a new dog. The dog is an {breed}.
Could you provide me with a succinct bulleted list of care recommendations
for my new dog? Please provide breed specific advice."""

if age:
    prompt += f"The dog is {age} years old."
if activity:
    prompt += f"The dog has a {activity} level."
if additional_info:
    prompt += f"Some additional information to consider is as follows {additional_info}"
Enter fullscreen mode Exit fullscreen mode

A formatting instruction is appended to keep the output consistent for parsing:

formatting_parameters = "Do not bold anything."
response = model.generate_content(prompt + formatting_parameters)
Enter fullscreen mode Exit fullscreen mode

4. Parsing and Rendering Results

Gemini returns a markdown-style bulleted list. The Results view uses a regex split on single asterisks to convert it into a Python list for clean rendering:

recommendation_list = re.split(r"\*(?!\*)", recommendation)
Enter fullscreen mode Exit fullscreen mode

The results template then iterates over the list and renders each item as an <li> element.


Setup & Configuration

Prerequisites

  • Python 3.9+
  • A Google Gemini API key

Local Setup

# Create and activate a virtual environment
python3 -m venv venv
venv\Scripts\activate        # Windows
source venv/bin/activate     # Linux/macOS

# Install dependencies
pip install -r requirements.txt

# Set your API key
$env:GOOGLE_API_KEY = "your_key_here"   # Windows
export GOOGLE_API_KEY="your_key_here"   # Linux/macOS

# Run the app
python app.py
Enter fullscreen mode Exit fullscreen mode

The app runs on http://localhost:8080 by default.


Docker Deployment

The included Dockerfile uses a python:3.9-slim base image and exposes port 8080.

FROM python:3.9-slim
WORKDIR /app
COPY . /app
RUN pip install --no-cache-dir -r requirements.txt
EXPOSE 8080
CMD ["python", "app.py"]
Enter fullscreen mode Exit fullscreen mode
docker build -t pet-recommendations .
docker run -e GOOGLE_API_KEY=your_key_here -p 8080:8080 pet-recommendations
Enter fullscreen mode Exit fullscreen mode

Key Design Decisions

  • Nyckel for classification — Nyckel offers a pretrained dog breed classifier that is free to use. Rather than building and training a custom model, it made more sense to use a solution that already existed.
  • Prompt composition — Not every user will know their dog's age or activity level, and requiring that information would add unnecessary friction. Optional fields are only added to the prompt when provided, so the app works out of the box without any extra input.
  • Filename sanitization — Uploaded files are renamed with a timestamp and random integer to prevent collisions. Without this, two users uploading a file with the same name would overwrite each other's image.

What I Would Change

1. Async API Calls

The API calls currently run synchronously with no feedback to the user — the page is unresponsive while waiting for both Nyckel and Gemini to respond. I'd restructure this using async Flask to show a loading indicator while the requests are processing.

2. Input Validation on Image Uploads

The app currently accepts any file without checking whether it is actually an image. Validating the file's MIME type before passing it to the classifier would prevent the app from breaking on invalid input like a Word document. A more involved improvement would be adding a check for whether the photo actually contains a dog, rather than returning a best-guess breed for an unrelated image.

3. Uploaded File Cleanup

Every uploaded image is saved to static/uploads/ with nothing to remove them afterward. In production this would become a storage problem over time. A simple fix would be a scheduled job to clear the folder every few days, or clearing it automatically when the application shuts down.


Conclusion

This project was an exercise in combining multiple AI tools to solve a specific problem. Building it gave me hands-on experience integrating third-party APIs and presenting the results through a web interface.

Top comments (0)