AI-Agent-Assisted Mood-Based Movie Recommendation
CineMood started as a machine-learning-based mood-based movie recommendation system that analyzed user input and recommended trending movies from The Movie Database (TMDB). However, it had limitations in dynamically matching moods with real-time trending movies.
With the rise of AI agents, we upgraded CineMood to CineMood2 in this post, making it an LLM-powered system that leverages GPT-4o-mini to enhance movie recommendations.
π― Project Overview
π¬ CineMood β CineMood2: What Changed?
- CineMood (ML-Based Approach)
- Used predefined mood-to-genre mapping.
- Limited adaptability to dynamic trending movies.
- CineMood2 (AI-Agent-Based Approach)
- Uses GPT-4o-mini to extract moods from user input.
- Fetches 100 trending movies from TMDB.
- Uses GPT-4o-mini again to find the 3 most relevant movies based on mood and movie overviews.
- More dynamic trending movies.
β Why ChatGPT Alone Cannot Recommend Trending Movies
While ChatGPT is a powerful language model, it lacks access to real-time trending movies.
- ChatGPT cannot fetch fresh data from TMDB on its own.
- It relies on training data, which may be outdated.
- It does not have access to external APIs like TMDB.
However, GPT-4o-mini can still be used as a tool to analyze moods and rank movies once we fetch the latest trending data from TMDB.
π€ Why GPT-4o-Mini?
It is my personal choice. Any model (e.g., Mistral-7B, Llama2, Claude, etc.) can be used for this project.
π οΈ Technical Steps in CineMood2
1οΈβ£ Extracting Mood from User Input
- When a user enters how they feel, GPT-4o-mini extracts 3 mood words.
- Example:
- User Input: βI feel relaxed and peaceful today.β
- Extracted Moods:
["calm", "peaceful", "serene"]
2οΈβ£ Fetching Trending Movies from TMDB
- Get 100 trending movies for the week.
- Filter out movies that have not been released yet.
- Movies are ranked from the latest to oldest by release date.
3οΈβ£ Matching Movies to Userβs Mood
- GPT-4o-mini analyzes the movie overviews and finds the 3 best matches.
-
It also provides a match explanation for each movie.
- Example Output:
π¬ Movie: "Forrest Gump" π Release Date: 1994-07-06 β Match Reason: This movie embodies warmth, nostalgia, and optimism, matching the user's mood perfectly.
- Example Output:
π How CineMood2 is Built (Using Docker & Streamlit)
πΉ Step 1: Setting Up Streamlit UI (app.py
)
π‘ Goal: Provide a simple user interface where users describe how they feel.
β How it Works:
- The Streamlit UI presents a text box where users type their current mood.
- A button click triggers the recommendation process.
- User input is processed and passed to GPT-4o-mini for mood detection.
π Technical Details:
- The app listens for user input via st.text_area().
- When the user clicks βFind Movies,β the input is passed to detect_mood() from
llm.py
. - The detected mood words are displayed in the UI.
Create app.py
:
import streamlit as st
from llm import detect_mood, get_movies_by_mood
from tmdb_api import fetch_movies
st.set_page_config(
page_title="π¬ Mood-Based Trending Movie Recommendation", layout="centered"
)
st.title("π¬ CineMood2: Get your Mood-based Trending Movies!β‘")
user_mood = st.text_area("π¬ How do you feel right now?", st.session_state.get("user_mood", ""), height=100)
if st.button("Find Movies"):
if user_mood.strip():
with st.spinner("π Analyzing your mood..."):
mood_words = detect_mood(user_mood)
st.success(f"π€ AI Detected Moods: {', '.join(mood_words).title()}")
with st.spinner("π₯ Fetching movies and ranking matches..."):
movies = fetch_movies(60)
recommended_movies = get_movies_by_mood(mood_words, movies)
if recommended_movies:
for movie in recommended_movies:
st.subheader(movie["title"])
st.write(f"π
Release Date: {movie['release_date']}")
st.write(f"π Match Reason: {movie['match_reason']}")
if movie["poster"]:
st.image(movie["poster"], width=200)
st.write(f"π Overview: {movie['overview']}")
st.markdown("---")
else:
st.warning("β οΈ No suitable movie recommendations found.")
else:
st.warning("β οΈ Please enter how you feel to get movie recommendations.")
# Footer Section
st.markdown("**Made by [Thanh Tung Vu](https://thanhtungvudata.github.io/)**")
πΉ Step 2: Using OpenAIβs GPT-4o-mini for Mood Detection (llm.py
)
π‘ Goal: Extract key mood words from user input.
β How it Works:
- The userβs input is sent to GPT-4o-mini via an API call.
- The model returns three mood-related words that best describe the userβs feelings.
π Technical Details:
- detect_mood(user_input) sends the input to GPT-4o-mini via the OpenAI API.
- The model is prompted to return exactly three words in a structured format.
- If an error occurs, a default neutral mood is returned.
Create llm.py
:
import json
import openai
from config import OPENAI_API_KEY
# Initialize OpenAI API client
client = openai.OpenAI(api_key=OPENAI_API_KEY)
def detect_mood(user_input):
"""
Uses GPT-4o-mini to detect mood from user input.
Returns exactly 3 descriptive mood words.
"""
prompt = f"""
Analyze the following user input and determine the three best words to describe the mood.
User input: "{user_input}"
Respond with exactly 3 words, separated by commas.
Example: happy, joyful, excited.
"""
try:
response = client.chat.completions.create(
model="gpt-4o-mini", messages=[{"role": "system", "content": prompt}]
)
mood_words = response.choices[0].message.content.strip().lower().split(", ")
return mood_words if len(mood_words) == 3 else ["neutral", "neutral", "neutral"]
except Exception as e:
print(f"β οΈ Error with OpenAI API in detect_mood: {e}")
return ["neutral", "neutral", "neutral"]
def get_movies_by_mood(mood_words, movies):
"""
Uses GPT-4o-mini to rank movies based on how well their overview matches the detected mood words.
Returns the top 3 movies with match explanations, sorted by release date (latest first).
"""
if not movies:
print("β οΈ No movies available to match moods.")
return []
movie_descriptions = "\n".join(
[f"{i+1}. {m['title']}: {m['overview']}" for i, m in enumerate(movies)]
)
prompt = f"""
You must output only valid JSON and nothing else.
The JSON should be an array of exactly 3 objects.
Each object must have two keys: "index" (an integer) and "match_reason" (a non-empty string).
The user is in a mood described by these words: {", ".join(mood_words)}.
Below are movie descriptions:
{movie_descriptions}
Select the top 3 movies that best match this mood and provide a brief explanation (1-2 sentences) for each.
Respond strictly in JSON format:
[
index,
index,
index
]
"""
try:
response = client.chat.completions.create(
model="gpt-4o-mini", messages=[{"role": "system", "content": prompt}]
)
json_response = response.choices[0].message.content.strip()
ranked_movies = json.loads(json_response)
matched_movies = []
default_explanation = "This movie appears to be a good match based on its emotional tone and themes."
for entry in ranked_movies:
index = entry.get("index", 0) - 1
explanation = entry.get("match_reason", "").strip() or default_explanation
if 0 <= index < len(movies):
matched_movie = movies[index]
matched_movie["match_reason"] = explanation
matched_movies.append(matched_movie)
# Ensure exactly 3 movies are returned by filling with fallbacks if necessary
while len(matched_movies) < 3 and len(movies) >= 3:
fallback = movies[len(matched_movies)]
fallback["match_reason"] = default_explanation
matched_movies.append(fallback)
# Sort the matched movies by release date (latest first)
matched_movies = sorted(
matched_movies, key=lambda x: x["release_date"], reverse=True
)
return matched_movies[:3]
except Exception as e:
print(f"β οΈ Error ranking movies: {e}")
fallback_movies = []
default_explanation = "This movie appears to be a good match based on its emotional tone and themes."
for m in movies[:3]:
m["match_reason"] = default_explanation
fallback_movies.append(m)
fallback_movies = sorted(
fallback_movies, key=lambda x: x["release_date"], reverse=True
)
return fallback_movies
πΉ Step 3: Fetching TMDB Trending Movies (tmdb_api.py
)
π‘ Goal: Get a list of currently trending movies from The Movie Database (TMDB) API.
β How it Works:
- The app fetches 100 trending movies using TMDBβs API.
- Movies without overviews or future releases are filtered out.
- Movies are sorted by release date (newest first).
π Technical Details:
- fetch_movies(max_movies=100) retrieves movies using TMDBβs /trending/movie/week endpoint.
- Each movie entry is checked for:
- A valid release date (must be in the past).
- A non-empty movie overview.
- The results are sorted by release date, with the most recent movies first.
Create tmdb_api.py
:
import datetime
import requests
from config import TMDB_API_KEY
def get_first_day_of_week():
"""Returns the first day (Monday) of the current week."""
today = datetime.date.today()
return today - datetime.timedelta(days=today.weekday())
def fetch_movies(max_movies=100):
"""
Fetch up to `max_movies` trending movies, ensuring only movies with release dates before the first day
of the current week are considered, and that they have non-empty overviews.
Returns the movies sorted by release date (latest first).
"""
movies = []
pages_to_fetch = (max_movies // 20) + 1
first_day_of_week = get_first_day_of_week()
for page in range(1, pages_to_fetch + 1):
url = f"https://api.themoviedb.org/3/trending/movie/week?api_key={TMDB_API_KEY}&language=en-US&page={page}"
try:
response = requests.get(url)
response.raise_for_status()
data = response.json()
for movie in data.get("results", []):
release_date = movie.get("release_date", "9999-12-31")
overview = movie.get("overview", "").strip()
try:
release_date_obj = datetime.datetime.strptime(
release_date, "%Y-%m-%d"
).date()
except ValueError:
continue
if overview and release_date_obj < first_day_of_week:
movies.append(
{
"title": movie["title"],
"overview": overview,
"poster": (
f"https://image.tmdb.org/t/p/w500{movie['poster_path']}"
if movie.get("poster_path")
else None
),
"release_date": release_date,
}
)
if len(movies) >= max_movies:
break
except requests.exceptions.RequestException as e:
print(f"β οΈ Error fetching movies: {e}")
break
return sorted(movies, key=lambda x: x["release_date"], reverse=True)[:max_movies]
πΉ Step 4: Using GPT-4o-mini to Find Best Matches (llm.py)
π‘ Goal: Select the top 3 movies that best match the userβs mood.
β How it Works:
- The 100 trending movies are sent to GPT-4o-mini.
- The model compares movie overviews against the 3 detected mood words.
- Top 3 movies are selected based on relevance, each with a brief explanation.
π Technical Details:
- get_movies_by_mood(mood_words, movies) processes the movies using GPT-4o-mini.
- The model is prompted to return structured JSON, ensuring:
- Three movie indices are selected.
- Each movie has a match explanation.
- If errors occur, the first three movies are used with a default explanation.
πΉ Step 5: Deploying CineMood2 in Docker
π‘ Goal: Ensure consistent deployment and easy sharing across environments.
β How it Works:
- The entire project is containerized using Docker.
- A Dockerfile defines:
- Base image (Python + dependencies).
- Installation of Streamlit, OpenAI API, and TMDB API tools.
- Instructions to run the Streamlit app inside the container.
- The app can now be deployed anywhere, ensuring reproducibility.
π Technical Details:
- Create
config.py
:
import os
from dotenv import load_dotenv
# Load environment variables from .env file
load_dotenv()
# Get API keys
OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
TMDB_API_KEY = os.getenv("TMDB_API_KEY")
- Create
Dockerfile
:
FROM python:3.11-slim
WORKDIR /app
COPY . .
RUN pip install --upgrade pip && pip install -r requirements.txt
EXPOSE 8501
CMD ["streamlit", "run", "/app/app.py", "--server.port=8501", "--server.address=0.0.0.0"]
- Create
docker-compose.yml
:
version: '3.8'
services:
mood-movie-app:
build: .
container_name: mood-movie-app
ports:
- "8501:8501"
restart: always
- Docker Commands:
- Build:
docker-compose up --build
- Re-Run:
docker-compose down
, thendocker-compose up --build
- Build:
π¦Tech Stack
- AI & Backend: GPT-4o-mini, TMDB API, Python
- Frontend: Streamlit
- Development & Deployment: Docker, VSCode, Flake8, pytest
π Results and Live Demo
The final web app delivers mood-based movie recommendations in just a second, with fresh content every week.
You can try it here:π CineMood Live App on Hugging Face Spaces
π¬ Try CineMood Now!
π Conclusion
CineMood2 provides an AI-powered, mood-based movie recommendation system that seamlessly integrates GPT-4o-mini, TMDB trending movies, and Docker deployment.
π‘ Key Advantages:
- Uses real-time trending data instead of pre-trained ChatGPT knowledge.
- Optimized for cost (GPT-4o-mini) and performance (Cloud API instead of local LLM).
- Fully containerized for easy deployment and scalability.
π Next Steps:
- Improve user feedback collection for better recommendations.
- Add multi-language support for global users.
The code of this project is available here.
For further inquiries or collaboration, please contact me at my email.