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
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"
})
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
}
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), ...]
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
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)