DEV Community

Esther Studer
Esther Studer

Posted on

I Built an AI That Recommends Therapy Pets — Here's What I Learned (and the Code)

We've all heard about therapy dogs. But what if you're allergic? Or live in a small apartment? Or just... really vibe with guinea pigs?

That was the problem I set out to solve. The result was an AI-powered pet therapy matching system. Here's the full tech breakdown — and a few things that surprised me.


The Core Problem

Pet therapy is genuinely effective for anxiety, loneliness, and depression. Studies back it up. But "just get a dog" is terrible advice for:

  • Apartment dwellers
  • People with allergies
  • Those with limited mobility
  • Anyone who's never owned a pet and has zero idea what they're getting into

The matching problem is actually fascinating from an ML perspective: you're trying to align human emotional needs, lifestyle constraints, and animal behavioral profiles — three completely different feature spaces.


The Architecture

User Input (survey)
        ↓
  Feature Extraction
        ↓
  Embedding Layer (sentence-transformers)
        ↓
  Similarity Scoring against Animal Profiles
        ↓
  Ranked Recommendations + Explanations
        ↓
  LLM-generated personalized summary
Enter fullscreen mode Exit fullscreen mode

Simple. But the devil is in each layer.


Step 1: The Survey → Feature Vector

We don't ask "what pet do you want?" (everyone says dog). We ask behavioral and lifestyle questions:

from sentence_transformers import SentenceTransformer
import numpy as np

model = SentenceTransformer('all-MiniLM-L6-v2')

def encode_user_profile(survey_responses: dict) -> np.ndarray:
    """
    Convert free-text + structured survey answers into a dense vector.
    """
    # Combine all responses into a natural-language description
    profile_text = f"""
    Activity level: {survey_responses['activity_level']}.
    Living situation: {survey_responses['living_situation']}.
    Primary emotional need: {survey_responses['emotional_need']}.
    Time available daily: {survey_responses['daily_time_hours']} hours.
    Experience with animals: {survey_responses['experience']}.
    Allergies or restrictions: {survey_responses.get('restrictions', 'none')}.
    """

    return model.encode(profile_text)

# Example
user = encode_user_profile({
    "activity_level": "low, mostly sedentary",
    "living_situation": "small apartment, no yard",
    "emotional_need": "companionship and stress relief",
    "daily_time_hours": 1.5,
    "experience": "never owned a pet",
    "restrictions": "mild cat allergy"
})
Enter fullscreen mode Exit fullscreen mode

Why sentence-transformers over a rule-based system? Because humans describe themselves weirdly. "I work from home 80% of the time" and "I'm home most days" should map to the same feature space. Embeddings handle this naturally.


Step 2: The Animal Profile Database

This is where we encode animal behavioral data the same way:

animal_profiles = [
    {
        "id": "rabbit_holland_lop",
        "name": "Holland Lop Rabbit",
        "description": """
        Quiet, affectionate, and apartment-friendly. Doesn't require outdoor walks.
        Hypoallergenic option for mild allergy sufferers. Responds well to gentle 
        handling and routine. Ideal for calm, indoor environments. Moderate time 
        commitment — about 1-2 hours of interaction daily is ideal.
        """
    },
    {
        "id": "guinea_pig_pair",
        "name": "Guinea Pig (bonded pair)",
        "description": """
        Social, vocal, low-maintenance. Perfect for small spaces. No allergy concerns 
        for most people. Highly responsive to their owners — will 'wheek' when they 
        hear you. Great for emotional support without high physical demands.
        """
    },
    # ... more profiles
]

# Pre-compute embeddings for all animal profiles
animal_vectors = {
    p["id"]: model.encode(p["description"])
    for p in animal_profiles
}
Enter fullscreen mode Exit fullscreen mode

Step 3: Cosine Similarity Scoring

from sklearn.metrics.pairwise import cosine_similarity

def rank_recommendations(user_vector: np.ndarray, animal_vectors: dict) -> list:
    scores = []

    for animal_id, animal_vec in animal_vectors.items():
        sim = cosine_similarity(
            user_vector.reshape(1, -1),
            animal_vec.reshape(1, -1)
        )[0][0]
        scores.append((animal_id, float(sim)))

    # Sort by similarity descending
    return sorted(scores, key=lambda x: x[1], reverse=True)

top_matches = rank_recommendations(user, animal_vectors)
# → [("guinea_pig_pair", 0.87), ("rabbit_holland_lop", 0.82), ...]
Enter fullscreen mode Exit fullscreen mode

Step 4: LLM-Generated Explanations

The raw similarity score means nothing to a user. We pass the top matches to an LLM to generate a warm, personalized explanation:

import anthropic

client = anthropic.Anthropic()

def generate_explanation(user_survey: dict, top_match: dict) -> str:
    prompt = f"""
    A person with the following profile:
    {user_survey}

    Has been matched with: {top_match['name']}
    Match reason (technical): {top_match['description']}

    Write a warm, 2-3 sentence explanation of WHY this pet is a great match for them.
    Be specific to their situation. Avoid generic phrases.
    """

    message = client.messages.create(
        model="claude-3-5-haiku-20241022",
        max_tokens=200,
        messages=[{"role": "user", "content": prompt}]
    )

    return message.content[0].text
Enter fullscreen mode Exit fullscreen mode

This is the piece that makes users go "wow, it really gets me" — even though the underlying logic is just cosine similarity on text embeddings.


What Surprised Me

1. The "experience" feature matters more than I expected.

First-time pet owners consistently matched poorly with high-maintenance animals even when lifestyle fit was good. I had to add a soft penalty for mismatched experience levels.

2. Allergy handling is genuinely hard.

Allergy severity is non-linear. A "mild" cat allergy might be fine with a Siberian cat (lower Fel d 1 protein) but terrible with a Persian. I ended up adding a separate allergy-screening layer before the main matching.

3. Users lie on surveys (unconsciously).

People say they have 2 hours/day but actually mean 30 minutes. The solution? Add a calibration question: "How many hours do you spend on [comparable hobby] per week?" Cross-referencing revealed the actual available time.


Results After 3 Months

  • 78% of matched users reported being "very satisfied" with their recommendation at 30-day follow-up
  • Average session length: 4.2 minutes (users actually engage with the explanations)
  • Most surprising top match: Axolotls. Who knew they'd be such a good anxiety companion for people who "just want to watch something calming"?

Try It Live

If you want to see this in action — or if you're genuinely curious what your ideal therapy animal is — the live matching system is at mypettherapist.com. Free to use, no signup required.

The codebase uses Python + FastAPI on the backend with React on the front. Happy to share more implementation details in the comments.


TL;DR

  • Use sentence-transformers to embed both user profiles and animal behavioral descriptions into the same vector space
  • Cosine similarity does the matching heavy lifting
  • LLM-generated explanations are what actually make users trust the result
  • Allergy handling and experience calibration are underrated complexity sources

What's the weirdest AI matching problem you've tackled? Drop it in the comments — genuinely curious.

Top comments (0)