MovieApp / src /recommendation_system.py
marijamaneva
Add Hugging Face Spaces deployment config
a63a593
Raw
History Blame Contribute Delete
11.2 kB
import chromadb
from chromadb.utils import embedding_functions
import json
import os
import re
from langchain_openai import ChatOpenAI
from langchain.prompts import PromptTemplate
from langchain.chains import LLMChain
from langchain.memory import ConversationBufferMemory
from tmdb_api_helper import TMDBHelper
class MovieRecommender:
def __init__(self):
# Initialize ChromaDB client
self.chroma_client = chromadb.PersistentClient(path="data/embeddings")
embedding_function = embedding_functions.SentenceTransformerEmbeddingFunction(
model_name='all-MiniLM-L6-v2'
)
try:
self.collection = self.chroma_client.get_collection(
name="movie_collection",
embedding_function=embedding_function,
)
except Exception:
self._initialize_database()
self.collection = self.chroma_client.get_collection(
name="movie_collection",
embedding_function=embedding_function,
)
# Initialize the language model and memory
self.memory = ConversationBufferMemory(
memory_key="chat_history",
input_key="human_input"
)
self.llm = ChatOpenAI(model="gpt-3.5-turbo", temperature=0.7, max_tokens=1024)
# User preferences storage
self.user_preferences_file = "data/user_preferences.json"
self.user_preferences = self._load_user_preferences()
# Initialize TMDB helper
self.tmdb_helper = TMDBHelper()
# Setup prompt templates
self._setup_prompts()
def _initialize_database(self):
"""Download MovieLens data and build the ChromaDB collection on first run."""
print("First run: downloading data and building movie database (this takes a few minutes)...")
import os
sys_path_entry = os.path.dirname(__file__)
import sys
if sys_path_entry not in sys.path:
sys.path.insert(0, sys_path_entry)
from movie_data_preparation import download_and_prepare_movielens
from vector_database_setup import prepare_movie_descriptions, create_vector_database
import ast
movies_df = download_and_prepare_movielens()
movies_df['genres'] = movies_df['genres'].apply(
lambda g: ast.literal_eval(g) if isinstance(g, str) else g
)
movies_df = prepare_movie_descriptions(movies_df)
create_vector_database(movies_df)
print("Movie database ready.")
def _setup_prompts(self):
self.recommendation_template = """
You are MovieMind, a friendly movie recommendation assistant.
Previous conversation:
{chat_history}
User query: {human_input}
Based on the query, these are the most relevant movies from our database:
{movie_results}
User preferences: {user_preferences}
Your task:
1. Analyze the user query and the provided movie results.
2. Consider the user's preferences if available.
3. Provide personalized movie recommendations from the list above.
4. For each recommendation, include:
- Title: (Movie Title)
- Year: (Year)
- Genre: (Genre)
- Director: (Director)
- Main actors: (Actors)
- Short plot summary
- Reason why you recommend it.
5. Always provide this information in the exact format shown above.
6. Make sure each movie is clearly separated from others.
7. If appropriate, ask a follow-up question to refine future recommendations.
Give at least 5 movie recommendations, not just a single movie.
Respond in a conversational, helpful tone. Avoid phrases like "based on your query" or "personalized recommendations".
AI Assistant:
"""
self.recommendation_chain = LLMChain(
llm=self.llm,
prompt=PromptTemplate(
input_variables=["chat_history", "human_input", "movie_results", "user_preferences"],
template=self.recommendation_template
),
memory=self.memory,
verbose=False
)
self.general_template = """
You are MovieMind, a friendly movie recommendation assistant.
Previous conversation:
{chat_history}
Human: {human_input}
Respond in a conversational, helpful tone.
If the user is asking about movies or for recommendations,
suggest they try asking more specifically about genres, actors, or the type of movie they're looking for.
AI Assistant:
"""
self.general_chain = LLMChain(
llm=self.llm,
prompt=PromptTemplate(
input_variables=["chat_history", "human_input"],
template=self.general_template
),
memory=self.memory,
verbose=False
)
def _load_user_preferences(self):
"""Load user preferences from file"""
if os.path.exists(self.user_preferences_file):
with open(self.user_preferences_file, 'r') as f:
return json.load(f)
else:
os.makedirs('data', exist_ok=True)
with open(self.user_preferences_file, 'w') as f:
json.dump({}, f)
return {}
def _save_user_preferences(self):
"""Save user preferences to file"""
with open(self.user_preferences_file, 'w') as f:
json.dump(self.user_preferences, f, indent=2)
""""
def update_preferences(self, user_id, movie_id, liked=True):
if user_id not in self.user_preferences:
self.user_preferences[user_id] = {"favorites": []}
prefs = self.user_preferences[user_id]
self._save_user_preferences()
"""
def get_response(self, user_id, message):
"""Generate a recommendation or general response based on user input"""
# Step 1: Search for relevant movies
results = self.collection.query(
query_texts=[message],
n_results=5
)
movie_results = results.get("documents", [[]])[0]
# Step 2: Prepare movie descriptions
movie_descriptions = ""
for movie in movie_results:
if isinstance(movie, str) and movie.strip():
try:
movie_info = json.loads(movie)
except json.JSONDecodeError:
continue
else:
movie_info = movie
if movie_info:
movie_descriptions += f"\nTitle: {movie_info.get('title', 'Unknown')}\n"
movie_descriptions += f"Year: {movie_info.get('year', 'Unknown')}\n"
movie_descriptions += f"Genre: {movie_info.get('genre', 'Unknown')}\n"
movie_descriptions += f"Director: {movie_info.get('director', 'Unknown')}\n"
actors = movie_info.get('actors', [])
if isinstance(actors, list):
movie_descriptions += f"Actors: {', '.join(actors)}\n"
else:
movie_descriptions += f"Actors: {actors}\n"
movie_descriptions += f"Plot: {movie_info.get('plot', 'No plot available')}\n\n"
# Step 3: Load user preferences
user_prefs = self.user_preferences.get(user_id, {})
favorites = user_prefs.get("favorites", [])
if favorites:
user_preferences_string = (
f"Favorite movies: {', '.join(favorites)}."
)
else:
user_preferences_string = "No preferences recorded yet."
# Step 4: Create final response
try:
response = self.recommendation_chain.invoke({
"chat_history": self.memory.buffer,
"human_input": message,
"movie_results": movie_descriptions,
"user_preferences": user_preferences_string
})
# Extract the text response
if isinstance(response, dict) and "text" in response:
response = response["text"]
except Exception as e:
print(f"Error generating recommendation: {e}")
# Fallback to general response
try:
response = self.general_chain.invoke({
"chat_history": self.memory.buffer,
"human_input": message
})
if isinstance(response, dict) and "text" in response:
response = response["text"]
except Exception as e2:
print(f"Error generating general response: {e2}")
response = "I'm having trouble generating a recommendation right now. Could you try again or ask in a different way?"
# Step 5: Process the response to add poster data
processed_response = self.process_response_with_posters(response)
return processed_response
def process_response_with_posters(self, response):
"""Process the response to add movie poster data"""
# Split the response into paragraphs
paragraphs = response.split("\n\n")
result = ""
# Process each paragraph
for paragraph in paragraphs:
# Skip empty paragraphs
if not paragraph.strip():
continue
# Check if this paragraph contains movie information
if "Title:" in paragraph or re.search(r'\b\(\d{4}\)\b', paragraph):
# Extract movie title
title_match = re.search(r"Title:\s*(.*?)(?:\n|$)", paragraph)
if not title_match:
# Try to find title in format "Movie Title (Year)"
title_match = re.search(r"(.*?)\s*\(\d{4}\)", paragraph)
if title_match:
movie_title = title_match.group(1).strip()
# Extract year if available
year_match = re.search(r"Year:\s*(\d{4})", paragraph)
year = year_match.group(1) if year_match else None
if not year:
# Try to find year in format "Movie Title (Year)"
year_match = re.search(r"\((\d{4})\)", paragraph)
year = year_match.group(1) if year_match else None
# Get poster URL
poster_url = self.tmdb_helper.get_poster_url(movie_title, year)
# Add poster URL to the response
if poster_url:
result += paragraph + f"\n[POSTER_URL: {poster_url}]\n\n"
else:
result += paragraph + "\n\n"
else:
result += paragraph + "\n\n"
else:
result += paragraph + "\n\n"
return result