31 Commits

Author SHA1 Message Date
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
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
8 changed files with 511 additions and 139 deletions
+5 -18
View File
@@ -3,36 +3,23 @@ backend:
url: "http://localhost:5004" url: "http://localhost:5004"
api: "/api/chat" api: "/api/chat"
preferred_ep: "Ollama-WARA"
endpoints: endpoints:
- model: "AUTODETECT" - model: "AUTODETECT"
title: "Ollama" title: "Ollama-local" # Must be a unique identifier
url: "http://localhost:11434" url: "http://localhost:11434"
provider: "ollama" provider: "ollama"
# - model: "AUTODETECT" # - model: "AUTODETECT"
- model: "AUTODETECT" - model: "llava:13b"
title: "Ollama-WARA" title: "Ollama-WARA" # Must be a unique identifier
url: "https://ollama-test.wara-ops.org" url: "https://ollama-test.wara-ops.org"
requestOptions: requestOptions:
headers: headers:
Authorization: "${OLLAMA_API_KEY}" # on MacOS: echo "Authorization: Basic $(echo -n 'user:password' | gbase64 -w 0)" Authorization: "${OLLAMA_API_KEY}" # on MacOS: echo "Authorization: Basic $(echo -n 'user:password' | gbase64 -w 0)"
provider: "ollama" provider: "ollama"
# Ollama Server Configuration
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 comment out the whole section for default level which is INFO
logging: logging:
+147 -32
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,10 +9,11 @@ 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)
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) # Find out the path to current directory according to the Python interpreter (venv)
@@ -38,20 +39,28 @@ 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 = os.environ['BE_API_ENDPOINT'] # Retrieve the environment variable
api_endpoint = global_state.get_backend_api_ep() # 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() 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: with open('smartassist/src/html/client.html', 'r') as f:
client_html = f.read() 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 # 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) return render_template('index.html', api_endpoint=api_endpoint, use_model = use_model, client_content=client_html)
@@ -73,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": "*",
@@ -85,40 +105,59 @@ 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:
# def tag(url = "http://localhost:11434/api/tags", headers = {"Content-Type": "application/json"}): """
"""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(model = "phi3:mini"): def chat() -> dict[str, any]:
# def chat(url_server = "http://localhost:11434/api/generate", 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()
message = data.get('query') message = data.get('query')
url_server = data.get('url_server', "https://ollama-test.wara-ops.org/api/generate") # Use provided URL or default url_server = data.get('url_server', global_state.get_host_url()) # Use provided URL or current if not provided
model = data.get('model', model) # Use provided model or default 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) # Get chat history from session storage (e.g., a dictionary)
chat_history = session.get('chat_history', []) chat_history = session.get('chat_history', [])
@@ -135,13 +174,12 @@ def chat(model = "phi3:mini"):
'prompt': '\n'.join([f"{item['role']}: {item['message']}" for item in chat_history]), 'prompt': '\n'.join([f"{item['role']}: {item['message']}" for item in chat_history]),
"stream": False "stream": False
} }
try:
url = url_server url = url_server
headers = { headers = get_auth_headers(url)
"Content-Type": "application/json",
"Authorization": "Basic ZWNzanBlcjoxM2JjMTU4ZDhmNmY5YTU4YTkzZDNmY2I=" logger.debug(f"Sending request to:\n\turl:\t{url}\nmodel:\n\t{model}")
} try:
url = url + "/api/generate"
logger.debug(f"url: {url} headers: {headers}") logger.debug(f"url: {url} headers: {headers}")
response = requests.post(url, response = requests.post(url,
headers=headers, headers=headers,
@@ -159,6 +197,54 @@ def chat(model = "phi3:mini"):
return jsonify({'error': 'Invalid JSON response from server'}), 500 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)}")
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': 'Endpoint and LLM selected successfully'})
@app.route('/smartassist', methods=["POST"]) @app.route('/smartassist', methods=["POST"])
def smartassist(): def smartassist():
@@ -178,6 +264,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):
""" """
+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
}
+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"> -->
@@ -9,6 +10,12 @@
<body> <body>
<h1>Ollama Chat</h1> <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"> <div id="chatbox">
<!-- messages will be rendered here --> <!-- messages will be rendered here -->
</div> </div>
+23 -11
View File
@@ -5,14 +5,15 @@ import yaml
import json import json
import socket import socket
import urllib.parse import urllib.parse
from backend import run_flask, tag from backend import run_flask
import logging import logging
import requests import requests
import utils import utils
from utils import GlobalState, fetch_models_from_endpoints from utils import GlobalState
from enums import LogLevel
global_state = GlobalState() # Configure root logger. The level will be adjusted later based on config file 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(): def configure():
""" """
@@ -44,7 +45,7 @@ def configure():
def update_dict_with_env_vars(d): # Check all keys in d 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 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}") # logger.info(f"key investigated now: {key}")
d[key] = update_value(d[key]) d[key] = update_value(d[key])
return d return d
@@ -56,8 +57,9 @@ def configure():
if isinstance(updated_config.get('logging'), dict): # Look for 'logging' key in config file if isinstance(updated_config.get('logging'), dict): # Look for 'logging' key in config file
logging_config = updated_config['logging'] logging_config = updated_config['logging']
if isinstance(logging_config.get('level'), str): # Set to value of the yaml file if specified if isinstance(logging_config.get('level'), str): # Set to value of the yaml file if specified
global_state.set_log_level(logging_config['level']) # 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(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) # Extract models (server url, api_key, model, et cetera)
@@ -67,22 +69,32 @@ def configure():
logger.debug("backend = \n{}".format(json.dumps(global_state.get_backend(), indent=4))) 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()}") 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 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 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))) # logger.debug("endpoints = \n{}".format(json.dumps(global_state.get_endpoints(), indent=4)))
fetch_models_from_endpoints(global_state.get_endpoints(), global_state) # Call the new function global_state.fetch_models()
endpoints = global_state.get_endpoints() endpoints = global_state.get_endpoints()
for endpoint in endpoints: # Set default LLM for each endpoint for endpoint in endpoints: # Set default LLM for each endpoint
available_llms = global_state.get_list_of_available_llms(endpoint=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 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}") logger.debug(f"url {endpoint['url']} = {available_llms}")
if endpoint["model"] in available_llms: # Check if specific LLM requested if endpoint["model"] in available_llms: # Check if specific LLM requested, AUTODETECT evaluates to False
llm = endpoint["model"] llm = endpoint["model"]
endpoint["default_llm"] = llm endpoint["default_llm"] = llm
if preferred_ep: # If preferred_ep is specified, set it as the default endpoint
global_state.set_host_url(next(iter(endpoints),None)) # Set initial host to the first item in endpoints (or None) list_of_eps = global_state.get_endpoints_with_key_value("title", preferred_ep) # Should only be one element in the list...
global_state.set_llm(llm) # Set which server and llm to use 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 return updated_config
@@ -26,6 +26,7 @@ h1 {
resize: both; /* Allow resizing vertically */ resize: both; /* Allow resizing vertically */
border: 1px solid #ccc; /* Add a thin grey border around chatbox */ border: 1px solid #ccc; /* Add a thin grey border around chatbox */
margin-bottom: 20px; /* Add some space between chatbox and userInput */ margin-bottom: 20px; /* Add some space between chatbox and userInput */
font-size: 14px; /* Decrease font size to 14 pixels */
} }
.message { .message {
@@ -83,3 +84,35 @@ button[onclick="window.frontendApi.sendMessage()"]:active {
background-color: #6f6f6f; /* Dark Grey when clicked */ 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;
}
+67 -1
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,7 +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);
}, },
}; // 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 // Wait for the event listener to set apiEndpoint and useModel
window.addEventListener('message', function(event) { window.addEventListener('message', function(event) {
@@ -82,3 +125,26 @@ document.addEventListener('keydown', function(event) {
document.addEventListener('keyup', function() { 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);
+208 -74
View File
@@ -4,45 +4,8 @@
import logging import logging
import json import json
import requests import requests
#from backend import GlobalState # Assuming GlobalState is defined there from typing import Optional
from enums import LogLevel, LOG_LEVEL_MAPPING
def fetch_models_from_endpoints(endpoints, global_state):
"""
Fetch models from endpoints and update the endpoint dictionaries.
Args:
endpoints (list): List of endpoint dictionaries.
global_state: The global state object.
Returns:
None
"""
logger = logging.getLogger(__name__)
for endpoint in endpoints:
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"]
})
try:
models_response = requests.get(endpoint["url"] + "/api/tags", headers=headers)
models_response.raise_for_status() # Raise an exception for HTTP errors
models = models_response.json()
except requests.exceptions.RequestException as e:
logger.error("Error fetching models from backend: %s", str(e))
continue
if isinstance(models, dict) and 'error' in models:
logger.error('Error fetching models from backend: %s', models['error'])
else:
endpoint["models"] = models.get("models", []) # Get the list of models directly
# logger.debug("models = \n{}".format(json.dumps(models, indent=4)))
class GlobalState: class GlobalState:
""" """
@@ -54,7 +17,12 @@ class GlobalState:
""" """
_instance = None # Private class attribute to hold the single instance of the class _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: if cls._instance is None:
cls._instance = super(GlobalState, cls).__new__(cls) cls._instance = super(GlobalState, cls).__new__(cls)
cls._instance.log_level = 'INFO' # Default logging level cls._instance.log_level = 'INFO' # Default logging level
@@ -75,77 +43,243 @@ class GlobalState:
return cls._instance return cls._instance
def configure_logging(self, level=None):
"""Set up logging for the project.""" def configure_logging(self, level: Optional[LogLevel] = None) -> None:
if level is 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 level = self.log_level
# numeric_level = getattr(logging, level.upper()) # Convert string to numeric level if isinstance(level, LogLevel):
numeric_level = getattr(logging, level.upper()) # Convert string to numeric level 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.setLevel(numeric_level)
self.logger.debug(f"utils.py -- configure_logging(): effective log level is {level} which is {self.logger.getEffectiveLevel()}") 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.log_level = level
self.configure_logging() self.configure_logging()
def get_log_level(self): def get_log_level(self) -> LogLevel:
"""Getter for log_level attribute.""" """
Get the current log level.
Returns:
str: The current log level (e.g., 'DEBUG', 'INFO', 'WARNING', etc.).
"""
return self.log_level return self.log_level
def get_effective_log_level(self): def get_effective_log_level(self) -> int:
"""Getter for effective log level of loggerattribute.""" """
Get the effective log level of the logger.
Returns:
int: The numeric value of the effective log level.
"""
return self.logger.getEffectiveLevel() return self.logger.getEffectiveLevel()
def getLogger(self, module_name = None): def get_logger(self, module_name: Optional[str] = None) -> logging.Logger:
"""Return a logger based on the module name."""
"""
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: if module_name is None:
module_name = __name__ module_name = __name__
logger = logging.getLogger(module_name) logger = logging.getLogger(module_name)
return logger return logger
def set_host_url(self, url="http://localhost:11434"): def set_host_url(self, url: str = "http://localhost:11434") -> None:
"""Set the host url to which LLM requests are sent""" """
self.host = url Set the URL of the host to which LLM requests are sent.
def get_host_url(self): Args:
"""Get the url for the currently used host for LLMs""" 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 return self.host_url
def set_llm(self, model_name="phi3:mini"): def set_llm(self, model_name: str = "phi3:mini") -> None:
"""Set LLM for queries""" """
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 self.llm = model_name
def get_llm(self): def get_llm(self) -> str:
"""Getter for which LLM is used for queries""" """
Get the current LLM used for queries.
Returns:
str: The name of the current LLM.
"""
return self.llm return self.llm
def set_backend(self, backend=None): def set_backend(self, backend: Optional[dict] = None) -> None:
"""Set backend server that web clients connect to"""
"""
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 self.backend = backend
def get_backend(self): def get_backend(self) -> dict:
"""Getter for backend server that web clients connect to""" """
Get the current backend server used by web clients.
Returns:
dict: A dictionary containing information about the current backend server.
"""
return self.backend return self.backend
def get_backend_api_ep(self): def get_backend_api_ep(self) -> str:
"""Getter for backend API endpoint""" """
Get the API endpoint of the backend server.
Returns:
str: The URL of the API endpoint.
"""
return self.backend["url"]+self.backend["api"] return self.backend["url"]+self.backend["api"]
def set_endpoints(self, endpoints=None): def set_endpoints(self, endpoints: Optional[list[dict]] = None) -> None:
"""Set the list of endpoints.""" """
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 endpoints is not None:
if not isinstance(endpoints, list): if not isinstance(endpoints, list):
raise ValueError("Endpoints must be a list, even if there is just one model") raise ValueError("Endpoints must be a list, even if there is just one model")
self.endpoints = endpoints self.endpoints = endpoints
def get_endpoints(self): def get_endpoints(self) -> list[dict]:
"""Return the list of endpoints""" """
Get the complete list of endpoints.
Returns:
List of endpoints
"""
return self.endpoints return self.endpoints
def get_list_of_available_llms(self, endpoint=None): def get_endpoints_with_key(self, key: str) -> list[dict]:
"""Return a sorted list of LLMs available at endpoint""" """
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 llm_list = None
if isinstance(endpoint["models"], list): if isinstance(endpoint["models"], list):
llm_list = sorted([list_item['name'] for list_item in endpoint["models"]], key=str.lower) llm_list = sorted([list_item['name'] for list_item in endpoint["models"]], key=str.lower)
return llm_list return llm_list