20 Commits

Author SHA1 Message Date
Joakim Persson 82c7887cf7 Lade till requests 2024-08-13 15:27:36 +02:00
Joakim Persson 5f546a848e Beroenden som behövs till appar i /web 2024-08-13 15:20:10 +02:00
Joakim Persson 7eb889a889 Nedladdning av bilder från websida. Kodbasen från GPT 4 Omni 2024-08-13 15:15:04 +02:00
Joakim Persson 6dab09c861 Fixat så bilderna landar i hämtade filer. Tar bort skalningsinformation m.m. efter filnamnet så att suffix blir korrekt. 2024-08-13 15:13:59 +02:00
Joakim Persson 8288450662 Lade till .DS_Store 2024-08-13 14:31:21 +02:00
Joakim Persson ae67b9c7b3 Test med att hämta alla bilder på hemsida 2024-08-13 14:30:34 +02:00
joakimp 97ee179b29 Snyggade till en loggutskrivt 2024-08-06 23:37:27 +02:00
joakimp 942f9f78c9 Lade till metod för att erhålla host_title för nuvarande endpoint 2024-08-06 23:36:49 +02:00
joakimp 3f39e11b10 Lagt ytterligare loggmeddelande för att underlätta felsökning. 2024-08-06 22:08:16 +02:00
joakimp 32098e3452 Experimenterar med olika parametrar i anrop till sendMessage() för att säkerställa att byte till ny modell verkligen görs. 2024-08-06 22:07:18 +02:00
Joakim Persson 5b143e75e0 Lagt till setEndointAndLlm() till frontendApi för att skicka gjorda val av endpoint title och llm till backend.py 2024-08-06 17:34:56 +02:00
Joakim Persson 168b8b13c1 Tog bort oanvnd och bortkommenterad kod 2024-08-06 17:33:14 +02:00
Joakim Persson f7f6ce2e49 Docstrings och type hinting. Lagt till route för (api/select_endpoint_llm. Brutit ut header-generering till get_auth_headers() 2024-08-06 17:32:46 +02:00
joakimp fa98c7b162 Nu sätts titeln på rullgardinsmenyn till det element som valts i denna. Lade till viewport för att underlätta för olika webklienter. 2024-08-06 00:50:47 +02:00
joakimp fd5f6199e9 Hanterar dynamisk uppdatering av innehållet i rullgardinsmenyn 2024-08-06 00:48:54 +02:00
joakimp c26dbc5612 /api/endpoint retunerar lista över endpoints och deras respektive llm:er 2024-08-06 00:47:28 +02:00
joakimp 7f557fadd6 Automatisk anpassning av dropdown-meny till textbredden på innehållet 2024-08-06 00:46:19 +02:00
joakimp 5ce92a5602 Tog bort en tomrad bara... 2024-08-06 00:22:16 +02:00
Joakim Persson 6dc93b66be Anropar Flask för att få en lista med tillgängliga endpoints och LLM:er 2024-08-05 17:27:49 +02:00
Joakim Persson 606becc5c3 Säkerställt så att innehållet i dropdown-menyn kan ändras dynamiskt 2024-08-05 17:27:00 +02:00
9 changed files with 366 additions and 68 deletions
+2 -1
View File
@@ -161,4 +161,5 @@ cython_debug/
#.idea/ #.idea/
# Exclude venv from smartassist # Exclude venv from smartassist
smartassist/smartassist_dev_venv smartassist/smartassist_dev_venv
.DS_Store
+138 -49
View File
@@ -1,7 +1,7 @@
# Import the necessary functions from ollama, Flask, requests, threading # Import the necessary functions from ollama, Flask, requests, threading
from ollama import Client from ollama import Client
from flask import Flask, request, jsonify, send_from_directory, render_template, session, make_response from flask import Flask, request, jsonify, send_from_directory, render_template, session, make_response, Response
from flask_cors import CORS, cross_origin # CORS stands for Cross-Origin Resource Sharing. This is necessary to allow the frontend to make requests to our backend. from flask_cors import CORS, cross_origin # CORS stands for Cross-Origin Resource Sharing. This is necessary to allow the frontend to make requests to our backend.
import requests import requests
import json import json
@@ -9,6 +9,7 @@ import logging
import os import os
import utils import utils
from utils import GlobalState from utils import GlobalState
from pathlib import Path
# Create a logger for this module # Create a logger for this module
global_state = GlobalState() # Import the singleton that holds global states (e.g., logger) global_state = GlobalState() # Import the singleton that holds global states (e.g., logger)
@@ -38,11 +39,17 @@ app.config['SESSION_TYPE'] = 'filesystem' # Store sessions on the filesystem
logger.debug("flask app template folder: %s", app.template_folder) logger.debug("flask app template folder: %s", app.template_folder)
@app.route('/') @app.route('/')
def index(): def index() -> Response:
"""
This route serves index.html to connecting clients
""" """
This route serves index.html to connecting clients.
Initializes a new chat session by clearing the chat history in the session object.
Retrieves environment variables for the backend API endpoint, host URL of LLMs, and the selected LLM model.
Reads the client HTML template from file and passes it to the index.html template along with other necessary parameters.
Returns:
Response: A Flask response containing the rendered index.html template.
"""
session['chat_history'] = [] # The session object (actually, a dictonary) holds the chat session session['chat_history'] = [] # The session object (actually, a dictonary) holds the chat session
logger.debug("Entering route '/'") logger.debug("Entering route '/'")
api_endpoint = global_state.get_backend_api_ep() # Retrieve the environment variable api_endpoint = global_state.get_backend_api_ep() # Retrieve the environment variable
@@ -75,11 +82,22 @@ def set_session():
# return 'No user ID found' # return 'No user ID found'
@app.route('/<path:filename>') @app.route('/<path:filename>')
def serve_static(filename): def serve_static(filename: str | Path) -> Response:
"""
Serves a static file from the application's static folder.
Args:
filename (str or os.PathLike[str]): The path to the static file, relative to the STATIC_FOLDER directory.
Returns:
Response: A Flask response containing the contents of the static file.
"""
return send_from_directory(app.config['STATIC_FOLDER'], filename) return send_from_directory(app.config['STATIC_FOLDER'], filename)
# CORS(app, resources={ # CORS(app, resources={
# r"/api/chat": { # r"/api/chat": {
# "origins": "*", # "origins": "*",
@@ -87,33 +105,52 @@ def serve_static(filename):
# } # }
# }) # })
CORS(app, resources={ # CORS(app, resources={
r"/api/chat": { # r"/api/chat": {
"origins": "*" # "origins": "*"
} # }
}) # })
@app.route('/api/tags', methods=['GET']) @app.route('/api/tags', methods=['GET'])
def tag(url = "http://localhost:11434/api/tags", headers = None): def get_tags(url: str = "http://localhost:11434/api/tags", headers: dict = None) -> dict:
"""Get a list of models for the server located at url.""" """
Retrieves a list of available models from a server.
Args:
url (str): The URL of the server to query. Defaults to http://localhost:11434/api/tags.
headers (dict, optional): A dictionary of HTTP headers to include in the request. Defaults to None.
Returns:
dict: A JSON response containing a list of available models, or an error message if the request fails.
Raises:
requests.exceptions.RequestException: If there is a problem with the request.
"""
try: try:
logger.debug(f"url: {url} headers: {headers}") logger.debug(f"url: {url} headers: {headers}")
response = requests.get(url, headers=headers) response = requests.get(url, headers=headers)
return response.json() return response.json()
# return response
except requests.exceptions.RequestException as e: except requests.exceptions.RequestException as e:
logger.error("Request Exception: %s", str(e)) logger.error("Request Exception: %s", str(e))
return {'error': 'Failed to process request'} return {'error': 'Failed to process request'}
@app.route('/api/chat', methods=['POST']) @app.route('/api/chat', methods=['POST'])
def chat(): def chat() -> dict[str, any]:
# def chat(model = "phi3:mini"):
""" """
This function handles the chat. The frontend client (web browser) calls the Handles chat functionality by sending a query to an LLM server and
backend server through this endpoint (/api/chat) that manage queries returning the response.
to the LLM (Large Language Model) server and it also manages the response
from the LLM server. This endpoint expects a JSON payload with the following structure:
{
'query': str,
'url_server': str (optional),
'model': str (optional)
}
:return: A dictionary containing the LLM's response
""" """
# Get the message from the JSON in the request body # Get the message from the JSON in the request body
data = request.get_json() data = request.get_json()
@@ -138,27 +175,9 @@ def chat():
"stream": False "stream": False
} }
url = url_server url = url_server
headers = get_auth_headers(url)
# TODO: This section should only run when changing to new endpoint... logger.debug(f"Sending request to:\n\turl:\t{url}\n\tmodel:\t{model}")
# begin refactor ###################################
headers = { # Set default header
"Content-Type": "application/json",
}
found_endpoint = False
endpoints = global_state.get_endpoints()
for endpoint in endpoints:
if endpoint["url"] == url: # Look for endpoint with this url
found_endpoint = True
if endpoint["provider"] == "ollama": # Currently only supporting ollama servers
if "requestOptions" in endpoint: # Check if authentication is needed
headers.update({
"Authorization": endpoint["requestOptions"]["headers"]["Authorization"]
})
if found_endpoint == False:
# Raise some error or whatever...
logger.debug(f"Host {url} not found")
# end refactor ###################################
try: try:
url = url + "/api/generate" url = url + "/api/generate"
logger.debug(f"url: {url} headers: {headers}") logger.debug(f"url: {url} headers: {headers}")
@@ -179,17 +198,58 @@ def chat():
@app.route('/api/endpoints', methods=['GET']) @app.route('/api/endpoints', methods=['GET'])
def get_endpoints(): def get_endpoints() -> str:
# Replace this with your actual logic to fetch endpoint data """
endpoints = [ Returns a list of available endpoints with their corresponding LLMs.
{'endpoint': 'endpoint1', 'llm': 'llm1'},
{'endpoint': 'endpoint2', 'llm': 'llm2'}, This endpoint fetches all endpoints and their associated LLMs from the global state,
{'endpoint': 'endpoint3', 'llm': 'llm3'}, then returns them as a JSON response.
{'endpoint': 'endpoint4', 'llm': 'llm4'},
] :return: A JSON string representing a dictionary containing a list of dictionaries,
each representing an endpoint title and supported LLM.
"""
endpoints = [] # List of dictionaries, each of which contains {'title': 'title1', 'llm': 'llm1'}
eps = global_state.get_endpoints()
for ep in eps:
llms = global_state.get_list_of_available_llms(ep)
for llm in llms:
endpoints.append({'title': ep.get('title'), 'llm': llm})
return jsonify(endpoints) return jsonify(endpoints)
@app.route('/api/select_endpoint_llm', methods=['POST'])
def select_endpoint_llm() -> Response:
"""
Selects the endpoint associated with the tuple (title, LLM) from the request body.
Request Body:
- title: str - The title of the endpoint to select.
- llm: str - The LLM to set.
Returns:
A JSON response indicating whether the endpoint and LLM were selected successfully.
Raises:
ValueError: If there is not exactly one endpoint with the specified title.
"""
data = request.get_json()
title = data['title']
llm = data['llm']
endpoints = global_state.get_endpoints_with_key_value('title', title)
if len(endpoints) != 1:
raise ValueError(f"Expected exactly one endpoint with title '{title}', found {len(endpoints)}")
# Reset the session
if (title != global_state.get_host_title()) or (llm != global_state.get_llm()): # A change in setting
session.clear()
logger.debug('Session cleared due to changed endpoint or changed LLM')
global_state.set_host_url(endpoints[0]['url'])
global_state.set_llm(llm)
logger.debug(f"Updated to host url {endpoints[0]['url']} and LLM {llm}")
return jsonify({'message': 'New endpoint and/or LLM detected, settings were changed successfully'})
else:
return jsonify({'message': 'Endpoint and LLM are untouched'})
@app.route('/smartassist', methods=["POST"]) @app.route('/smartassist', methods=["POST"])
def smartassist(): def smartassist():
@@ -209,6 +269,35 @@ def get_response(user_query):
response = client.generate_response(user_query) # Generate and retrieve the response based on user's query response = client.generate_response(user_query) # Generate and retrieve the response based on user's query
return response return response
def get_auth_headers(url: str) -> dict:
"""
Returns authentication headers for a given URL.
This function checks if an endpoint with the provided URL exists in the global state,
and returns the corresponding authentication headers. If no such endpoint is found,
it returns a default header.
"""
# TODO: The full operation should only have to run when changing to new endpoint.
# Set default header
headers = {
"Content-Type": "application/json",
}
found_endpoint = False
endpoints = global_state.get_endpoints()
for endpoint in endpoints:
if endpoint["url"] == url: # Look for endpoint with this URL
found_endpoint = True
#if endpoint["provider"] == "ollama": # Currently only supporting ollama servers - not needed if API the same
if "requestOptions" in endpoint: # Check if authentication is needed
headers.update({
"Authorization": endpoint["requestOptions"]["headers"]["Authorization"]
})
if not found_endpoint:
logger.debug(f"Host {url} not found")
return headers
def run_flask(fport=5005): def run_flask(fport=5005):
""" """
+3 -7
View File
@@ -2,6 +2,7 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Ollama Chat</title> <title>Ollama Chat</title>
<link rel="stylesheet" href="/css/clientstyle.css"> <link rel="stylesheet" href="/css/clientstyle.css">
<!-- <link rel="stylesheet" href="python_test/smartassist/src/css/clientstyle.css"> --> <!-- <link rel="stylesheet" href="python_test/smartassist/src/css/clientstyle.css"> -->
@@ -10,14 +11,9 @@
<body> <body>
<h1>Ollama Chat</h1> <h1>Ollama Chat</h1>
<!-- Add the dropdown menu here -->
<div class="dropdown"> <div class="dropdown">
<button class="dropbtn">Select Endpoint/LLM</button> <button class="dropbtn" id="selected-endpoint">Select Endpoint/LLM</button>
<div class="dropdown-content"> <div class="dropdown-content" id="endpoint-dropdown"></div>
<a href="#" onclick="frontendApi.setEndpointAndLlm('endpoint1', 'llm1')">Endpoint 1 - LLM 1</a>
<a href="#" onclick="frontendApi.setEndpointAndLlm('endpoint2', 'llm2')">Endpoint 2 - LLM 2</a>
<a href="#" onclick="frontendApi.setEndpointAndLlm('endpoint3', 'llm3')">Endpoint 3 - LLM 3</a>
</div>
</div> </div>
<div id="chatbox"> <div id="chatbox">
+3 -2
View File
@@ -94,9 +94,10 @@ button[onclick="window.frontendApi.sendMessage()"]:active {
display: none; display: none;
position: absolute; position: absolute;
background-color: #f9f9f9; background-color: #f9f9f9;
min-width: 160px; /* min-width: 160px; */
box-shadow: 0px 8px 16px 0px rgba(0,0,0,0.2); box-shadow: 0px 8px 16px 0px rgba(0,0,0,0.2);
z-index: 1; z-index: 1;
width: auto; /* Add this property */
} }
.dropdown-content a { .dropdown-content a {
@@ -107,7 +108,7 @@ button[onclick="window.frontendApi.sendMessage()"]:active {
display: block; display: block;
font-size: 0.7rem; /* Decrease font size relative to root element */ font-size: 0.7rem; /* Decrease font size relative to root element */
line-height: 0.5; /* Decrease line height to reduce spacing */ line-height: 0.5; /* Decrease line height to reduce spacing */
white-space: nowrap; /* Add this property */
} }
.dropdown-content a:hover {background-color: #f1f1f1;} .dropdown-content a:hover {background-color: #f1f1f1;}
+65 -6
View File
@@ -25,6 +25,7 @@ const frontendApi = {
fetch(window.apiEndpoint, { fetch(window.apiEndpoint, {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
// body: JSON.stringify({ query }), // Add these parameters here
body: JSON.stringify({ query, model: window.useModel }), // Add these parameters here body: JSON.stringify({ query, model: window.useModel }), // Add these parameters here
}) })
.then(response => response.json()) .then(response => response.json())
@@ -53,13 +54,49 @@ const frontendApi = {
// Append the message element to the chatbox immediately // Append the message element to the chatbox immediately
chatbox.appendChild(messageElement); chatbox.appendChild(messageElement);
}, },
// Set the endpoint and LLM variables // Make an AJAX request to fetch endpoint data from Flask backend
setEndpointAndLlm: function(endpoint, llm) { fillMenu: function() {
window.endpoint = endpoint; fetch('/api/endpoints')
window.llm = llm; .then(response => response.json())
console.log(`Selected Endpoint: ${endpoint}, LLM: ${llm}`); .then(data => {
const dropdownContainer = document.getElementById('endpoint-dropdown');
// Clear existing content
dropdownContainer.innerHTML = '';
// Populate the dropdown menu with received data
data.forEach(endpoint => {
const linkElement = document.createElement('a');
linkElement.href = ''; // If attribute is set to '#', browser scrolls to top of page and reload
linkElement.onclick = () => frontendApi.setEndpointAndLlm(endpoint.title, endpoint.llm);
linkElement.textContent = `${endpoint.title} - ${endpoint.llm}`;
dropdownContainer.appendChild(linkElement);
});
})
.catch(error => console.error('Error fetching endpoints:', error));
}, },
}; // Set the endpoint (remember, endpoint here is the 'title' of endpoin) and LLM variables
setEndpointAndLlm: function(title, llm) {
window.endpointTitle = title;
window.useModel = llm;
// Lets tell Flask about the new setting
fetch('/api/select_endpoint_llm', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title, llm }),
// body: JSON.stringify(`${{ title, llm }}`),
})
.then(response => response.json())
.then(data => {
// If everything went well, let's tell frontend about it
message = data.message;
console.log(message)
console.log(`Selected endpoint title: ${title}, LLM: ${llm}`);
})
.catch(error => console.error('Error setting endpoint and LLM:', error));
},
}
// Wait for the event listener to set apiEndpoint and useModel // Wait for the event listener to set apiEndpoint and useModel
window.addEventListener('message', function(event) { window.addEventListener('message', function(event) {
@@ -89,3 +126,25 @@ document.addEventListener('keyup', function() {
sendButton.style.backgroundColor = ''; // Restore the original style when any key is released sendButton.style.backgroundColor = ''; // Restore the original style when any key is released
}); });
// Get the dropdown button and the dropdown content elements
const dropbtn = document.getElementById('selected-endpoint');
const dropdownContent = document.getElementById('endpoint-dropdown');
// Add event listeners to each dropdown item
dropdownContent.addEventListener('click', (e) => {
if (e.target.tagName === 'A') { // Only respond to clicks on anchor tags
e.preventDefault(); // Prevent default link behavior, i.e., do NOT navigate to the link's URL when clicked
const selectedEndpoint = e.target.textContent;
dropbtn.textContent = selectedEndpoint; // Update the button's text
// You can also add code here to update the current endpoint in your application
}
});
function init() {
// Other initialization code here...
frontendApi.fillMenu();
}
document.addEventListener('DOMContentLoaded', init);
+15 -3
View File
@@ -123,13 +123,26 @@ class GlobalState:
def get_host_url(self) -> str: def get_host_url(self) -> str:
""" """
Get the current URL of the host used for LLMs. Get the URL of the host currently used for LLMs.
Returns: Returns:
str: The current URL of the host. str: The URL of the current host.
""" """
return self.host_url return self.host_url
def get_host_title(self) -> str:
"""
Get the title of the host currently used for LLMs.
There must be a 1-to-1 mapping from host_url to host_title.
Returns:
str: The title of the current host.
"""
endpoints = self.get_endpoints_with_key_value('url', self.get_host_url())
if len(endpoints) != 1:
raise ValueError(f"Expected exactly one endpoint with url '{self.get_host_url()}', found {len(endpoints)}")
return endpoints[0]["title"]
def set_llm(self, model_name: str = "phi3:mini") -> None: def set_llm(self, model_name: str = "phi3:mini") -> None:
""" """
Set the LLM to use for queries. Set the LLM to use for queries.
@@ -268,7 +281,6 @@ class GlobalState:
return # No value returned return # No value returned
def get_list_of_available_llms(self, endpoint: Optional[dict] = None) -> Optional[list[str]]: def get_list_of_available_llms(self, endpoint: Optional[dict] = None) -> Optional[list[str]]:
""" """
Returns a sorted list of Large Language Models (LLMs) available at the specified endpoint. Returns a sorted list of Large Language Models (LLMs) available at the specified endpoint.
+76
View File
@@ -0,0 +1,76 @@
import os
import requests
from bs4 import BeautifulSoup
from urllib.parse import urljoin, urlparse
import base64
import re
def download_image(url, folder_path):
if not os.path.isdir(folder_path):
os.makedirs(folder_path)
try:
response = requests.get(url, stream=True)
response.raise_for_status() # Kontrollera om förfrågan lyckades
except requests.RequestException as e:
print(f"Failed to retrieve image {url}: {e}")
return
filename = os.path.join(folder_path, os.path.basename(urlparse(url).path))
with open(filename, 'wb') as file:
for chunk in response.iter_content(1024):
file.write(chunk)
print(f"Downloaded: {filename}")
def save_base64_image(data_url, folder_path, count):
if not os.path.isdir(folder_path):
os.makedirs(folder_path)
match = re.match(r'data:image/(?P<ext>[^;]+);base64,(?P<data>.+)', data_url)
if match:
ext = match.group('ext')
data = match.group('data')
img_data = base64.b64decode(data)
filename = os.path.join(folder_path, f'image_{count}.{ext}')
with open(filename, 'wb') as file:
file.write(img_data)
print(f"Downloaded: {filename}")
else:
print(f"Invalid base64 image data: {data_url}")
def download_all_images(html_content, base_url, folder_path):
soup = BeautifulSoup(html_content, 'html.parser')
img_tags = soup.find_all('img')
count = 0
for img in img_tags:
img_url = img.get('src')
if not img_url:
continue
if img_url.startswith(('http://', 'https://')):
img_url = urljoin(base_url, img_url)
print(f"Attempting to download image: {img_url}")
download_image(img_url, folder_path)
elif img_url.startswith('data:image/'):
print(f"Attempting to save base64 image: {img_url[:30]}...") # Print only the start of the data URL
count += 1
save_base64_image(img_url, folder_path, count)
else:
print(f"Ignoring non-http URL: {img_url}")
def main():
url = input("Enter the URL of the webpage: ")
folder_path = os.path.expanduser("~/Downloads/downloaded_images")
try:
response = requests.get(url)
response.raise_for_status() # Kontrollera om förfrågan lyckades
except requests.RequestException as e:
print(f"Failed to retrieve webpage {url}: {e}")
return
download_all_images(response.content, url, folder_path)
if __name__ == "__main__":
main()
+62
View File
@@ -0,0 +1,62 @@
import os
import re
from bs4 import BeautifulSoup
import requests
from urllib.parse import urljoin
import base64
def ladda_ner_bilder(url):
# Hämta HTML-sidan
svar = requests.get(url)
soup = BeautifulSoup(svar.text, 'html.parser')
# Hitta alla bilder
bilder = []
for img in soup.find_all('img'):
src = img.get('src')
if src:
bilder.append(src)
# Hantera inline-bilder i base64
INLINE_BILD_MÖNSTER = r'data:image/(.*?);base64,(.*)'
matcher = re.compile(INLINE_BILD_MÖNSTER)
for match in matcher.finditer(svar.text):
bild_typ = match.group(1)
bild_data = match.group(2)
bilder.append(f"data:{bild_typ};base64,{bild_data}")
# Ladda ner bilderna
bild_katalog = os.path.expanduser("~/Downloads/bilder")
if not os.path.exists(bild_katalog):
os.makedirs(bild_katalog)
for bild_url in bilder:
if not bild_url.startswith('http'):
bild_url = urljoin(url, bild_url)
if bild_url.startswith('data:'):
# Dekodera base64-strängen och spara den som en bild
format, data = bild_url.split(';base64,')
data = base64.b64decode(data)
filnamn = 'inline_' + str(len(bilder)) + '.gif'
with open(os.path.join(bild_katalog, filnamn), 'wb') as f:
f.write(data)
else:
svar = requests.get(bild_url)
if svar.status_code == 200:
filnamn = os.path.basename(bild_url).split('?')[0]
with open(os.path.join(bild_katalog, filnamn), 'wb') as f:
f.write(svar.content)
print(f"Bilden {filnamn} har laddats ner till {bild_katalog}.")
def main():
url = input("Ange URL till sidan från vilken du vill hämta bilder: ")
if not url.startswith('http'):
url = 'http://' + url
try:
ladda_ner_bilder(url)
except Exception as e:
print(f"Fel inträffade: {e}")
if __name__ == "__main__":
main()
+2
View File
@@ -0,0 +1,2 @@
BeautifulSoup4
requests