Spaces:
Sleeping
Sleeping
Merge branch 'master' into main
Browse files- .gitignore +3 -0
- Dockerfile +20 -0
- agent.py +70 -0
- app.py +99 -0
- functions.py +68 -0
- requirements.txt +6 -0
- static/css/style.css +94 -0
- static/js/script.js +62 -0
- templates/base.html +27 -0
- templates/cast.html +12 -0
- templates/genres.html +11 -0
- templates/index.html +45 -0
- templates/query.html +23 -0
- tools.py +251 -0
.gitignore
ADDED
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
1 |
+
__pycache__/
|
2 |
+
.env
|
3 |
+
|
Dockerfile
ADDED
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Use an official Python runtime as a parent image
|
2 |
+
FROM python:3.8-slim-buster
|
3 |
+
|
4 |
+
# Set the working directory in the container to /app
|
5 |
+
WORKDIR /app
|
6 |
+
|
7 |
+
# Copy the current directory contents into the container at /app
|
8 |
+
ADD . /app
|
9 |
+
|
10 |
+
# Install any needed packages specified in requirements.txt
|
11 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
12 |
+
|
13 |
+
# Make port 7860 available to the world outside this container
|
14 |
+
EXPOSE 7860
|
15 |
+
|
16 |
+
# Define environment variable
|
17 |
+
ENV NAME World
|
18 |
+
|
19 |
+
# Run app.py when the container launches
|
20 |
+
CMD ["python", "app.py"]
|
agent.py
ADDED
@@ -0,0 +1,70 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
|
2 |
+
import re
|
3 |
+
import json
|
4 |
+
|
5 |
+
import functools
|
6 |
+
import functions
|
7 |
+
import torch
|
8 |
+
from tools import tools
|
9 |
+
from transformers import (
|
10 |
+
AutoModelForCausalLM,
|
11 |
+
AutoTokenizer,
|
12 |
+
BitsAndBytesConfig
|
13 |
+
)
|
14 |
+
|
15 |
+
# Get all functions from functions.py
|
16 |
+
all_functions = [func for func in dir(functions) if callable(
|
17 |
+
getattr(functions, func)) and not func.startswith("__")]
|
18 |
+
|
19 |
+
# Create names_to_function dict containing partials for all functions in functions.py
|
20 |
+
names_to_functions = {func: functools.partial(
|
21 |
+
getattr(functions, func)) for func in all_functions}
|
22 |
+
|
23 |
+
|
24 |
+
model_id = "meta-llama/Meta-Llama-3.1-8B-Instruct"
|
25 |
+
|
26 |
+
# specify how to quantize the model
|
27 |
+
quantization_config = BitsAndBytesConfig(
|
28 |
+
load_in_4bit=True,
|
29 |
+
bnb_4bit_quant_type="nf4",
|
30 |
+
bnb_4bit_compute_dtype=torch.bfloat16,
|
31 |
+
)
|
32 |
+
|
33 |
+
tokenizer = AutoTokenizer.from_pretrained(model_id)
|
34 |
+
model = AutoModelForCausalLM.from_pretrained(
|
35 |
+
model_id, device_map="auto", quantization_config=quantization_config
|
36 |
+
)
|
37 |
+
|
38 |
+
|
39 |
+
def extract_function_call(output):
|
40 |
+
match = re.search(r'<\|python_tag\|>(.*)<\|eom_id\|>', output)
|
41 |
+
if match:
|
42 |
+
function_call = match.group(1)
|
43 |
+
return json.loads(function_call)
|
44 |
+
else:
|
45 |
+
return None
|
46 |
+
|
47 |
+
|
48 |
+
def chatbot(query):
|
49 |
+
messages = [
|
50 |
+
{"role": "system", "content": "You are a movie search assistant bot who uses TMDB to help users find movies. Think step by step and identify the sequence of function calls that will help to answer."},
|
51 |
+
{"role": "user", "content": query},
|
52 |
+
]
|
53 |
+
|
54 |
+
tokenized_chat = tokenizer.apply_chat_template(
|
55 |
+
messages, tools=tools, add_generation_prompt=True, tokenize=True, return_tensors="pt")
|
56 |
+
|
57 |
+
outputs = model.generate(tokenized_chat, max_new_tokens=128)
|
58 |
+
answer = tokenizer.batch_decode(outputs[:, tokenized_chat.shape[1]:])[0]
|
59 |
+
tool_call = extract_function_call(answer)
|
60 |
+
|
61 |
+
if tool_call:
|
62 |
+
function_name = tool_call['name']
|
63 |
+
function_params = tool_call['parameters']
|
64 |
+
print("\nfunction_name: ", function_name,
|
65 |
+
"\nfunction_params: ", function_params)
|
66 |
+
function_result = names_to_functions[function_name](**function_params)
|
67 |
+
print(function_result['results'])
|
68 |
+
return function_result['results']
|
69 |
+
else:
|
70 |
+
print("No tool calls found in the answer.")
|
app.py
ADDED
@@ -0,0 +1,99 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from flask import Flask, render_template, request, jsonify
|
2 |
+
from agent import chatbot
|
3 |
+
from functions import query_tmdb
|
4 |
+
|
5 |
+
app = Flask(__name__)
|
6 |
+
|
7 |
+
|
8 |
+
@app.route('/')
|
9 |
+
def index():
|
10 |
+
page = request.args.get('page', 1)
|
11 |
+
url = f'https://api.themoviedb.org/3/movie/popular?page={page}'
|
12 |
+
data = query_tmdb(url)
|
13 |
+
movies = data.get('results', [])
|
14 |
+
total_pages = data.get('total_pages', 1)
|
15 |
+
return render_template('index.html', movies=movies, page=int(page), total_pages=total_pages, title="Popular Movies")
|
16 |
+
|
17 |
+
|
18 |
+
@app.route('/movie/<int:movie_id>')
|
19 |
+
def movie_details(movie_id):
|
20 |
+
url = f'https://api.themoviedb.org/3/movie/{movie_id}'
|
21 |
+
movie = query_tmdb(url)
|
22 |
+
return jsonify(movie)
|
23 |
+
|
24 |
+
|
25 |
+
@app.route('/movie/<int:movie_id>/credits')
|
26 |
+
def movie_credits(movie_id):
|
27 |
+
url = f'https://api.themoviedb.org/3/movie/{movie_id}/credits'
|
28 |
+
credits = query_tmdb(url)
|
29 |
+
return jsonify(credits)
|
30 |
+
|
31 |
+
|
32 |
+
@app.route('/genre/<int:genre_id>')
|
33 |
+
def genre_filter(genre_id):
|
34 |
+
page = request.args.get('page', 1)
|
35 |
+
url = f'https://api.themoviedb.org/3/discover/movie?with_genres={genre_id}&page={page}'
|
36 |
+
data = query_tmdb(url)
|
37 |
+
genre_url = 'https://api.themoviedb.org/3/genre/movie/list'
|
38 |
+
genres = query_tmdb(genre_url).get('genres', [])
|
39 |
+
genre_name = next(
|
40 |
+
(genre['name'] for genre in genres if genre['id'] == genre_id), 'Unknown')
|
41 |
+
return render_template('index.html', movies=data.get('results', []), page=int(page), total_pages=data.get('total_pages', 1), title=f"Movies in Genre: {genre_name}")
|
42 |
+
|
43 |
+
|
44 |
+
@app.route('/genres')
|
45 |
+
def genres():
|
46 |
+
genre_url = 'https://api.themoviedb.org/3/genre/movie/list'
|
47 |
+
genres = query_tmdb(genre_url).get('genres', [])
|
48 |
+
return render_template('genres.html', genres=genres)
|
49 |
+
|
50 |
+
|
51 |
+
@app.route('/cast')
|
52 |
+
def cast():
|
53 |
+
page = request.args.get('page', 1)
|
54 |
+
url = f'https://api.themoviedb.org/3/trending/person/week?page={page}'
|
55 |
+
data = query_tmdb(url)
|
56 |
+
persons = data.get('results', [])
|
57 |
+
total_pages = data.get('total_pages', 1)
|
58 |
+
return render_template('cast.html', persons=persons, page=int(page), total_pages=total_pages, title="Trending Cast Members")
|
59 |
+
|
60 |
+
|
61 |
+
class PersonInfo:
|
62 |
+
def __init__(self, name, image, bio):
|
63 |
+
self.name = name
|
64 |
+
self.image = image
|
65 |
+
self.bio = bio
|
66 |
+
|
67 |
+
|
68 |
+
@app.route('/cast/<int:cast_id>')
|
69 |
+
def cast_filter(cast_id):
|
70 |
+
page = request.args.get('page', 1)
|
71 |
+
url = f'https://api.themoviedb.org/3/discover/movie?with_cast={cast_id}&page={page}'
|
72 |
+
data = query_tmdb(url)
|
73 |
+
cast_url = f'https://api.themoviedb.org/3/person/{cast_id}'
|
74 |
+
cast_data = query_tmdb(cast_url)
|
75 |
+
cast_name = cast_data.get('name', 'Unknown')
|
76 |
+
cast_bio = cast_data.get('biography', 'No biography available')
|
77 |
+
cast_image = f"https://image.tmdb.org/t/p/w500{cast_data.get('profile_path', '')}"
|
78 |
+
cast_info = PersonInfo(name=cast_name, image=cast_image, bio=cast_bio)
|
79 |
+
return render_template('index.html',
|
80 |
+
movies=data.get('results', []),
|
81 |
+
page=int(page),
|
82 |
+
total_pages=data.get('total_pages', 1),
|
83 |
+
title=f"Movies with Actor/Director: {cast_name}",
|
84 |
+
cast_info=cast_info)
|
85 |
+
|
86 |
+
|
87 |
+
@app.route('/query', methods=['GET', 'POST'])
|
88 |
+
def query():
|
89 |
+
if request.method == 'POST':
|
90 |
+
query = request.form.get('query')
|
91 |
+
result = chatbot(query) # Call the chatbot function from agent.py
|
92 |
+
print("Got result: ", result)
|
93 |
+
print("Movie ids are: ", [r['id'] for r in result])
|
94 |
+
return render_template('query.html', result=result)
|
95 |
+
return render_template('query.html')
|
96 |
+
|
97 |
+
|
98 |
+
if __name__ == '__main__':
|
99 |
+
app.run(host='0.0.0.0', port=5000)
|
functions.py
ADDED
@@ -0,0 +1,68 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import requests
|
2 |
+
import os
|
3 |
+
|
4 |
+
BASE_URL = "https://api.themoviedb.org/3"
|
5 |
+
API_KEY = os.environ['TMDB_API_KEY']
|
6 |
+
|
7 |
+
|
8 |
+
def query_tmdb(endpoint, params={}):
|
9 |
+
headers = {
|
10 |
+
"accept": "application/json",
|
11 |
+
"Authorization": f"Bearer {API_KEY}"
|
12 |
+
}
|
13 |
+
response = requests.get(endpoint, headers=headers, params=params)
|
14 |
+
return response.json()
|
15 |
+
|
16 |
+
|
17 |
+
def discover_movie(include_adult=False, include_video=False, language="en-US", page=1, sort_by="popularity.desc", **kwargs):
|
18 |
+
endpoint = f"{BASE_URL}/discover/movie"
|
19 |
+
params = {}
|
20 |
+
|
21 |
+
for key, value in kwargs.items():
|
22 |
+
if value is not None:
|
23 |
+
params[key] = value
|
24 |
+
response = query_tmdb(endpoint, params=params)
|
25 |
+
return response
|
26 |
+
|
27 |
+
|
28 |
+
def get_movie_details(movie_id, append_to_response=None):
|
29 |
+
endpoint = f"{BASE_URL}/movie/{movie_id}"
|
30 |
+
params = {}
|
31 |
+
|
32 |
+
response = query_tmdb(endpoint, params=params)
|
33 |
+
return response
|
34 |
+
|
35 |
+
|
36 |
+
def search_person(query, include_adult=False, language="en-US", page=1):
|
37 |
+
endpoint = f"{BASE_URL}/search/person"
|
38 |
+
params = {
|
39 |
+
'query': query,
|
40 |
+
'include_adult': include_adult,
|
41 |
+
'language': language,
|
42 |
+
'page': page
|
43 |
+
}
|
44 |
+
|
45 |
+
response = query_tmdb(endpoint, params=params)
|
46 |
+
return response
|
47 |
+
|
48 |
+
|
49 |
+
def get_person_details(person_id, language="en-US", append_to_response=None):
|
50 |
+
endpoint = f"{BASE_URL}/person/{person_id}"
|
51 |
+
params = {
|
52 |
+
'language': language
|
53 |
+
}
|
54 |
+
if append_to_response:
|
55 |
+
params['append_to_response'] = append_to_response
|
56 |
+
|
57 |
+
response = query_tmdb(endpoint, params=params)
|
58 |
+
return response
|
59 |
+
|
60 |
+
|
61 |
+
def get_movie_genres(language="en-US"):
|
62 |
+
endpoint = f"{BASE_URL}/genre/movie/list"
|
63 |
+
params = {
|
64 |
+
'language': language
|
65 |
+
}
|
66 |
+
|
67 |
+
response = query_tmdb(endpoint, params=params)
|
68 |
+
return response
|
requirements.txt
ADDED
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
Flask
|
2 |
+
requests
|
3 |
+
transformers
|
4 |
+
torch
|
5 |
+
bitsandbytes
|
6 |
+
accelerate
|
static/css/style.css
ADDED
@@ -0,0 +1,94 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
#sidebar {
|
2 |
+
width: 200px;
|
3 |
+
position: fixed;
|
4 |
+
height: 100%;
|
5 |
+
left: 0;
|
6 |
+
background-color: #f8f9fa;
|
7 |
+
padding: 20px;
|
8 |
+
}
|
9 |
+
|
10 |
+
#sidebar ul {
|
11 |
+
list-style-type: none;
|
12 |
+
padding: 0;
|
13 |
+
}
|
14 |
+
|
15 |
+
#sidebar li {
|
16 |
+
margin-bottom: 10px;
|
17 |
+
}
|
18 |
+
|
19 |
+
#sidebar a {
|
20 |
+
text-decoration: none;
|
21 |
+
color: #333;
|
22 |
+
}
|
23 |
+
|
24 |
+
#main-content {
|
25 |
+
margin-left: 200px;
|
26 |
+
padding: 40px;
|
27 |
+
}
|
28 |
+
|
29 |
+
.tile-element {
|
30 |
+
display: inline-block;
|
31 |
+
width: 200px;
|
32 |
+
margin: 10px;
|
33 |
+
cursor: pointer;
|
34 |
+
vertical-align: top;
|
35 |
+
}
|
36 |
+
|
37 |
+
.tile-element img {
|
38 |
+
width: 100%;
|
39 |
+
}
|
40 |
+
|
41 |
+
#side-panel {
|
42 |
+
position: fixed;
|
43 |
+
top: 50%;
|
44 |
+
left: 50%;
|
45 |
+
transform: translate(-50%, -50%);
|
46 |
+
width: 60%;
|
47 |
+
background: white;
|
48 |
+
border: 1px solid #ccc;
|
49 |
+
padding: 20px;
|
50 |
+
z-index: 1000;
|
51 |
+
display: none;
|
52 |
+
flex-direction: row;
|
53 |
+
justify-content: space-between;
|
54 |
+
align-items: flex-start;
|
55 |
+
}
|
56 |
+
|
57 |
+
#side-panel.active {
|
58 |
+
display: flex;
|
59 |
+
}
|
60 |
+
|
61 |
+
#close-button {
|
62 |
+
align-self: flex-start;
|
63 |
+
cursor: pointer;
|
64 |
+
font-size: 20px;
|
65 |
+
}
|
66 |
+
|
67 |
+
#movie-details {
|
68 |
+
padding-left: 20px;
|
69 |
+
text-align: left;
|
70 |
+
flex-direction: column;
|
71 |
+
}
|
72 |
+
|
73 |
+
#movie-poster {
|
74 |
+
width: 30%;
|
75 |
+
object-fit: contain;
|
76 |
+
}
|
77 |
+
|
78 |
+
#overlay {
|
79 |
+
display: none;
|
80 |
+
/* Hidden by default */
|
81 |
+
position: fixed;
|
82 |
+
top: 0;
|
83 |
+
left: 0;
|
84 |
+
width: 100%;
|
85 |
+
height: 100%;
|
86 |
+
background: rgba(0, 0, 0, 0.5);
|
87 |
+
/* Semi-transparent background */
|
88 |
+
z-index: 999;
|
89 |
+
}
|
90 |
+
|
91 |
+
#overlay.active {
|
92 |
+
display: block;
|
93 |
+
/* Show overlay when active */
|
94 |
+
}
|
static/js/script.js
ADDED
@@ -0,0 +1,62 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
|
2 |
+
function generatePanelContent(data) {
|
3 |
+
const { movie, credits } = data;
|
4 |
+
|
5 |
+
const posterUrl = movie.poster_path
|
6 |
+
? `https://image.tmdb.org/t/p/w200${movie.poster_path}`
|
7 |
+
: 'default_image_url';
|
8 |
+
|
9 |
+
const genres = movie.genres.map(genre => {
|
10 |
+
return `<a href="/genre/${genre.id}">${genre.name}</a>`;
|
11 |
+
}).join(', ');
|
12 |
+
|
13 |
+
const director = credits.crew.find(crewMember => crewMember.job === 'Director');
|
14 |
+
const directorLink = director
|
15 |
+
? `<a href="/cast/${director.id}">${director.name}</a>`
|
16 |
+
: 'N/A';
|
17 |
+
|
18 |
+
const cast = credits.cast.slice(0, 5).map(castMember => {
|
19 |
+
return `<a href="/cast/${castMember.id}">${castMember.name}</a>`;
|
20 |
+
}).join(', ');
|
21 |
+
|
22 |
+
const movieDetailsInnerHTML = `
|
23 |
+
<h2>${movie.title}</h2>
|
24 |
+
<p>${movie.overview}</p>
|
25 |
+
<strong>Genres:</strong> ${genres}<br>
|
26 |
+
<strong>Director:</strong> ${directorLink}<br>
|
27 |
+
<strong>Cast:</strong> ${cast}`;
|
28 |
+
|
29 |
+
return { posterUrl, movieDetailsInnerHTML };
|
30 |
+
}
|
31 |
+
|
32 |
+
function showDetails(movieId) {
|
33 |
+
fetch(`/movie/${movieId}`)
|
34 |
+
.then(response => response.json())
|
35 |
+
.then(movie => {
|
36 |
+
fetch(`/movie/${movieId}/credits`)
|
37 |
+
.then(response => response.json())
|
38 |
+
.then(credits => {
|
39 |
+
const panel = document.getElementById('side-panel');
|
40 |
+
const overlay = document.getElementById('overlay');
|
41 |
+
const moviePoster = document.getElementById('movie-poster');
|
42 |
+
const movieDetails = document.getElementById('movie-details');
|
43 |
+
|
44 |
+
// Retrieve posterUrl and movieDetailsInnerHTML from generatePanelContent
|
45 |
+
const { posterUrl, movieDetailsInnerHTML } = generatePanelContent({ movie, credits });
|
46 |
+
|
47 |
+
// Update the respective DOM elements
|
48 |
+
moviePoster.src = posterUrl;
|
49 |
+
movieDetails.innerHTML = movieDetailsInnerHTML;
|
50 |
+
|
51 |
+
panel.classList.add('active'); // Add active class to slide in
|
52 |
+
overlay.classList.add('active'); // Show overlay
|
53 |
+
});
|
54 |
+
});
|
55 |
+
}
|
56 |
+
|
57 |
+
function closePanel() {
|
58 |
+
const panel = document.getElementById('side-panel');
|
59 |
+
const overlay = document.getElementById('overlay');
|
60 |
+
panel.classList.remove('active'); // Remove active class to slide out
|
61 |
+
overlay.classList.remove('active'); // Hide overlay
|
62 |
+
}
|
templates/base.html
ADDED
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<!DOCTYPE html>
|
2 |
+
<html lang="en">
|
3 |
+
|
4 |
+
<head>
|
5 |
+
<meta charset="UTF-8">
|
6 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
7 |
+
<title>TMDB Movies Explorer</title>
|
8 |
+
|
9 |
+
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
|
10 |
+
<script src="{{ url_for('static', filename='js/script.js') }}"></script>
|
11 |
+
</head>
|
12 |
+
|
13 |
+
<body>
|
14 |
+
<div id="sidebar">
|
15 |
+
<ul>
|
16 |
+
<li><a href="/">Popular Movies</a></li>
|
17 |
+
<li><a href="/genres">By Genre</a></li>
|
18 |
+
<li><a href="/cast">By Cast</a></li>
|
19 |
+
<li><a href="/query">Query</a></li>
|
20 |
+
</ul>
|
21 |
+
</div>
|
22 |
+
<div id="main-content">
|
23 |
+
{% block content %}{% endblock %}
|
24 |
+
</div>
|
25 |
+
</body>
|
26 |
+
|
27 |
+
</html>
|
templates/cast.html
ADDED
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
{% extends 'base.html' %}
|
2 |
+
|
3 |
+
{% block content %}
|
4 |
+
{% for person in persons %}
|
5 |
+
<div class="tile-element">
|
6 |
+
<a href="cast/{{ person.id }}">
|
7 |
+
<img src="https://image.tmdb.org/t/p/w500{{ person.profile_path }}" alt="{{ person.name}}">
|
8 |
+
</a>
|
9 |
+
<h2>{{ person.name }}</h2>
|
10 |
+
</div>
|
11 |
+
{% endfor %}
|
12 |
+
{% endblock %}
|
templates/genres.html
ADDED
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<!-- genres.html -->
|
2 |
+
{% extends 'base.html' %}
|
3 |
+
|
4 |
+
{% block content %}
|
5 |
+
<h1>Genres</h1>
|
6 |
+
<ul>
|
7 |
+
{% for genre in genres %}
|
8 |
+
<li><a href="/genre/{{ genre.id }}">{{ genre.name }}</a></li>
|
9 |
+
{% endfor %}
|
10 |
+
</ul>
|
11 |
+
{% endblock %}
|
templates/index.html
ADDED
@@ -0,0 +1,45 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
{% extends 'base.html' %}
|
2 |
+
|
3 |
+
{% block content %}
|
4 |
+
|
5 |
+
{% if cast_info %}
|
6 |
+
<div style="display: flex;">
|
7 |
+
<div style="max-width: 30%;">
|
8 |
+
<img src="{{ cast_info.image }}" alt="{{ title }}" style="width: 300px; object-fit: contain;">
|
9 |
+
</div>
|
10 |
+
<div style="max-width: 70%; padding-left: 20px;">
|
11 |
+
<h2>{{ cast_info.name }}</h2>
|
12 |
+
<p>{{ cast_info.bio }}</p>
|
13 |
+
</div>
|
14 |
+
</div>
|
15 |
+
<h2>{{ title }}</h2>
|
16 |
+
{% else %}
|
17 |
+
<h1>{{ title }}</h1>
|
18 |
+
{% endif %}
|
19 |
+
|
20 |
+
<div>
|
21 |
+
{% for movie in movies %}
|
22 |
+
<div class="tile-element" onclick="showDetails({{ movie.id }})">
|
23 |
+
<img src="https://image.tmdb.org/t/p/w500{{ movie.poster_path }}" alt="{{ movie.title }}">
|
24 |
+
<h4>{{ movie.title }}</h4>
|
25 |
+
</div>
|
26 |
+
{% endfor %}
|
27 |
+
</div>
|
28 |
+
<div>
|
29 |
+
{% if page > 1 %}
|
30 |
+
<a href="?page={{ page - 1 }}">Previous</a>
|
31 |
+
{% endif %}
|
32 |
+
{% if page < total_pages %} <a href="?page={{ page + 1 }}">Next</a>
|
33 |
+
{% endif %}
|
34 |
+
</div>
|
35 |
+
|
36 |
+
<div id="overlay" onclick="closePanel()"></div> <!-- Overlay for closing panel -->
|
37 |
+
<div id="side-panel">
|
38 |
+
<img id="movie-poster" src="" alt="${movie.title} Poster" style="max-width: 150px;">
|
39 |
+
<div id="movie-details">
|
40 |
+
<!-- Movie details will be injected here -->
|
41 |
+
</div>
|
42 |
+
<span id="close-button" onclick="closePanel()">×</span>
|
43 |
+
|
44 |
+
</div>
|
45 |
+
{% endblock %}
|
templates/query.html
ADDED
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
{% extends 'base.html' %}
|
2 |
+
|
3 |
+
{% block content %}
|
4 |
+
<h1>Query</h1>
|
5 |
+
<form action="/query" method="post">
|
6 |
+
<input type="text" name="query" placeholder="Enter your query...">
|
7 |
+
<input type="submit" value="Submit">
|
8 |
+
</form>
|
9 |
+
|
10 |
+
{% if result %}
|
11 |
+
<h2>Query Result</h2>
|
12 |
+
<div>
|
13 |
+
{% for movie in result %}
|
14 |
+
<div class="tile-element" onclick="showDetails({{ movie.id }})">
|
15 |
+
<img src="https://image.tmdb.org/t/p/w500{{ movie.poster_path }}" alt="{{ movie.title }}">
|
16 |
+
<h4>{{ movie.title }}</h4>
|
17 |
+
</div>
|
18 |
+
{% endfor %}
|
19 |
+
</div>
|
20 |
+
{% elif result is defined %}
|
21 |
+
<p>No results found.</p>
|
22 |
+
{% endif %}
|
23 |
+
{% endblock %}
|
tools.py
ADDED
@@ -0,0 +1,251 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
tools = [
|
2 |
+
{
|
3 |
+
"type": "function",
|
4 |
+
"function": {
|
5 |
+
"name": "discover_movie",
|
6 |
+
"description": "Find movies using over 30 filters and sort options",
|
7 |
+
"parameters": {
|
8 |
+
"type": "object",
|
9 |
+
"properties": {
|
10 |
+
"region": {
|
11 |
+
"type": "string",
|
12 |
+
"description": "ISO 3166-1 code to filter release dates",
|
13 |
+
},
|
14 |
+
"sort_by": {
|
15 |
+
"type": "string",
|
16 |
+
"description": "Sort the results",
|
17 |
+
},
|
18 |
+
"certification_country": {
|
19 |
+
"type": "string",
|
20 |
+
"description": "Used in conjunction with the certification filter",
|
21 |
+
},
|
22 |
+
"certification": {
|
23 |
+
"type": "string",
|
24 |
+
"description": "Filter results with a valid certification from the certification_country",
|
25 |
+
},
|
26 |
+
"certification.lte": {
|
27 |
+
"type": "string",
|
28 |
+
"description": "Filter and only include movies that have a certification that is less than or equal to the specified value",
|
29 |
+
},
|
30 |
+
"certification.gte": {
|
31 |
+
"type": "string",
|
32 |
+
"description": "Filter and only include movies that have a certification that is greater than or equal to the specified value",
|
33 |
+
},
|
34 |
+
"include_adult": {
|
35 |
+
"type": "boolean",
|
36 |
+
"description": "Choose whether to include adult (pornography) content in the results",
|
37 |
+
},
|
38 |
+
"include_video": {
|
39 |
+
"type": "boolean",
|
40 |
+
"description": "Choose whether to include videos in the results",
|
41 |
+
},
|
42 |
+
"page": {
|
43 |
+
"type": "integer",
|
44 |
+
"description": "Specify which page to query",
|
45 |
+
},
|
46 |
+
"primary_release_year": {
|
47 |
+
"type": "integer",
|
48 |
+
"description": "Filter the results to only include movies that have a primary release year that equals the specified value",
|
49 |
+
},
|
50 |
+
"primary_release_date.gte": {
|
51 |
+
"type": "string",
|
52 |
+
"description": "Filter and only include movies that have a primary release date that is greater or equal to the specified value",
|
53 |
+
},
|
54 |
+
"primary_release_date.lte": {
|
55 |
+
"type": "string",
|
56 |
+
"description": "Filter and only include movies that have a primary release date that is less than or equal to the specified value",
|
57 |
+
},
|
58 |
+
"release_date.gte": {
|
59 |
+
"type": "string",
|
60 |
+
"description": "Filter and only include movies that have a release date (looking at all release dates) that is greater or equal to the specified value",
|
61 |
+
},
|
62 |
+
"release_date.lte": {
|
63 |
+
"type": "string",
|
64 |
+
"description": "Filter and only include movies that have a release date (looking at all release dates) that is less than or equal to the specified value",
|
65 |
+
},
|
66 |
+
"with_release_type": {
|
67 |
+
"type": "integer",
|
68 |
+
"description": "Specify a comma (AND) or pipe (OR) separated value to filter release types",
|
69 |
+
},
|
70 |
+
"year": {
|
71 |
+
"type": "integer",
|
72 |
+
"description": "Filter the results to only include movies that have a release year that equals the specified value",
|
73 |
+
},
|
74 |
+
"vote_count.gte": {
|
75 |
+
"type": "integer",
|
76 |
+
"description": "Filter and only include movies that have a vote count that is greater or equal to the specified value",
|
77 |
+
},
|
78 |
+
"vote_count.lte": {
|
79 |
+
"type": "integer",
|
80 |
+
"description": "Filter and only include movies that have a vote count that is less than or equal to the specified value",
|
81 |
+
},
|
82 |
+
"vote_average.gte": {
|
83 |
+
"type": "number",
|
84 |
+
"description": "Filter and only include movies that have a rating that is greater or equal to the specified value",
|
85 |
+
},
|
86 |
+
"vote_average.lte": {
|
87 |
+
"type": "number",
|
88 |
+
"description": "Filter and only include movies that have a rating that is less than or equal to the specified value",
|
89 |
+
},
|
90 |
+
"with_cast": {
|
91 |
+
"type": "string",
|
92 |
+
"description": "A comma separated list of person ID's to filter the results with",
|
93 |
+
},
|
94 |
+
"with_crew": {
|
95 |
+
"type": "string",
|
96 |
+
"description": "A comma separated list of person ID's to filter the results with",
|
97 |
+
},
|
98 |
+
"with_people": {
|
99 |
+
"type": "string",
|
100 |
+
"description": "A comma separated list of person ID's to filter the results with",
|
101 |
+
},
|
102 |
+
"with_companies": {
|
103 |
+
"type": "string",
|
104 |
+
"description": "A comma separated list of production company ID's to filter the results with",
|
105 |
+
},
|
106 |
+
"with_genres": {
|
107 |
+
"type": "string",
|
108 |
+
"description": "A comma separated list of genre ID's to filter the results with",
|
109 |
+
},
|
110 |
+
"without_genres": {
|
111 |
+
"type": "string",
|
112 |
+
"description": "A comma separated list of genre ID's to exclude from the results",
|
113 |
+
},
|
114 |
+
"with_keywords": {
|
115 |
+
"type": "string",
|
116 |
+
"description": "A comma separated list of keyword ID's to filter the results with",
|
117 |
+
},
|
118 |
+
"without_keywords": {
|
119 |
+
"type": "string",
|
120 |
+
"description": "A comma separated list of keyword ID's to exclude from the results",
|
121 |
+
},
|
122 |
+
"with_runtime.gte": {
|
123 |
+
"type": "integer",
|
124 |
+
"description": "Filter and only include movies that have a runtime that is greater or equal to the specified value",
|
125 |
+
},
|
126 |
+
"with_runtime.lte": {
|
127 |
+
"type": "integer",
|
128 |
+
"description": "Filter and only include movies that have a runtime that is less than or equal to the specified value",
|
129 |
+
},
|
130 |
+
"with_original_language": {
|
131 |
+
"type": "string",
|
132 |
+
"description": "Specify an ISO 639-1 string to filter results by their original language value",
|
133 |
+
},
|
134 |
+
"with_watch_providers": {
|
135 |
+
"type": "string",
|
136 |
+
"description": "A comma or pipe separated list of watch provider ID's to filter the results with",
|
137 |
+
},
|
138 |
+
"watch_region": {
|
139 |
+
"type": "string",
|
140 |
+
"description": "An ISO 3166-1 code to filter the watch provider results",
|
141 |
+
},
|
142 |
+
"with_watch_monetization_types": {
|
143 |
+
"type": "string",
|
144 |
+
"description": "Filter the results by monetization type",
|
145 |
+
},
|
146 |
+
"without_companies": {
|
147 |
+
"type": "string",
|
148 |
+
"description": "A comma separated list of production company ID's to filter the results with",
|
149 |
+
},
|
150 |
+
},
|
151 |
+
"required": [],
|
152 |
+
},
|
153 |
+
},
|
154 |
+
},
|
155 |
+
{
|
156 |
+
"type": "function",
|
157 |
+
"function": {
|
158 |
+
"name": "get_movie_details",
|
159 |
+
"description": "Get the top level details of a movie by ID",
|
160 |
+
"parameters": {
|
161 |
+
"type": "object",
|
162 |
+
"properties": {
|
163 |
+
"movie_id": {
|
164 |
+
"type": "integer",
|
165 |
+
"description": "The ID of the movie to get details for",
|
166 |
+
},
|
167 |
+
"append_to_response": {
|
168 |
+
"type": "string",
|
169 |
+
"description": "Comma-separated list of sub requests to append to the response",
|
170 |
+
},
|
171 |
+
},
|
172 |
+
"required": ["movie_id"],
|
173 |
+
},
|
174 |
+
},
|
175 |
+
},
|
176 |
+
{
|
177 |
+
"type": "function",
|
178 |
+
"function": {
|
179 |
+
"name": "search_person",
|
180 |
+
"description": "Search for people in the entertainment industry.",
|
181 |
+
"parameters": {
|
182 |
+
"type": "object",
|
183 |
+
"properties": {
|
184 |
+
"query": {
|
185 |
+
"type": "string",
|
186 |
+
"description": "The search query for the person"
|
187 |
+
},
|
188 |
+
"include_adult": {
|
189 |
+
"type": "boolean",
|
190 |
+
"description": "Include adult (pornography) content in the results",
|
191 |
+
"default": False
|
192 |
+
},
|
193 |
+
"language": {
|
194 |
+
"type": "string",
|
195 |
+
"description": "Language for the search results",
|
196 |
+
"default": "en-US"
|
197 |
+
},
|
198 |
+
"page": {
|
199 |
+
"type": "integer",
|
200 |
+
"description": "Page number of results",
|
201 |
+
"default": 1
|
202 |
+
}
|
203 |
+
},
|
204 |
+
"required": ["query"]
|
205 |
+
}
|
206 |
+
}
|
207 |
+
},
|
208 |
+
{
|
209 |
+
"type": "function",
|
210 |
+
"function": {
|
211 |
+
"name": "get_person_details",
|
212 |
+
"description": "Get detailed information about a specific person.",
|
213 |
+
"parameters": {
|
214 |
+
"type": "object",
|
215 |
+
"properties": {
|
216 |
+
"person_id": {
|
217 |
+
"type": "integer",
|
218 |
+
"description": "The ID of the person to get details for"
|
219 |
+
},
|
220 |
+
"language": {
|
221 |
+
"type": "string",
|
222 |
+
"description": "Language for the person details",
|
223 |
+
"default": "en-US"
|
224 |
+
},
|
225 |
+
"append_to_response": {
|
226 |
+
"type": "string",
|
227 |
+
"description": "Comma-separated list of additional details to append to the response (e.g., 'images,credits')"
|
228 |
+
}
|
229 |
+
},
|
230 |
+
"required": ["person_id"]
|
231 |
+
}
|
232 |
+
}
|
233 |
+
},
|
234 |
+
{
|
235 |
+
"type": "function",
|
236 |
+
"function": {
|
237 |
+
"name": "get_movie_genres",
|
238 |
+
"description": "Get the list of official genres for movies.",
|
239 |
+
"parameters": {
|
240 |
+
"type": "object",
|
241 |
+
"properties": {
|
242 |
+
"language": {
|
243 |
+
"type": "string",
|
244 |
+
"description": "Language for the genre names",
|
245 |
+
"default": "en-US"
|
246 |
+
}
|
247 |
+
}
|
248 |
+
}
|
249 |
+
}
|
250 |
+
}
|
251 |
+
]
|