|
| 1 | +#!/usr/bin/python3 |
| 2 | +""" |
| 3 | +Movie Bot - Recommend movies based on user preferences |
| 4 | +
|
| 5 | +The job of movie bot is to read the history of previous movies recommend and then suggestion a new |
| 6 | +movie based on the user's preferences. The bot will also store the new movie recommendation in the |
| 7 | +database for future reference. |
| 8 | +
|
| 9 | + * The bot will identify the genre of the movie it recommends from a set of predefined genres from |
| 10 | + the database. |
| 11 | + * The bot will use the LLM to generate the movie recommendation. |
| 12 | + * The bot will store the new movie recommendation in the database. |
| 13 | + * The bot will return the movie recommendation to the user along with the genre. |
| 14 | + # The bot will attempt to recommend a movie that is not already in the database and |
| 15 | + will try to vary the genre of the movie recommendation. |
| 16 | +
|
| 17 | +Author: Jason A. Cox |
| 18 | +2 Feb 2025 |
| 19 | +https://github.com/jasonacox/TinyLLM |
| 20 | +
|
| 21 | +""" |
| 22 | + |
| 23 | +import openai |
| 24 | +import sqlite3 |
| 25 | +import os |
| 26 | +import time |
| 27 | +import datetime |
| 28 | +import random |
| 29 | + |
| 30 | +# Version |
| 31 | +VERSION = "v0.0.1" |
| 32 | + |
| 33 | +# Configuration Settings |
| 34 | +api_key = os.environ.get("OPENAI_API_KEY", "open_api_key") # Required, use bogus string for Llama.cpp |
| 35 | +api_base = os.environ.get("OPENAI_API_BASE", "http://localhost:8000/v1") # Required, use https://api.openai.com for OpenAI |
| 36 | +mymodel = os.environ.get("LLM_MODEL", "models/7B/gguf-model.bin") # Pick model to use e.g. gpt-3.5-turbo for OpenAI |
| 37 | +DATABASE = os.environ.get("DATABASE", "moviebot.db") # Database file to store movie data |
| 38 | +DEBUG = os.environ.get("DEBUG", "false").lower() == "true" # Set to True to enable debug mode |
| 39 | +TEMPERATURE = float(os.environ.get("TEMPERATURE", 0.7)) # LLM temperature |
| 40 | +MESSAGE_FILE = os.environ.get("MESSAGE_FILE", "message.txt") # File to store the message |
| 41 | +ABOUT_ME = os.environ.get("ABOUT_ME", |
| 42 | + "We love movies! Action, adventure, sci-fi and feel good movies are our favorites.") # About me text |
| 43 | + |
| 44 | +#DEBUG = True |
| 45 | + |
| 46 | +# Debugging functions |
| 47 | +def log(text): |
| 48 | + # Print to console |
| 49 | + if DEBUG: |
| 50 | + print(f"INFO: {text}") |
| 51 | + |
| 52 | +def error(text): |
| 53 | + # Print to console |
| 54 | + print(f"ERROR: {text}") |
| 55 | + |
| 56 | +class MovieDatabase: |
| 57 | + def __init__(self, db_name=DATABASE): |
| 58 | + self.db_name = db_name |
| 59 | + self.conn = None |
| 60 | + |
| 61 | + def connect(self): |
| 62 | + self.conn = sqlite3.connect(self.db_name) |
| 63 | + return self.conn |
| 64 | + |
| 65 | + def create(self): |
| 66 | + self.connect() |
| 67 | + c = self.conn.cursor() |
| 68 | + # Create table to store movies |
| 69 | + c.execute('''CREATE TABLE IF NOT EXISTS movies (title text, genre text, date_recommended text)''') |
| 70 | + self.conn.commit() |
| 71 | + # Create table to store genres (make them unique) |
| 72 | + c.execute('''CREATE TABLE IF NOT EXISTS genres (genre text UNIQUE)''') |
| 73 | + self.conn.commit() |
| 74 | + self.conn.close() |
| 75 | + |
| 76 | + def destroy(self): |
| 77 | + self.connect() |
| 78 | + c = self.conn.cursor() |
| 79 | + # Drop tables |
| 80 | + c.execute('''DROP TABLE IF EXISTS movies''') |
| 81 | + self.conn.commit() |
| 82 | + c.execute('''DROP TABLE IF EXISTS genres''') |
| 83 | + self.conn.commit() |
| 84 | + self.conn.close() |
| 85 | + |
| 86 | + def insert_movie(self, title, genre, date_recommended=None): |
| 87 | + if date_recommended is None: |
| 88 | + date_recommended = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") |
| 89 | + self.connect() |
| 90 | + c = self.conn.cursor() |
| 91 | + c.execute("INSERT INTO movies VALUES (?, ?, ?)", (title, genre, date_recommended)) |
| 92 | + self.conn.commit() |
| 93 | + self.conn.close() |
| 94 | + |
| 95 | + def select_all_movies(self, sort_by='date_recommended'): |
| 96 | + self.connect() |
| 97 | + c = self.conn.cursor() |
| 98 | + c.execute(f"SELECT * FROM movies ORDER BY {sort_by}") |
| 99 | + rows = c.fetchall() |
| 100 | + self.conn.close() |
| 101 | + # return list of movie titles |
| 102 | + m = [row[0] for row in rows] |
| 103 | + return m |
| 104 | + |
| 105 | + def select_movies_by_genre(self, genre): |
| 106 | + self.connect() |
| 107 | + c = self.conn.cursor() |
| 108 | + c.execute(f"SELECT * FROM movies WHERE genre = ?", (genre,)) |
| 109 | + rows = c.fetchall() |
| 110 | + self.conn.close() |
| 111 | + # return list of movie titles |
| 112 | + m = [row[0] for row in rows] |
| 113 | + return m |
| 114 | + |
| 115 | + def delete_all_movies(self): |
| 116 | + self.connect() |
| 117 | + c = self.conn.cursor() |
| 118 | + c.execute("DELETE FROM movies") |
| 119 | + self.conn.commit() |
| 120 | + self.conn.close() |
| 121 | + |
| 122 | + def delete_movie_by_title(self, title): |
| 123 | + self.connect() |
| 124 | + c = self.conn.cursor() |
| 125 | + c.execute("DELETE FROM movies WHERE title = ?", (title,)) |
| 126 | + self.conn.commit() |
| 127 | + self.conn.close() |
| 128 | + |
| 129 | + def insert_genre(self, genre): |
| 130 | + self.connect() |
| 131 | + c = self.conn.cursor() |
| 132 | + c.execute("INSERT OR IGNORE INTO genres VALUES (?)", (genre,)) |
| 133 | + self.conn.commit() |
| 134 | + self.conn.close() |
| 135 | + |
| 136 | + def select_genres(self): |
| 137 | + self.connect() |
| 138 | + c = self.conn.cursor() |
| 139 | + c.execute("SELECT * FROM genres") |
| 140 | + rows = c.fetchall() |
| 141 | + self.conn.close() |
| 142 | + # convert payload into a list of genres |
| 143 | + g = [row[0] for row in rows] |
| 144 | + return g |
| 145 | + |
| 146 | +# Initialize the database |
| 147 | +db = MovieDatabase() |
| 148 | +#db.destroy() # Clear the database |
| 149 | +db.create() # Set up the database if it doesn't exist |
| 150 | + |
| 151 | +# Load genres |
| 152 | +genres = db.select_genres() |
| 153 | +if len(genres) == 0: |
| 154 | + for genre in ['action', 'fantasy', 'comedy', 'drama', 'horror', 'romance', |
| 155 | + 'sci-fi', 'christmas', 'musical', 'thriller', 'mystery', |
| 156 | + 'animated', 'western', 'documentary', 'adventure', 'family']: |
| 157 | + db.insert_genre(genre) |
| 158 | + genres = db.select_genres() |
| 159 | + |
| 160 | +# Load movies |
| 161 | +movies = db.select_all_movies() |
| 162 | +if len(movies) == 0: |
| 163 | + # Load some sample movies |
| 164 | + db.insert_movie("Home Alone", "christmas") |
| 165 | + db.insert_movie("Sound of Music", "musical") |
| 166 | + db.insert_movie("Die Hard", "action") |
| 167 | + db.insert_movie("Sleepless in Seattle", "romance") |
| 168 | + db.insert_movie("The Game", "thriller") |
| 169 | + db.insert_movie("Toy Story", "animated") |
| 170 | + db.insert_movie("Sherlock Holmes", "mystery") |
| 171 | + db.insert_movie("Airplane", "comedy") |
| 172 | + db.insert_movie("Raiders of the Lost Ark", "action") |
| 173 | + db.insert_movie("A Bug's Life", "drama") |
| 174 | + |
| 175 | +# Test LLM |
| 176 | +while True: |
| 177 | + log("Testing LLM...") |
| 178 | + try: |
| 179 | + log(f"Using openai library version {openai.__version__}") |
| 180 | + log(f"Connecting to OpenAI API at {api_base} using model {mymodel}") |
| 181 | + llm = openai.OpenAI(api_key=api_key, base_url=api_base) |
| 182 | + # Get models |
| 183 | + models = llm.models.list() |
| 184 | + # build list of models |
| 185 | + model_list = [model.id for model in models.data] |
| 186 | + log(f"LLM: Models available: {model_list}") |
| 187 | + if len(models.data) == 0: |
| 188 | + log("LLM: No models available - check your API key and endpoint.") |
| 189 | + raise Exception("No models available") |
| 190 | + if not mymodel in model_list: |
| 191 | + log(f"LLM: Model {mymodel} not found in models list.") |
| 192 | + if len(model_list) == 1: |
| 193 | + log("LLM: Switching to default model") |
| 194 | + mymodel = model_list[0] |
| 195 | + else: |
| 196 | + log(f"LLM: Unable to find model {mymodel} in models list.") |
| 197 | + raise Exception(f"Model {mymodel} not found") |
| 198 | + log(f"LLM: Using model {mymodel}") |
| 199 | + # Test LLM |
| 200 | + llm.chat.completions.create( |
| 201 | + model=mymodel, |
| 202 | + stream=False, |
| 203 | + temperature=TEMPERATURE, |
| 204 | + messages=[{"role": "user", "content": "Hello"}], |
| 205 | + ) |
| 206 | + break |
| 207 | + except Exception as e: |
| 208 | + log("OpenAI API Error: %s" % e) |
| 209 | + log(f"Unable to connect to OpenAI API at {api_base} using model {mymodel}.") |
| 210 | + log("Sleeping 10 seconds...") |
| 211 | + time.sleep(10) |
| 212 | + |
| 213 | +# LLM Function to ask the chatbot a question |
| 214 | +def ask_chatbot(prompt): |
| 215 | + log(f"LLM: Asking chatbot: {prompt}") |
| 216 | + response = llm.chat.completions.create( |
| 217 | + model=mymodel, |
| 218 | + stream=False, |
| 219 | + temperature=TEMPERATURE, |
| 220 | + messages=[{"role": "user", "content": prompt}], |
| 221 | + ) |
| 222 | + log(f"LLM: Response: {response.choices[0].message.content.strip()}\n") |
| 223 | + return response.choices[0].message.content.strip() |
| 224 | + |
| 225 | +# Function to recommend a movie |
| 226 | +def recommend_movie(): |
| 227 | + # Get list of movies but limit it to last 12 |
| 228 | + movies = db.select_all_movies(sort_by='date_recommended')[-12:] |
| 229 | + # Get list of genres |
| 230 | + genres = db.select_genres() |
| 231 | + # Randomly mix up the elements in the list |
| 232 | + random.shuffle(genres) |
| 233 | + |
| 234 | + # Get today's date in format: Month Day, Year |
| 235 | + today = datetime.datetime.now().strftime("%B %d, %Y") |
| 236 | + |
| 237 | + prompt = f"""You are a expert movie bot that recommends movies based on user preferences. |
| 238 | + Today is {today}, in case it is helpful in suggesting a movie. |
| 239 | + Provide variety and interest. |
| 240 | + Do no repeat previous recommendations. |
| 241 | + You have recommended the following movies in the past: |
| 242 | + {movies} |
| 243 | + You have the following genres available: |
| 244 | + {genres} |
| 245 | + {ABOUT_ME} |
| 246 | + Please recommend a new movie to watch. Give interesting details about the movie. |
| 247 | + """ |
| 248 | + |
| 249 | + # Ask the chatbot for a movie recommendation |
| 250 | + recommendation = ask_chatbot(prompt).strip() |
| 251 | + |
| 252 | + # Save the movie recommendation to file |
| 253 | + with open(MESSAGE_FILE, "w") as f: |
| 254 | + f.write(recommendation) |
| 255 | + |
| 256 | + # Ask the chatbot to provide just one move and the title only |
| 257 | + prompt = f"""About me: {ABOUT_ME} |
| 258 | + |
| 259 | + Recommendation: |
| 260 | + {recommendation} |
| 261 | +
|
| 262 | + Based on the above recommendation, select the best movie for me. List the movie title only: |
| 263 | + """ |
| 264 | + movie_recommendation = ask_chatbot(prompt).strip() |
| 265 | + |
| 266 | + # Ask the chatbot what type of genre the movie is, provide list of genres |
| 267 | + prompt = f"""You are a expert movie bot that recommends movies based on user preferences. |
| 268 | + You have the following genres available: |
| 269 | + {genres} |
| 270 | + |
| 271 | + Based on that list, what genre is the movie "{movie_recommendation}"? List the genre only: |
| 272 | + """ |
| 273 | + genre = ask_chatbot(prompt).strip().lower() |
| 274 | + |
| 275 | + # Store the movie recommendation |
| 276 | + db.insert_movie(movie_recommendation, genre) |
| 277 | + return movie_recommendation, genre |
| 278 | + |
| 279 | +# Main |
| 280 | +if __name__ == "__main__": |
| 281 | + log(f"Movie Bot {VERSION} - Recommends movies based on user preferences and history.") |
| 282 | + movie, genre = recommend_movie() |
| 283 | + output = f"Movie Bot recommends the {genre} movie: {movie}" |
| 284 | + print(output) |
0 commit comments