Most recommender systems stop at a score. In this guide we will build a hybrid movie recommender that uses collaborative filtering to surface candidates and an LLM on Oxlo.ai to rank them and generate concise explanations. It is useful for any engineer who needs transparent, reasoning-aware recommendations without training a separate model.
What you'll need
- Python 3.10 or newer
- An Oxlo.ai API key from https://portal.oxlo.ai
- The OpenAI SDK:
pip install openai - pandas and scikit-learn:
pip install pandas scikit-learn
Step 1: Build the collaborative filtering layer
I start with a small synthetic ratings matrix so the example is fully self-contained. We compute user-user cosine similarity to find taste neighbors.
import numpy as np
import pandas as pd
from sklearn.metrics.pairwise import cosine_similarity
np.random.seed(42)
ratings = np.random.randint(0, 6, size=(10, 8))
movies = [
"The Matrix", "Inception", "Toy Story", "Pulp Fiction",
"Forrest Gump", "The Godfather", "Spirited Away", "Blade Runner 2049"
]
df = pd.DataFrame(ratings, columns=movies)
user_sim = cosine_similarity(df)
print("User similarity shape:", user_sim.shape)
Step 2: Retrieve candidates
Given a target user, we look at the top three most similar users and collect movies they rated four or higher that the target has not seen. These become our candidate pool.
def get_candidates(target_user, df, user_sim, top_n_similar=3, top_n_items=5):
sim_scores = user_sim[target_user]
similar_users = np.argsort(sim_scores)[::-1][1:top_n_similar + 1]
seen = set(df.columns[df.iloc[target_user] > 0])
candidates = {}
for u in similar_users:
for movie in df.columns:
if movie not in seen and df.loc[u, movie] >= 4:
weight = sim_scores[u] * df.loc[u, movie]
candidates[movie] = candidates.get(movie, 0) + weight
sorted_candidates = sorted(candidates.items(), key=lambda x: x[1], reverse=True)
return sorted_candidates[:top_n_items], similar_users
candidates, neighbors = get_candidates(0, df, user_sim)
print("Candidates:", candidates)
print("Neighbors:", neighbors)
Step 3: Write the system prompt
The LLM needs guardrails. I keep the prompt tight: it must return valid JSON, rank by the provided confidence scores, and never hallucinate titles.
SYSTEM_PROMPT = """You are a movie recommender engine. Your job is to rank candidate movies for a user and explain each recommendation briefly.
You will receive:
1. A list of movies the user already rated.
2. A list of candidate movies from collaborative filtering, with confidence scores.
3. A list of similar users and their ratings.
Instructions:
- Return a JSON array of up to 3 recommendations.
- Each object must have: "movie" (string), "rank" (integer starting at 1), "reason" (string, max 20 words).
- Base your ranking on the candidate confidence scores and the taste overlap with similar users.
- Be concise and specific. Do not hallucinate movies not in the candidate list.
Example output:
[
{"movie": "Inception", "rank": 1, "reason": "Liked by users with similar taste in sci-fi."}
]
"""
Step 4: Rank with Oxlo.ai
Now we wire the pieces together. We format the user history, neighbors, and candidates into a single prompt and call Oxlo.ai through the OpenAI-compatible endpoint. I use llama-3.3-70b because it follows structured instructions reliably at low temperature.
from openai import OpenAI
import json
client = OpenAI(base_url="https://api.oxlo.ai/v1", api_key="YOUR_OXLO_API_KEY")
def recommend(target_user, df, user_sim):
candidates, neighbors = get_candidates(target_user, df, user_sim)
history = [f"{m}: {r}/5" for m, r in zip(df.columns, df.iloc[target_user]) if r > 0]
neighbors_str = []
for u in neighbors:
rated = [f"{m}({df.loc[u, m]})" for m in df.columns if df.loc[u, m] > 0]
neighbors_str.append(f"User {u}: " + ", ".join(rated))
cand_str = "\n".join([f"{m} (score: {s:.2f})" for m, s in candidates])
user_message = (
f"User {target_user} history:\n"
+ "\n".join(history)
+ "\n\nSimilar users:\n"
+ "\n".join(neighbors_str)
+ "\n\nCandidate movies:\n"
+ cand_str
+ "\n\nReturn the JSON array now."
)
response = client.chat.completions.create(
model="llama-3.3-70b",
messages=[
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": user_message},
],
temperature=0.3,
max_tokens=512,
)
raw = response.choices[0].message.content
if "
```" in raw:
raw = raw.split("```
")[1].replace("json", "").strip()
return json.loads(raw)
recommendations = recommend(0, df, user_sim)
print(json.dumps(recommendations, indent=2))
Run it
Here is a complete script that runs the full pipeline for user 0. The collaborative filtering layer narrows the field to a handful of high-confidence candidates, and the LLM on Oxlo.ai produces the final ranked list with human-readable reasons.
if __name__ == "__main__":
recs = recommend(0, df, user_sim)
for r in recs:
print(f"{r['rank']}. {r['movie']}")
print(f" Why: {r['reason']}")
Example output:
1. Inception
Why: Highly rated by your closest taste neighbors who also enjoy sci-fi.
2. The Godfather
Why: Users with similar dramatic preferences gave this top marks.
3. Spirited Away
Why: Strong overlap with neighbors who rate animation highly.
Next steps
Swap the synthetic matrix for a real dataset like MovieLens 1M, and replace the user-user similarity with matrix factorization for better scalability. If you want to add content-based signals, pipe movie descriptions through an embedding model on Oxlo.ai and blend those vectors into the candidate score before the LLM ranking step.
Top comments (0)