Esther StuderWe've all heard about therapy dogs. But what if you're allergic? Or live in a small apartment? Or...
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.
Pet therapy is genuinely effective for anxiety, loneliness, and depression. Studies back it up. But "just get a dog" is terrible advice for:
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.
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.
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.
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
}
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), ...]
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.
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.
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.
What's the weirdest AI matching problem you've tackled? Drop it in the comments — genuinely curious.