Compare commits
29 Commits
c7630bf6b3
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 82c7887cf7 | |||
| 5f546a848e | |||
| 7eb889a889 | |||
| 6dab09c861 | |||
| 8288450662 | |||
| ae67b9c7b3 | |||
| 97ee179b29 | |||
| 942f9f78c9 | |||
| 3f39e11b10 | |||
| 32098e3452 | |||
| 5b143e75e0 | |||
| 168b8b13c1 | |||
| f7f6ce2e49 | |||
| fa98c7b162 | |||
| fd5f6199e9 | |||
| c26dbc5612 | |||
| 7f557fadd6 | |||
| 5ce92a5602 | |||
| 6dc93b66be | |||
| 606becc5c3 | |||
| 9717202bb4 | |||
| dc209b3595 | |||
| 8ac365862a | |||
| ab9bb1324c | |||
| 17c20a4ce8 | |||
| 210a75e8bf | |||
| 01d4a5f314 | |||
| 4621cf6cbf | |||
| 56f9038e6c |
@@ -162,3 +162,4 @@ cython_debug/
|
||||
|
||||
# Exclude venv from smartassist
|
||||
smartassist/smartassist_dev_venv
|
||||
.DS_Store
|
||||
|
||||
@@ -3,36 +3,23 @@ backend:
|
||||
url: "http://localhost:5004"
|
||||
api: "/api/chat"
|
||||
|
||||
preferred_ep: "Ollama-WARA"
|
||||
|
||||
endpoints:
|
||||
- model: "AUTODETECT"
|
||||
title: "Ollama-local"
|
||||
title: "Ollama-local" # Must be a unique identifier
|
||||
url: "http://localhost:11434"
|
||||
provider: "ollama"
|
||||
# - model: "AUTODETECT"
|
||||
- model: "AUTODETECT"
|
||||
title: "Ollama-WARA"
|
||||
- model: "llava:13b"
|
||||
title: "Ollama-WARA" # Must be a unique identifier
|
||||
url: "https://ollama-test.wara-ops.org"
|
||||
requestOptions:
|
||||
headers:
|
||||
Authorization: "${OLLAMA_API_KEY}" # on MacOS: echo "Authorization: Basic $(echo -n 'user:password' | gbase64 -w 0)"
|
||||
provider: "ollama"
|
||||
|
||||
# Ollama Server Configuration – this section is to be removed when endpoints are being parsed correctly
|
||||
ollama:
|
||||
title: "Ollama-local"
|
||||
# url: "http://localhost:11434"
|
||||
url: "https://ollama-test.wara-ops.org"
|
||||
api_key: "${OLLAMA_API_KEY}" # Refer to environment variable
|
||||
# model: "phi3:mini" # Select a model supported by the Ollama server
|
||||
# model: "llama3:70b" # Select a model supported by the Ollama server
|
||||
model: "llama3.1:70b" # Select a model supported by the Ollama server
|
||||
# model: "llama3.1:8b" # Select a model supported by the Ollama server
|
||||
# model: "llama3:latest" # Select a model supported by the Ollama server
|
||||
# model: "mannix/llama3-8b-ablitered-v3:latest" # Select a model supported by the Ollama server
|
||||
# model: "mistral-nemo:latest" # Select a model supported by the Ollama server
|
||||
# model: "gemma2:27b"
|
||||
# model: "AUTODETECT"
|
||||
|
||||
|
||||
# Logging – comment out the whole section for default level which is INFO
|
||||
logging:
|
||||
|
||||
+140
-39
@@ -1,7 +1,7 @@
|
||||
|
||||
# Import the necessary functions from ollama, Flask, requests, threading
|
||||
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.
|
||||
import requests
|
||||
import json
|
||||
@@ -9,6 +9,7 @@ import logging
|
||||
import os
|
||||
import utils
|
||||
from utils import GlobalState
|
||||
from pathlib import Path
|
||||
|
||||
# Create a logger for this module
|
||||
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)
|
||||
|
||||
@app.route('/')
|
||||
def index():
|
||||
"""
|
||||
This route serves index.html to connecting clients
|
||||
def index() -> Response:
|
||||
"""
|
||||
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
|
||||
logger.debug("Entering route '/'")
|
||||
api_endpoint = global_state.get_backend_api_ep() # Retrieve the environment variable
|
||||
@@ -75,11 +82,22 @@ def set_session():
|
||||
# return 'No user ID found'
|
||||
|
||||
|
||||
|
||||
@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)
|
||||
|
||||
|
||||
|
||||
# CORS(app, resources={
|
||||
# r"/api/chat": {
|
||||
# "origins": "*",
|
||||
@@ -87,33 +105,52 @@ def serve_static(filename):
|
||||
# }
|
||||
# })
|
||||
|
||||
CORS(app, resources={
|
||||
r"/api/chat": {
|
||||
"origins": "*"
|
||||
}
|
||||
})
|
||||
# CORS(app, resources={
|
||||
# r"/api/chat": {
|
||||
# "origins": "*"
|
||||
# }
|
||||
# })
|
||||
|
||||
@app.route('/api/tags', methods=['GET'])
|
||||
def tag(url = "http://localhost:11434/api/tags", headers = None):
|
||||
"""Get a list of models for the server located at url."""
|
||||
def get_tags(url: str = "http://localhost:11434/api/tags", headers: dict = None) -> dict:
|
||||
"""
|
||||
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:
|
||||
logger.debug(f"url: {url} headers: {headers}")
|
||||
response = requests.get(url, headers=headers)
|
||||
return response.json()
|
||||
# return response
|
||||
except requests.exceptions.RequestException as e:
|
||||
logger.error("Request Exception: %s", str(e))
|
||||
return {'error': 'Failed to process request'}
|
||||
|
||||
|
||||
|
||||
|
||||
@app.route('/api/chat', methods=['POST'])
|
||||
def chat():
|
||||
# def chat(model = "phi3:mini"):
|
||||
def chat() -> dict[str, any]:
|
||||
"""
|
||||
This function handles the chat. The frontend client (web browser) calls the
|
||||
backend server through this endpoint (/api/chat) that manage queries
|
||||
to the LLM (Large Language Model) server and it also manages the response
|
||||
from the LLM server.
|
||||
Handles chat functionality by sending a query to an LLM server and
|
||||
returning the response.
|
||||
|
||||
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
|
||||
data = request.get_json()
|
||||
@@ -138,27 +175,9 @@ def chat():
|
||||
"stream": False
|
||||
}
|
||||
url = url_server
|
||||
headers = get_auth_headers(url)
|
||||
|
||||
# TODO: This section should only run when changing to new endpoint...
|
||||
# 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 ###################################
|
||||
|
||||
logger.debug(f"Sending request to:\n\turl:\t{url}\n\tmodel:\t{model}")
|
||||
try:
|
||||
url = url + "/api/generate"
|
||||
logger.debug(f"url: {url} headers: {headers}")
|
||||
@@ -178,6 +197,59 @@ def chat():
|
||||
return jsonify({'error': 'Invalid JSON response from server'}), 500
|
||||
|
||||
|
||||
@app.route('/api/endpoints', methods=['GET'])
|
||||
def get_endpoints() -> str:
|
||||
"""
|
||||
Returns a list of available endpoints with their corresponding LLMs.
|
||||
|
||||
This endpoint fetches all endpoints and their associated LLMs from the global state,
|
||||
then returns them as a JSON response.
|
||||
|
||||
: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)
|
||||
|
||||
@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"])
|
||||
def smartassist():
|
||||
@@ -197,6 +269,35 @@ def get_response(user_query):
|
||||
response = client.generate_response(user_query) # Generate and retrieve the response based on user's query
|
||||
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):
|
||||
"""
|
||||
|
||||
@@ -8,3 +8,11 @@ class LogLevel(Enum):
|
||||
WARNING = 'WARNING'
|
||||
ERROR = 'ERROR'
|
||||
CRITICAL = 'CRITICAL'
|
||||
|
||||
LOG_LEVEL_MAPPING = {
|
||||
LogLevel.DEBUG: 10,
|
||||
LogLevel.INFO: 20,
|
||||
LogLevel.WARNING: 30,
|
||||
LogLevel.ERROR: 40,
|
||||
LogLevel.CRITICAL: 50
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Ollama Chat</title>
|
||||
<link rel="stylesheet" href="/css/clientstyle.css">
|
||||
<!-- <link rel="stylesheet" href="python_test/smartassist/src/css/clientstyle.css"> -->
|
||||
@@ -9,6 +10,12 @@
|
||||
|
||||
<body>
|
||||
<h1>Ollama Chat</h1>
|
||||
|
||||
<div class="dropdown">
|
||||
<button class="dropbtn" id="selected-endpoint">Select Endpoint/LLM</button>
|
||||
<div class="dropdown-content" id="endpoint-dropdown"></div>
|
||||
</div>
|
||||
|
||||
<div id="chatbox">
|
||||
<!-- messages will be rendered here -->
|
||||
</div>
|
||||
|
||||
@@ -10,6 +10,7 @@ import logging
|
||||
import requests
|
||||
import utils
|
||||
from utils import GlobalState
|
||||
from enums import LogLevel
|
||||
|
||||
global_state = GlobalState() # Configure root logger. The level will be adjusted later based on config file
|
||||
logger = global_state.get_logger(__name__) # Logger for this module, inherit properties of the root logger
|
||||
@@ -56,8 +57,9 @@ def configure():
|
||||
if isinstance(updated_config.get('logging'), dict): # Look for 'logging' key in config file
|
||||
logging_config = updated_config['logging']
|
||||
if isinstance(logging_config.get('level'), str): # Set to value of the yaml file if specified
|
||||
global_state.set_log_level(logging_config['level'])
|
||||
logger.debug("configure(): This logger now has effective log level %s", logger.getEffectiveLevel())
|
||||
# global_state.set_log_level(logging_config['level'])
|
||||
global_state.set_log_level(LogLevel(logging_config['level']))
|
||||
logger.info("configure(): This logger now has effective log level %s", logger.getEffectiveLevel())
|
||||
|
||||
####################################
|
||||
# Extract models (server url, api_key, model, et cetera)
|
||||
@@ -67,6 +69,8 @@ def configure():
|
||||
logger.debug("backend = \n{}".format(json.dumps(global_state.get_backend(), indent=4)))
|
||||
logger.debug(f"Backend API endpoint is set to: {global_state.get_backend_api_ep()}")
|
||||
|
||||
preferred_ep = updated_config.get('preferred_ep', None) # Get the preferred endpoint if specified, otherwise None
|
||||
|
||||
if isinstance(updated_config.get('endpoints'), list): # Extract info on endpoint, model, url, provider et cetera from list
|
||||
global_state.set_endpoints(endpoints=updated_config.get('endpoints')) # Extract and set list of endpoints
|
||||
# logger.debug("endpoints = \n{}".format(json.dumps(global_state.get_endpoints(), indent=4)))
|
||||
@@ -80,7 +84,11 @@ def configure():
|
||||
llm = endpoint["model"]
|
||||
endpoint["default_llm"] = llm
|
||||
|
||||
default_endpoint = next(iter(endpoints),None) # Set default_endpoint to first endpoint from list of endpoints
|
||||
if preferred_ep: # If preferred_ep is specified, set it as the default endpoint
|
||||
list_of_eps = global_state.get_endpoints_with_key_value("title", preferred_ep) # Should only be one element in the list...
|
||||
default_endpoint = next(iter(list_of_eps),None) # Same as default_endpoint = list_of_eps[0] if list_of_eps else None
|
||||
else:
|
||||
default_endpoint = next(iter(endpoints),None) # Set default_endpoint to first endpoint from list of all endpoints
|
||||
default_llm = default_endpoint["default_llm"] # Get default LLM for default_endpoint
|
||||
default_ulr = default_endpoint["url"] # Get ulr of default_endpoint
|
||||
global_state.set_host_url(default_ulr) # Set initial host to the first item in endpoints (or None)
|
||||
|
||||
@@ -26,6 +26,7 @@ h1 {
|
||||
resize: both; /* Allow resizing vertically */
|
||||
border: 1px solid #ccc; /* Add a thin grey border around chatbox */
|
||||
margin-bottom: 20px; /* Add some space between chatbox and userInput */
|
||||
font-size: 14px; /* Decrease font size to 14 pixels */
|
||||
}
|
||||
|
||||
.message {
|
||||
@@ -83,3 +84,35 @@ button[onclick="window.frontendApi.sendMessage()"]:active {
|
||||
background-color: #6f6f6f; /* Dark Grey when clicked */
|
||||
}
|
||||
|
||||
|
||||
.dropdown {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.dropdown-content {
|
||||
display: none;
|
||||
position: absolute;
|
||||
background-color: #f9f9f9;
|
||||
/* min-width: 160px; */
|
||||
box-shadow: 0px 8px 16px 0px rgba(0,0,0,0.2);
|
||||
z-index: 1;
|
||||
width: auto; /* Add this property */
|
||||
}
|
||||
|
||||
.dropdown-content a {
|
||||
color: black;
|
||||
/* padding: 12px 16px; */
|
||||
padding: 6px 8px;
|
||||
text-decoration: none;
|
||||
display: block;
|
||||
font-size: 0.7rem; /* Decrease font size relative to root element */
|
||||
line-height: 0.5; /* Decrease line height to reduce spacing */
|
||||
white-space: nowrap; /* Add this property */
|
||||
}
|
||||
|
||||
.dropdown-content a:hover {background-color: #f1f1f1;}
|
||||
|
||||
.dropdown:hover .dropdown-content {
|
||||
display: block;
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ const frontendApi = {
|
||||
fetch(window.apiEndpoint, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
// body: JSON.stringify({ query }), // Add these parameters here
|
||||
body: JSON.stringify({ query, model: window.useModel }), // Add these parameters here
|
||||
})
|
||||
.then(response => response.json())
|
||||
@@ -53,7 +54,49 @@ const frontendApi = {
|
||||
// Append the message element to the chatbox immediately
|
||||
chatbox.appendChild(messageElement);
|
||||
},
|
||||
};
|
||||
// Make an AJAX request to fetch endpoint data from Flask backend
|
||||
fillMenu: function() {
|
||||
fetch('/api/endpoints')
|
||||
.then(response => response.json())
|
||||
.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
|
||||
window.addEventListener('message', function(event) {
|
||||
@@ -82,3 +125,26 @@ document.addEventListener('keydown', function(event) {
|
||||
document.addEventListener('keyup', function() {
|
||||
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);
|
||||
|
||||
|
||||
|
||||
+39
-31
@@ -5,7 +5,7 @@ import logging
|
||||
import json
|
||||
import requests
|
||||
from typing import Optional
|
||||
from enums import LogLevel
|
||||
from enums import LogLevel, LOG_LEVEL_MAPPING
|
||||
|
||||
class GlobalState:
|
||||
"""
|
||||
@@ -43,50 +43,34 @@ class GlobalState:
|
||||
|
||||
return cls._instance
|
||||
|
||||
# def configure_logging(self, level: Optional[LogLevel] = None) -> None:
|
||||
# """
|
||||
# Configure the logging system for this project.
|
||||
|
||||
# Args:
|
||||
# level (LogLevel): The log level to use. If None, uses the default log level set in `self.log_level`.
|
||||
|
||||
# Notes:
|
||||
# This method sets up logging for the project and logs a message at the debug level indicating the effective log level.
|
||||
# """
|
||||
def configure_logging(self, level: Optional[str] = None) -> None:
|
||||
|
||||
def configure_logging(self, level: Optional[LogLevel] = None) -> None:
|
||||
"""
|
||||
Configure the logging system for this project.
|
||||
|
||||
Args:
|
||||
level (str): The log level to use. Can be one of the standard Python log levels (e.g., 'DEBUG', 'INFO', 'WARNING', etc.). If None, uses the default log level set in `self.log_level`.
|
||||
level (LogLevel): The log level to use. If None, uses the default log level set in `self.log_level`.
|
||||
|
||||
Notes:
|
||||
This method sets up logging for the project and logs a message at the debug level indicating the effective log level.
|
||||
"""
|
||||
if level is None:
|
||||
if level == None:
|
||||
level = self.log_level
|
||||
# numeric_level = getattr(logging, level.upper()) # Convert string to numeric level
|
||||
numeric_level = getattr(logging, level.upper()) # Convert string to numeric level
|
||||
if isinstance(level, LogLevel):
|
||||
logging.info(f"Trying to set up logging with level {level}")
|
||||
numeric_level = LOG_LEVEL_MAPPING[level]
|
||||
if numeric_level is None:
|
||||
raise ValueError("Invalid log level")
|
||||
self.logger.setLevel(numeric_level)
|
||||
self.logger.debug(f"utils.py -- configure_logging(): effective log level is {level} which is {self.logger.getEffectiveLevel()}")
|
||||
|
||||
# def set_log_level(self, level: LogLevel) -> None:
|
||||
# """
|
||||
# Set the log level for this project.
|
||||
|
||||
# Args:
|
||||
# level (LogLevel): The new log level to use. Can be one of the evels defined in enum.py (e.g., DEBUG, INFO, WARNING, CRITICAL etc.).
|
||||
|
||||
# Notes:
|
||||
# This method updates the `self.log_level` attribute and calls `configure_logging()` to apply the change.
|
||||
# """
|
||||
def set_log_level(self, level: str = 'INFO') -> None:
|
||||
def set_log_level(self, level: LogLevel) -> None:
|
||||
"""
|
||||
Set the log level for this project.
|
||||
|
||||
Args:
|
||||
level (str): The new log level to use. Can be one of the standard Python log levels (e.g., 'DEBUG', 'INFO', 'WARNING', etc.).
|
||||
level (LogLevel): The new log level to use. Can be one of the evels defined in enum.py (e.g., DEBUG, INFO, WARNING, CRITICAL etc.).
|
||||
|
||||
Notes:
|
||||
This method updates the `self.log_level` attribute and calls `configure_logging()` to apply the change.
|
||||
@@ -94,7 +78,7 @@ class GlobalState:
|
||||
self.log_level = level
|
||||
self.configure_logging()
|
||||
|
||||
def get_log_level(self) -> str:
|
||||
def get_log_level(self) -> LogLevel:
|
||||
"""
|
||||
Get the current log level.
|
||||
|
||||
@@ -139,13 +123,26 @@ class GlobalState:
|
||||
|
||||
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:
|
||||
str: The current URL of the host.
|
||||
str: The URL of the current host.
|
||||
"""
|
||||
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:
|
||||
"""
|
||||
Set the LLM to use for queries.
|
||||
@@ -232,6 +229,18 @@ class GlobalState:
|
||||
"""
|
||||
return [ep for ep in self.endpoints if key in ep]
|
||||
|
||||
def get_endpoints_with_key_value(self, key: str, value: any) -> list[dict]:
|
||||
"""
|
||||
Returns a list of endpoint dictionaries that contain the specified key-value pair.
|
||||
|
||||
Args:
|
||||
key (str): The key to search for in the endpoint dictionaries.
|
||||
value (Any): The value to search for in the endpoint dictionaries.
|
||||
|
||||
Returns:
|
||||
list[dict]: A list of endpoint dictionaries containing the specified key.
|
||||
"""
|
||||
return [ep for ep in self.endpoints if key in ep and value == ep[key]]
|
||||
|
||||
def fetch_models(self) -> None:
|
||||
"""
|
||||
@@ -272,7 +281,6 @@ class GlobalState:
|
||||
|
||||
return # No value returned
|
||||
|
||||
|
||||
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.
|
||||
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -0,0 +1,2 @@
|
||||
BeautifulSoup4
|
||||
requests
|
||||
Reference in New Issue
Block a user