44 Commits

Author SHA1 Message Date
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
Joakim Persson 9717202bb4 Lade till ett skelett för /api/endpoints för att dynamiskt kunna fylla listan med möjliga val i client.html 2024-08-05 17:10:15 +02:00
Joakim Persson dc209b3595 Lade till setEndpointAndLlm() till frontendApi() 2024-08-05 16:59:58 +02:00
Joakim Persson 8ac365862a Platshållare för meny. För tillfället är innehållet statiskt 2024-08-05 15:40:00 +02:00
Joakim Persson ab9bb1324c Lagt till dropdown-meny för val av endpoint och LLM 2024-08-05 15:38:15 +02:00
Joakim Persson 17c20a4ce8 Bytt namn: get_endpoints_with_key_values() > get_endpoints_with_key_value() 2024-08-05 14:19:47 +02:00
Joakim Persson 210a75e8bf Använder numer LogLevel för att hantera nivån på loggar. Lade till get_endpoints_with_key_values() 2024-08-05 14:18:29 +02:00
Joakim Persson 01d4a5f314 Använder nu LogLevels. Väljer endpoint baserat på preferred_ep i konfigurationsfilen 2024-08-05 14:16:00 +02:00
Joakim Persson 4621cf6cbf Lade till mappning till numeriska värden som överenstämmer med standardmappnng för loggning 2024-08-05 14:11:12 +02:00
Joakim Persson 56f9038e6c Tog bort oanvänd konfiguration. Lade till preferred_ep 2024-08-05 14:10:21 +02:00
joakimp c7630bf6b3 Utförligare beskrivningar (docstrings) och lagt till "type hinting". Nya metod get_endpoints_with_key(). Döpt om getLogger till get_logger 2024-08-05 00:55:05 +02:00
joakimp 0db750358e getLogger > get_logger pga namnbyte 2024-08-05 00:51:44 +02:00
joakimp 8452c7569b Nu används informationen från konfigurationsfilen när url för LLM-server bestäms 2024-08-05 00:50:45 +02:00
joakimp cb1caceee7 Added to a comment 2024-08-05 00:48:05 +02:00
joakimp 9e86617ae9 Borttagen, ersatt av fil med annat namn (enums.py) 2024-08-04 23:40:42 +02:00
joakimp c0b97871e7 Bytt namn till pluralform 2024-08-04 23:39:51 +02:00
joakimp 44893fce39 Fil med användbara konstanter 2024-08-04 23:36:02 +02:00
joakimp a4189360d1 Anpassat anrop för att hämta lista av endpoints till den nya klassmetoden. Sätter url och llm enligt första modellen i första endpoint i yaml-konfigurationen. 2024-08-04 15:26:46 +02:00
joakimp 8e983919e5 Refakrotiserat så att den externa funktionen fetch_models_from_endpoints() gjorts om till klassmetoden fetch_models() 2024-08-04 11:48:38 +02:00
joakimp 9e2cebd6cc Bytte titeln för lokal endpoint 2024-08-04 11:47:15 +02:00
joakimp 352e704537 Lagt till hantering av host_url där LLM:er körs. 2024-08-04 01:16:40 +02:00
joakimp 76f20e5cf8 Tagit bort utkommenterad kod 2024-08-04 01:12:12 +02:00
joakimp cf9fcc46dd Extraherar tillgängliga LLM:er för varje endpoint i yaml-filen. Rensat ut kommentarer och oanvänd kod. 2024-08-04 01:11:20 +02:00
joakimp f160092b2a Ökat på storleken på sessionskakan för att se om det tar bort varningen om att den är större än 4096 byte. 2024-08-03 18:45:50 +02:00
joakimp 06628d5c19 Lagt till funktion som hämtar information om tillgängliga LLM från endpoint-servrar 2024-08-03 18:44:35 +02:00
joakimp 2e24be1e44 Flyttat ut kod för att hämta data från endpoints till utils.py 2024-08-03 18:43:56 +02:00
joakimp 41c0c86d82 Tog bort en onödig nästling av dictionary för "models" 2024-08-03 00:52:55 +02:00
Joakim Persson 5aa47d11ea Hämtar lista över tillgängliga modeller från ollama-servrarna i yaml-konfigurationen 2024-08-02 23:10:37 +02:00
Joakim Persson 5f2b71c965 Lagt till /api/tags för att kunna få ut lista av tillgängliga modeller (LLM:er) på aktuell ollama-server 2024-08-02 23:09:15 +02:00
Joakim Persson 3659308675 Ändrat namn på metoder, *models > *endpoints 2024-08-02 23:05:49 +02:00
Joakim Persson a72a46d777 Bytt till "endpoints" från "models" för att unvika upprepning av samma namn på olika nivåer av konfigurationsfilen 2024-08-02 23:04:11 +02:00
joakimp a7ce92524a Extraherar dictionary (backend) och lista (models) direkt från smartassist.yaml 2024-08-02 00:11:45 +02:00
joakimp b52c98c6b3 FIxat syntaxiskt fel: true -> True. Rensat bortkommernterad kod. 2024-08-02 00:10:05 +02:00
joakimp c8269f3152 Rensat onödiga metoder och attribut 2024-08-02 00:07:59 +02:00
Joakim Persson d553deed13 Bara ändrat någon kommentar 2024-08-01 18:21:00 +02:00
Joakim Persson ab23585711 Testar uttökad yaml-konfiguration 2024-08-01 17:04:25 +02:00
Joakim Persson 916b6f9e52 Testar med fjärrserver för ollama. Endast tillfällig lösning. 2024-08-01 17:03:25 +02:00
Joakim Persson cfed550a3e Lagt till backend och modules från yaml-filen 2024-08-01 17:02:20 +02:00
joakimp 5ee7ae520f Förbättrat utläsning av data från konfigurationsfilen smartassist.yaml 2024-07-31 23:24:38 +02:00
joakimp 1f6f0a72d5 Lade till information för ollama-test.wara-ops.org 2024-07-31 23:23:14 +02:00
Joakim Persson 9e6e6048f3 Rensat bort utkommenterad kod 2024-07-31 17:37:47 +02:00
Joakim Persson 538f02e6a7 Fixat så att sänd-knappen faktiskt gör det den ska göra 2024-07-31 17:37:26 +02:00
Joakim Persson 810b369721 Tagit bort utkommenterad kod 2024-07-31 17:36:37 +02:00
8 changed files with 478 additions and 253 deletions
+18 -15
View File
@@ -1,22 +1,25 @@
# Frontend Configuration
frontend:
url: "http://localhost:5004"
# Backend Configuration
backend:
url: "http://localhost:5004"
api: "/api/chat"
# Ollama Server Configuration
ollama:
url: "http://localhost:11434"
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: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"
preferred_ep: "Ollama-WARA"
endpoints:
- model: "AUTODETECT"
title: "Ollama-local" # Must be a unique identifier
url: "http://localhost:11434"
provider: "ollama"
# - model: "AUTODETECT"
- 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"
# Logging comment out the whole section for default level which is INFO
logging:
@@ -27,7 +30,7 @@ logging:
# Cache Settings (Optional)
cache:
enabled: true
enabled: True
timeout: 60 # Seconds
test:
+71 -21
View File
@@ -12,7 +12,7 @@ from utils import GlobalState
# Create a logger for this module
global_state = GlobalState() # Import the singleton that holds global states (e.g., logger)
logger = global_state.getLogger(__name__) # Logger for this module, inherit properties of the root logger
logger = global_state.get_logger(__name__) # Logger for this module, inherit properties of the root logger
# Find out the path to current directory according to the Python interpreter (venv)
@@ -22,6 +22,10 @@ logger.debug("Current working directory: %s", os.getcwd())
app = Flask(__name__)
app.config['STATIC_FOLDER'] = 'static' # Adjust if needed
# Increase the maximum cookie size
app.config['SESSION_COOKIE_SAMESITE'] = 'Lax'
app.config['SESSION_COOKIE_SIZE_LIMIT'] = 4096 * 2 # Allow up to 8KB cookies
# Set the secret key for session management
secret_key = os.urandom(24)
app.config['SECRET_KEY'] = secret_key # When do I need this. How is it retained between sessions?
@@ -41,13 +45,15 @@ def index():
session['chat_history'] = [] # The session object (actually, a dictonary) holds the chat session
logger.debug("Entering route '/'")
# api_endpoint = os.environ['BE_API_ENDPOINT'] # Retrieve the environment variable
api_endpoint = global_state.get_backend_api_ep() # Retrieve the environment variable
logger.debug("Backend API endpoint: %s", api_endpoint)
host_url = global_state.get_host_url()
use_model = global_state.get_llm()
logger.debug("Backend API endpoint:\t%s", api_endpoint)
logger.debug("Host of LLMs:\t\t%s", host_url)
logger.debug("LLM to use:\t\t\t%s", use_model)
with open('smartassist/src/html/client.html', 'r') as f:
client_html = f.read()
logger.debug("Client HTML (first few characters): %s", client_html[:50]) # Print to see if it's loading
# logger.debug("Client HTML (first few characters): %s", client_html[:50]) # Print to see if it's loading
# logger.debug("Client HTML (all characters): %s", client_html) # Print to see if it's loading
return render_template('index.html', api_endpoint=api_endpoint, use_model = use_model, client_content=client_html)
@@ -58,15 +64,15 @@ def set_session():
resp.set_cookie('session', 'some-value', samesite='None', secure=True) # Add SameSite attribute here
return resp
@app.route('/profile')
def profile():
# Retrieve data from the session
user_id = session.get('user_id')
# @app.route('/profile')
# def profile():
# # Retrieve data from the session
# user_id = session.get('user_id')
if user_id:
return f'User ID: {user_id}'
else:
return 'No user ID found'
# if user_id:
# return f'User ID: {user_id}'
# else:
# return 'No user ID found'
@app.route('/<path:filename>')
@@ -87,9 +93,22 @@ CORS(app, resources={
}
})
@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."""
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(url_server = "http://localhost:11434/api/generate", model = "phi3:mini"):
def chat():
# def chat(model = "phi3:mini"):
"""
This function handles the chat. The frontend client (web browser) calls the
backend server through this endpoint (/api/chat) that manage queries
@@ -99,8 +118,9 @@ def chat(url_server = "http://localhost:11434/api/generate", model = "phi3:mini"
# Get the message from the JSON in the request body
data = request.get_json()
message = data.get('query')
url_server = data.get('url_server', url_server) # Use provided URL or default
model = data.get('model', model) # Use provided model or default
url_server = data.get('url_server', global_state.get_host_url()) # Use provided URL or current if not provided
# url_server = data.get('url_server', "https://ollama-test.wara-ops.org/api/generate") # Use provided URL or default
model = data.get('model', global_state.get_llm()) # Use provided model or current if not provided
# Get chat history from session storage (e.g., a dictionary)
chat_history = session.get('chat_history', [])
@@ -117,13 +137,31 @@ def chat(url_server = "http://localhost:11434/api/generate", model = "phi3:mini"
'prompt': '\n'.join([f"{item['role']}: {item['message']}" for item in chat_history]),
"stream": False
}
url = url_server
# 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 ###################################
try:
url = url_server
headers = {
"Content-Type": "application/json",
}
url = url + "/api/generate"
logger.debug(f"url: {url} headers: {headers}")
response = requests.post(url,
headers=headers,
data=json.dumps(data_to_send))
@@ -140,6 +178,18 @@ def chat(url_server = "http://localhost:11434/api/generate", model = "phi3:mini"
return jsonify({'error': 'Invalid JSON response from server'}), 500
@app.route('/api/endpoints', methods=['GET'])
def get_endpoints():
# Replace this with your actual logic to fetch endpoint data
endpoints = [
{'endpoint': 'endpoint1', 'llm': 'llm1'},
{'endpoint': 'endpoint2', 'llm': 'llm2'},
{'endpoint': 'endpoint3', 'llm': 'llm3'},
{'endpoint': 'endpoint4', 'llm': 'llm4'},
]
return jsonify(endpoints)
@app.route('/smartassist', methods=["POST"])
def smartassist():
+18
View File
@@ -0,0 +1,18 @@
# Some useful constants to be used in the code
from enum import Enum
class LogLevel(Enum):
DEBUG = 'DEBUG'
INFO = 'INFO'
WARNING = 'WARNING'
ERROR = 'ERROR'
CRITICAL = 'CRITICAL'
LOG_LEVEL_MAPPING = {
LogLevel.DEBUG: 10,
LogLevel.INFO: 20,
LogLevel.WARNING: 30,
LogLevel.ERROR: 40,
LogLevel.CRITICAL: 50
}
+9 -25
View File
@@ -6,35 +6,22 @@
<link rel="stylesheet" href="/css/clientstyle.css">
<!-- <link rel="stylesheet" href="python_test/smartassist/src/css/clientstyle.css"> -->
</head>
<body>
<h1>Ollama Chat</h1>
<!-- Add the dropdown menu here -->
<div class="dropdown">
<button class="dropbtn">Select Endpoint/LLM</button>
<div class="dropdown-content" id="endpoint-dropdown"></div>
</div>
<div id="chatbox">
<!-- messages will be rendered here -->
</div>
<textarea id="userInput" placeholder="Type your message..." rows="5"></textarea>
<button id="sendButton" onclick="sendMessage()">Send</button>
<button id="sendButton" onclick="window.frontendApi.sendMessage()">Send</button>
<!-- Get the apiEndpoint and the useModel -->
<!-- <script>
const apiEndpoint = window.apiEndpoint;
const useModel = window.useModel;
console.log("client.html - API Endpoint: ", apiEndpoint);
console.log("client.html - use model: ", useModel);
</script> -->
<!-- <script>
let apiEndpoint; // Make variable available outside of the scope of the event listener
let useModel; // Make variable available outside of the scope of the event listener
window.addEventListener('message', function(event) {
if (event.origin === 'http://localhost:5004') { // Make sure this matches your origin
const { apiEndpoint, useModel } = event.data;
console.log("client.html - API Endpoint: ", apiEndpoint);
console.log("client.html - use model: ", useModel);
window.apiEndpoint = apiEndpoint;
window.useModel = useModel;
}
});
</script> -->
<!-- Marked-it for markdown rendering -->
<script src="https://cdn.jsdelivr.net/npm/markdown-it@14.1.0/dist/markdown-it.min.js"></script>
@@ -54,9 +41,6 @@
};
</script>
<script>
const chatContainer = document.getElementById('chatbox');
+53 -56
View File
@@ -7,11 +7,13 @@ import socket
import urllib.parse
from backend import run_flask
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.getLogger(__name__) # Logger for this module, inherit properties of the root logger
logger = global_state.get_logger(__name__) # Logger for this module, inherit properties of the root logger
def configure():
"""
@@ -30,16 +32,23 @@ def configure():
env_var_name = value[2:-1] # Extract name between ${}
return os.getenv(env_var_name, None)
return value
def update_value(value):
if isinstance(value, dict): # Dictionaries need recursive check
return update_dict_with_env_vars(value)
elif isinstance(value, list): # Lists must be traversed element by element
return [update_value(item) for item in value]
elif isinstance(value, str): # If value is a string it might be an environmnet variable
return resolve_env_var(value)
else: # Anything else, just keep the old value
return value
def update_dict_with_env_vars(d):
for key in d:
if isinstance(d[key], dict):
update_dict_with_env_vars(d[key]) # Recursively check nested dictionaries
elif isinstance(d[key], str):
d[key] = resolve_env_var(d[key])
def update_dict_with_env_vars(d): # Check all keys in d
for key in d: # Iterate over all keys in the dictionary. The keys seen are all at the top-level of d
# logger.info(f"key investigated now: {key}")
d[key] = update_value(d[key])
return d
# Update the config dictionary with resolved environment variables
updated_config = update_dict_with_env_vars(config)
####################################
@@ -48,57 +57,46 @@ 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 and export backend API
# endpoint as global state variable
# Extract models (server url, api_key, model, et cetera)
####################################
if isinstance(updated_config.get('backend'), dict): # Look for 'backend' key
if isinstance(updated_config['backend'].get('url'), str): # Look for 'url' key
url = updated_config['backend'].get('url')
if isinstance(updated_config['backend'].get('api'), str): # Look for 'api' key
api = updated_config['backend'].get('api')
# backend_api_ep = url+api # Extract API endpoint if defined
logger.debug(f"Constructing endpoint address as url+api: {url+api}")
global_state.set_backend_api_ep(url+api) # Extract API endpoint if defined and set in global_state
logger.debug(f"Backend API endpoint is set to {global_state.get_backend_api_ep()}")
# os.environ['BE_API_ENDPOINT'] = backend_api_ep # Look into alternative way to share this with backend.py
if isinstance(updated_config.get('backend'),dict): # Extract backend info from dictionary
global_state.set_backend(backend=updated_config.get('backend'))
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
####################################
# Extract Ollama parameters (url, api_key, model)
####################################
if isinstance(updated_config.get('ollama'), dict): # Look for 'ollama' key
if isinstance(updated_config['ollama'].get('model'), str): # Look for 'model' key
model_to_use = updated_config['ollama'].get('model')
global_state.set_llm(model_to_use)
logger.debug("configure(): LLM is set to: %s",global_state.get_llm())
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)))
global_state.fetch_models()
endpoints = global_state.get_endpoints()
for endpoint in endpoints: # Set default LLM for each endpoint
available_llms = global_state.get_list_of_available_llms(endpoint=endpoint)
llm = next(iter(available_llms),None) # First available LLM or None. Default for AUTODETECT and requests for non-existing LLMs
logger.debug(f"url {endpoint['url']} = {available_llms}")
if endpoint["model"] in available_llms: # Check if specific LLM requested, AUTODETECT evaluates to False
llm = endpoint["model"]
endpoint["default_llm"] = llm
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)
global_state.set_llm(default_llm) # Set which llm to use
logger.debug(f"Desired default endpoint: {default_ulr},\tDesired default LLM: {default_llm}")
logger.debug(f"Returned default endpoint: {global_state.get_host_url()},\tReturned default LLM: {global_state.get_llm()}")
return updated_config
# def start_frontend(config):
# parsed_url = urllib.parse.urlparse(config['frontend']['url'])
# hostname = parsed_url.netloc.split(':')[0] # Split by ':' and take the first part, i.e., 'localhost', IP, or domain name
# port = parsed_url.port # This is the server port
# # Use the socket module in Python to check whether a port is in use,
# # which would indicate that a server is already running on that port.
# with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
# try:
# s.bind((hostname, port))
# logger.debug("No server is running on %s -— starting one.", parsed_url.netloc)
# # Start frontend (web server) as a separate process
# subprocess.Popen(["python", "-m", "http.server", str(port)])
# except socket.error as e:
# if e.errno == 48:
# logger.debug("A server is already running on %s -— will use this.", parsed_url.netloc)
# else:
# raise # Unexpected error, re-raise it so we can see the traceback
# except Exception as e:
# logger.error("Failed to start frontend: %s", str(e)) # Corresponds to print(f"Failed to start frontend: {e}")
return updated_config
def start_backend(config):
@@ -115,7 +113,6 @@ def start_backend(config):
if __name__ == '__main__':
conf = configure() # Read config from file and set up config dict
logger.debug('conf dictionary set to {}'.format(json.dumps(conf, indent=4)))
# logger.debug('conf dictionary set to \n{}'.format(json.dumps(conf, indent=4)))
# start_frontend(config=conf) # Not needed as we are using Flask for backend now
start_backend(config=conf)
+35 -3
View File
@@ -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 {
@@ -61,7 +62,7 @@ h1 {
border-color: #66afe9; /* Blue outline on focus */
}
button[onclick="sendMessage()"] {
button[onclick="window.frontendApi.sendMessage()"] {
background-color: #4CAF50; /* Green */
border: none;
color: white;
@@ -75,11 +76,42 @@ button[onclick="sendMessage()"] {
transition: background-color 0.3s; /* Smooth transition effect */
}
button[onclick="sendMessage()"]:hover {
button[onclick="window.frontendApi.sendMessage()"]:hover {
background-color: #b2b2b2; /* Light Grey on hover */
}
button[onclick="sendMessage()"]:active {
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;
}
.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 */
}
.dropdown-content a:hover {background-color: #f1f1f1;}
.dropdown:hover .dropdown-content {
display: block;
}
+38 -108
View File
@@ -1,111 +1,3 @@
// // Get the user input element from the DOM
// const chatbox = document.getElementById('chatbox');
// const userInput = document.getElementById('userInput');
// const parser = window.markdownit({
// linkify: true,
// strikethrough: true,
// });
// parser.enable(['table']);
// // const apiEndpoint = window.apiEndpoint; // Get the API endpoint
// // const useModel = window.useModel; // Get whether to use a model or not
// // console.log("frontend.js - API Endpoint: ", window.apiEndpoint);
// // console.log("frontend.js - Use model: ", window.useModel);
// let apiEndpoint; // Make variable available outside of the scope of the event listener
// let useModel; // Make variable available outside of the scope of the event listener
// window.addEventListener('message', function(event) {
// if (event.origin === 'http://localhost:5004') { // Make sure this matches your origin
// const { apiEndpoint, useModel } = event.data;
// console.log("client.html - API Endpoint: ", apiEndpoint);
// console.log("client.html - use model: ", useModel);
// window.apiEndpoint = apiEndpoint;
// window.useModel = useModel;
// }
// });
// console.log("frontend.js - API Endpoint: ", window.apiEndpoint);
// console.log("frontend.js - Use model: ", window.useModel);
// // Define a function to send the user's message to the AI
// function sendMessage() {
// // Get the user's input message and trim any whitespace
// const query = userInput.value.trim();
// // Check if the message is not empty
// if (query !== '') {
// // fetch(`${apiEndpoint}`, {
// fetch(apiEndpoint, {
// method: 'POST',
// headers: { 'Content-Type': 'application/json' },
// body: JSON.stringify({ query, model: useModel }), // Add these parameters here
// // body: JSON.stringify({ query, url_server: "http://your-custom-url", model: "phi3:mini" }), // Add these parameters here
// })
// .then(response => response.json())
// .then(data => {
// // Get the AI's response from the API data
// const aiResponse = data.response;
// // Render the user's original message in the chatbox
// renderMessage(query, 'user-message');
// // Render the AI's response in the chatbox
// renderMessage(aiResponse, 'ai-response');
// // Clear the user input field for the next message
// userInput.value = '';
// })
// .catch(error => console.error('Error sending message:', error));
// }
// }
// // Define a function to render a message in the chatbox with a specific class name
// function renderMessage(text, className) {
// // Create a new div element to hold the message
// const messageElement = document.createElement('div');
// // Add the specified class name to the element
// messageElement.className = className;
// // // Set the text content of the element to the message text
// // messageElement.textContent = text;
// // Use the markdown-it parser
// const html = parser.render(text);
// messageElement.innerHTML = html;
// // Append the message element to the chatbox immediately
// // chatbox.appendChild(messageElement);
// // Typeset math in the message element
// MathJax.typesetPromise([messageElement]).then(() => {
// // No need to append anything here, it's already appended above
// chatbox.appendChild(messageElement);
// });
// }
// // Make the button toggle colour when user presses Enter on keyboard
// const sendButton = document.getElementById('sendButton');
// document.addEventListener('keydown', function(event) {
// if (event.key === 'Enter') {
// sendButton.style.backgroundColor = '#6f6f6f'; // Dark Grey when Enter is pressed
// }
// });
// document.addEventListener('keyup', function() {
// sendButton.style.backgroundColor = ''; // Restore the original style when any key is released
// });
// Get the user input element from the DOM
const chatbox = document.getElementById('chatbox');
const userInput = document.getElementById('userInput');
@@ -161,6 +53,34 @@ 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 = '#';
linkElement.onclick = () => frontendApi.setEndpointAndLlm(endpoint.endpoint, endpoint.llm);
linkElement.textContent = `${endpoint.endpoint} - ${endpoint.llm}`;
dropdownContainer.appendChild(linkElement);
});
})
.catch(error => console.error('Error fetching endpoints:', error));
},
// Set the endpoint and LLM variables
setEndpointAndLlm: function(endpoint, llm) {
window.endpoint = endpoint;
window.llm = llm;
console.log(`Selected Endpoint: ${endpoint}, LLM: ${llm}`);
},
};
// Wait for the event listener to set apiEndpoint and useModel
@@ -190,3 +110,13 @@ document.addEventListener('keydown', function(event) {
document.addEventListener('keyup', function() {
sendButton.style.backgroundColor = ''; // Restore the original style when any key is released
});
function init() {
// Other initialization code here...
frontendApi.fillMenu();
}
document.addEventListener('DOMContentLoaded', init);
+236 -25
View File
@@ -2,6 +2,10 @@
# imported to more than one other module. The rational for defining these things here
# is that it is easier to avoid circular imports when they are defined in a central location.
import logging
import json
import requests
from typing import Optional
from enums import LogLevel, LOG_LEVEL_MAPPING
class GlobalState:
"""
@@ -13,7 +17,12 @@ class GlobalState:
"""
_instance = None # Private class attribute to hold the single instance of the class
def __new__(cls):
def __new__(cls) -> 'GlobalState':
"""
Create a new instance of the GlobalState class.
This is a singleton implementation, so only one instance will be created.
"""
if cls._instance is None:
cls._instance = super(GlobalState, cls).__new__(cls)
cls._instance.log_level = 'INFO' # Default logging level
@@ -24,52 +33,254 @@ class GlobalState:
cls._instance.logger.addHandler(handler)
cls._instance.logger.setLevel(getattr(logging, cls._instance.log_level)) # Initialize root logger level
cls._instance.logger.info(" __new__(cls): Logger in GlobalState created: %s", cls._instance.logger)
cls._instance.host_url = None # Currently used LLM host
cls._instance.llm = "phi3:mini" # Default LLM for queries. TODO: Check with ollama server that it actually exists
cls._instance.backend_api_ep = "http://localhost:5005/api/chat" # Default backend API endpoint
# cls._instance.backend_api_ep = "http://localhost:5005/api/chat" # Default backend API endpoint
# Try making things more aligned with the outline of the yaml file
cls._instance.backend = dict() # A dictionary that holds info on which server the clients connect to
cls._instance.endpoints = [] # A list that holds info on which endpoints are available for use (server url, model name, provider et cetera)
# logging - already done in __new__, perhaps change layout later
return cls._instance
def configure_logging(self, level=None):
"""Set up logging for the project."""
if level is None:
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.
"""
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 = 'INFO'):
"""Set the logging level."""
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.
"""
self.log_level = level
self.configure_logging()
def get_log_level(self):
"""Getter for log_level attribute."""
def get_log_level(self) -> LogLevel:
"""
Get the current log level.
Returns:
str: The current log level (e.g., 'DEBUG', 'INFO', 'WARNING', etc.).
"""
return self.log_level
def get_effective_log_level(self):
"""Getter for effective log level of loggerattribute."""
def get_effective_log_level(self) -> int:
"""
Get the effective log level of the logger.
Returns:
int: The numeric value of the effective log level.
"""
return self.logger.getEffectiveLevel()
def getLogger(self, module_name = None):
"""Return a logger based on the module name."""
def get_logger(self, module_name: Optional[str] = None) -> logging.Logger:
"""
Get a logger instance based on the module name.
Args:
module_name (str): The name of the module to get a logger for. If None, uses the current module name (`__name__`).
Returns:
Logger: A logger instance configured for the specified module.
"""
if module_name is None:
module_name = __name__
logger = logging.getLogger(module_name)
return logger
def set_llm(self, model_name="phi3:mini"):
"""Set LLM for queries"""
def set_host_url(self, url: str = "http://localhost:11434") -> None:
"""
Set the URL of the host to which LLM requests are sent.
Args:
url (str): The new URL to use. Defaults to 'http://localhost:11434' if not specified.
"""
self.host_url = url
def get_host_url(self) -> str:
"""
Get the current URL of the host used for LLMs.
Returns:
str: The current URL of the host.
"""
return self.host_url
def set_llm(self, model_name: str = "phi3:mini") -> None:
"""
Set the LLM to use for queries.
Args:
model_name (str): The name of the LLM to use. Defaults to 'phi3:mini' if not specified.
"""
self.llm = model_name
def get_llm(self):
"""Getter for which LLM is used for queries"""
def get_llm(self) -> str:
"""
Get the current LLM used for queries.
Returns:
str: The name of the current LLM.
"""
return self.llm
def set_backend_api_ep(self, be_api_ep=None):
"""Set backend API endpoint"""
self.backend_api_ep = be_api_ep
def set_backend(self, backend: Optional[dict] = None) -> None:
def get_backend_api_ep(self):
"""Getter for backend API endpoint"""
return self.backend_api_ep
"""
Set the backend server that web clients connect to.
Args:
backend (dict): A dictionary containing information about the backend server. If None, resets the backend server to its default value.
"""
self.backend = backend
def get_backend(self) -> dict:
"""
Get the current backend server used by web clients.
Returns:
dict: A dictionary containing information about the current backend server.
"""
return self.backend
def get_backend_api_ep(self) -> str:
"""
Get the API endpoint of the backend server.
Returns:
str: The URL of the API endpoint.
"""
return self.backend["url"]+self.backend["api"]
def set_endpoints(self, endpoints: Optional[list[dict]] = None) -> None:
"""
Set the list of endpoints used by this object.
Args:
endpoints (list): A list of endpoint dictionaries. Each dictionary should contain information about an endpoint.
If None, resets the endpoints to their default value.
Raises:
ValueError: If endpoints is not a list.
Notes:
Endpoints can be reset to their default value by passing None as the argument.
"""
if endpoints is not None:
if not isinstance(endpoints, list):
raise ValueError("Endpoints must be a list, even if there is just one model")
self.endpoints = endpoints
def get_endpoints(self) -> list[dict]:
"""
Get the complete list of endpoints.
Returns:
List of endpoints
"""
return self.endpoints
def get_endpoints_with_key(self, key: str) -> list[dict]:
"""
Returns a list of endpoint dictionaries that contain the specified key.
Args:
key (str): The key 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]
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:
"""
Fetch models from endpoints and update the endpoint dictionaries.
Returns:
None
"""
logger = logging.getLogger(__name__)
for endpoint in self.endpoints:
try:
if endpoint["provider"] == "ollama":
headers = {
"Content-Type": "application/json",
}
if "requestOptions" in endpoint: # Check if authentication is needed
headers.update({
"Authorization": endpoint["requestOptions"]["headers"]["Authorization"]
})
models_response = requests.get(endpoint["url"] + "/api/tags", headers=headers)
models_response.raise_for_status() # Raise an exception for HTTP errors
try:
models = models_response.json()
except json.JSONDecodeError as e:
logger.error(f"Failed to parse JSON response: {e}")
continue
if isinstance(models, dict) and 'error' in models: # Unclear if requests to any API actually add this in the response
logger.error('Error fetching models from backend: %s', models['error'])
else:
endpoint["models"] = models.get("models", []) # Get the list of models directly
except requests.exceptions.RequestException as e:
logger.error(f"Request error: {e}")
except Exception as e:
logger.error(f"Unexpected error: {e}")
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.
Args:
endpoint (dict): Optional endpoint dictionary to retrieve LLMs from. If not provided, will use internal endpoint configuration.
Returns:
list: A sorted list of LLM names (strings). Returns None if no LLMs are found or endpoint is invalid.
"""
llm_list = None
if isinstance(endpoint["models"], list):
llm_list = sorted([list_item['name'] for list_item in endpoint["models"]], key=str.lower)
return llm_list