113 Commits

Author SHA1 Message Date
joakimp 97ee179b29 Snyggade till en loggutskrivt 2024-08-06 23:37:27 +02:00
joakimp 942f9f78c9 Lade till metod för att erhålla host_title för nuvarande endpoint 2024-08-06 23:36:49 +02:00
joakimp 3f39e11b10 Lagt ytterligare loggmeddelande för att underlätta felsökning. 2024-08-06 22:08:16 +02:00
joakimp 32098e3452 Experimenterar med olika parametrar i anrop till sendMessage() för att säkerställa att byte till ny modell verkligen görs. 2024-08-06 22:07:18 +02:00
Joakim Persson 5b143e75e0 Lagt till setEndointAndLlm() till frontendApi för att skicka gjorda val av endpoint title och llm till backend.py 2024-08-06 17:34:56 +02:00
Joakim Persson 168b8b13c1 Tog bort oanvnd och bortkommenterad kod 2024-08-06 17:33:14 +02:00
Joakim Persson f7f6ce2e49 Docstrings och type hinting. Lagt till route för (api/select_endpoint_llm. Brutit ut header-generering till get_auth_headers() 2024-08-06 17:32:46 +02:00
joakimp fa98c7b162 Nu sätts titeln på rullgardinsmenyn till det element som valts i denna. Lade till viewport för att underlätta för olika webklienter. 2024-08-06 00:50:47 +02:00
joakimp fd5f6199e9 Hanterar dynamisk uppdatering av innehållet i rullgardinsmenyn 2024-08-06 00:48:54 +02:00
joakimp c26dbc5612 /api/endpoint retunerar lista över endpoints och deras respektive llm:er 2024-08-06 00:47:28 +02:00
joakimp 7f557fadd6 Automatisk anpassning av dropdown-meny till textbredden på innehållet 2024-08-06 00:46:19 +02:00
joakimp 5ce92a5602 Tog bort en tomrad bara... 2024-08-06 00:22:16 +02:00
Joakim Persson 6dc93b66be Anropar Flask för att få en lista med tillgängliga endpoints och LLM:er 2024-08-05 17:27:49 +02:00
Joakim Persson 606becc5c3 Säkerställt så att innehållet i dropdown-menyn kan ändras dynamiskt 2024-08-05 17:27:00 +02:00
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
Joakim Persson 9203641e6a Inläsning av apiEndpoint och useModel har flyttats hit. Funktioner och variabler från frontend.js exporteras numer via frontendApi 2024-07-31 15:46:18 +02:00
Joakim Persson 14eaf57335 Ökat upp chatfönstrets höjd vid start 2024-07-31 15:44:18 +02:00
Joakim Persson 9ae5840e8b Flyttat inläsning av apiEndpoint och useModel till frontend.js 2024-07-31 15:43:43 +02:00
Joakim Persson 659b0937d3 Changed to phi3:mini 2024-07-31 15:42:32 +02:00
joakimp 7bc8a129c6 Använder nu postMessage för att få över variabler till iframe 2024-07-30 23:47:16 +02:00
joakimp a12c11c058 Arbetar med att göra externt skapade variabler tillgängliga. Safari krävre annnan hantering än övriga webläsare 2024-07-30 23:46:37 +02:00
joakimp 5771e52c49 Händelsestyrd läsning av apiEndpoint och useModel 2024-07-30 23:45:07 +02:00
joakimp d038f1ac24 Lag till hantering av backend API endpoint 2024-07-30 23:42:56 +02:00
joakimp 86374c91fe Delar variabler enhetligt nu genom global_state 2024-07-30 23:41:42 +02:00
joakimp 0adbb9c222 Lagt till svaren till chat-historiken. Hanterar och delar variabler enhetligt med global_state, inte miljövariabler. 2024-07-30 23:33:00 +02:00
joakimp c3d7f3ba4f Test med annan LLM 2024-07-30 23:30:21 +02:00
Joakim Persson d864d7529a Konfigurerar parser. Städat upp från bortkommenterad kod. 2024-07-30 16:42:59 +02:00
Joakim Persson 0b971dffc4 Ändrat på färger i chat-fönstret 2024-07-30 16:42:09 +02:00
Joakim Persson ecf45bd2e7 Importerar plugin till markdown-it för tabeller och annat. 2024-07-30 16:41:37 +02:00
Joakim Persson b88e573761 Lagt till chat-historik 2024-07-30 16:40:47 +02:00
Joakim Persson ecf2ddf46e Formatting output using markdown-it and MathJax 2024-07-30 14:32:00 +02:00
Joakim Persson b4a35bafef Replace Marked with markdown-it. Added some configuration of MathJax 2024-07-30 14:30:13 +02:00
joakimp de3e59afea Rendrering av markdown-formatterade svar. Experimenterar med MathJax. Ändrar färg på knappen när man trycker på Retur-tangenten 2024-07-30 00:16:55 +02:00
joakimp 9f5bdddb8f Förbättrat det visuella genom att ändra färg på knapp och ta bort storleksbegränsning på chatt-rutan 2024-07-30 00:14:54 +02:00
joakimp 854ebbdd94 Läser in Marked och MathJax för rendrering 2024-07-30 00:13:34 +02:00
joakimp 7d954e7380 Ändrade till phi3:mini som standardmodell för dess lilla storlek och snabbhet 2024-07-30 00:12:05 +02:00
Joakim Persson d843a2b74c Lagt till logmeddelande 2024-07-29 13:15:54 +02:00
Joakim Persson ab94c06e75 Skickar med use_model när index.html rendreras 2024-07-29 13:15:02 +02:00
joakimp 1757542077 Läser nu ut Ollama-modell från yaml-fil 2024-07-29 00:48:21 +02:00
joakimp 941b426574 Lagt till vidarebefordran av LLM från backen.py till frontend (index.html, client.html, frontend.js 2024-07-29 00:47:42 +02:00
joakimp 534874e810 Lagt till tillstånd och metoder för vilken llm som ska användas 2024-07-29 00:45:10 +02:00
Joakim Persson 0cccd02050 Förberett för att skicka med modell i anrop till apiEndpoint 2024-07-28 23:26:33 +02:00
Joakim Persson e69a131875 Fixat så att loggningen fungerar med GlobalState 2024-07-28 23:25:42 +02:00
Joakim Persson d797440288 Hämtar url_server och model när frågor skickas till api/chat 2024-07-28 23:24:39 +02:00
Joakim Persson 1b5624018c Lagt till fler metoder. Sett till det är att root-loggaren som skapas vid instasieringen av denna singleton 2024-07-28 23:22:08 +02:00
Joakim Persson 807a766a63 Bytt till klassbaserad lösning för globala varabler 2024-07-27 23:49:23 +02:00
Joakim Persson 5245e53954 Tagit bort kod som inte behövs längre 2024-07-27 22:46:55 +02:00
Joakim Persson d1b0554748 Skickar apiEndpoint till iframe genom att sätta contentWindow.apiEndpoint 2024-07-27 22:46:13 +02:00
Joakim Persson f8c3bcd156 Hämntar ut apiEndpoint från fönstret som koden exekverar i (en iframe) 2024-07-27 22:44:32 +02:00
Joakim Persson 25e5c6acea Testar vidare... 2024-07-25 23:23:57 +02:00
Joakim Persson 20fa94f533 Fortsatta försök att få med api_endpoint genom Flask till frontend.js där den behövs 2024-07-25 22:55:21 +02:00
joakimp ccc8e73f48 Anpassat till den nya designen där Flask skapar websidor m.h.a. mallar. Webfiler (html, css, js) har flyttats till egna kataloger. 2024-07-25 00:41:50 +02:00
joakimp f179e8e19b Removed start of separate frontend server 2024-07-25 00:37:46 +02:00
joakimp bfe8479016 Frontend and backed now using the same Flask web server 2024-07-25 00:36:35 +02:00
joakimp b261437952 Borttagna då filerna flyttats till separata kataloger 2024-07-24 20:10:08 +02:00
joakimp dac233ba9c Flyttat .html, .css och .js till separata kataloger 2024-07-24 20:09:27 +02:00
Joakim Persson 7c9d20896e Testar render_template() för att få över information till klienter 2024-07-24 17:26:57 +02:00
Joakim Persson ce5532e9a6 Fixat loggning 2024-07-24 17:25:14 +02:00
Joakim Persson feb7e61fd8 Parametrisering av API endpoint 2024-07-24 17:24:52 +02:00
Joakim Persson 9a9b1388ce Fixat loggningen. Experimenterar med hur information kan föras över till frontend.js 2024-07-24 17:24:21 +02:00
Joakim Persson 61974859bd Fixat loggningen. Olika tester av hur information kan föras över till frontend.js 2024-07-24 17:22:58 +02:00
Joakim Persson bf165d8181 Test med olika api-endpoint. 2024-07-24 17:20:40 +02:00
Joakim Persson fbcac82bbf För att få över information om API-endpoint till klienten. 2024-07-24 17:19:27 +02:00
joakimp 60ad3d109b Testar med mistral-nemo 2024-07-24 08:55:04 +02:00
joakimp 8ddb920e99 Felsökning. 2024-07-24 08:54:40 +02:00
joakimp adbf14c43c Försöker att förbättra loggningen 2024-07-23 23:28:37 +02:00
Joakim Persson a2144e0480 Tog bort sektionen <style>, länkar till extern stilmall istället. Lade till möjlighet att ändra storlek på chattfönstret. 2024-07-23 21:44:33 +02:00
Joakim Persson d0935f771a Extraherade stilmall till extern fil clientstyle.css 2024-07-23 21:41:51 +02:00
Joakim Persson 9bb9b76227 Snyggat till klienten med tydligare markeringar och bättre textinmatning. 2024-07-23 20:34:23 +02:00
Joakim Persson 3b2577084e FIxat problem med cirkulär kod. Tagit bort utkommenterad kod. 2024-07-23 14:24:31 +02:00
Joakim Persson eaba29fc6f Löst problem med cirkulär import. Tagit bort utkommenterad kod. 2024-07-23 14:23:42 +02:00
Joakim Persson a5d54c2cb6 Tog bort import från backend.py för att undvika cirkulär import 2024-07-23 14:22:30 +02:00
11 changed files with 1052 additions and 262 deletions
+19 -13
View File
@@ -1,30 +1,36 @@
# Frontend Configuration
frontend:
url: "http://localhost:8000"
# Backend Configuration # Backend Configuration
backend: backend:
url: "http://localhost:5005" url: "http://localhost:5004"
api: "/api/chat" api: "/api/chat"
# Ollama Server Configuration preferred_ep: "Ollama-WARA"
ollama:
endpoints:
- model: "AUTODETECT"
title: "Ollama-local" # Must be a unique identifier
url: "http://localhost:11434" url: "http://localhost:11434"
api_key: "${OLLAMA_API_KEY}" # Refer to environment variable provider: "ollama"
# model: "phi3:mini" # Select a model supported by the Ollama server # - model: "AUTODETECT"
# model: "llama3:70b" # Select a model supported by the Ollama server - model: "llava:13b"
# model: "llama3:latest" # Select a model supported by the Ollama server title: "Ollama-WARA" # Must be a unique identifier
model: "mannix/llama3-8b-ablitered-v3:latest" # Select a model supported by the Ollama server 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 comment out the whole section for default level which is INFO
logging: logging:
level: DEBUG # Set log level (DEBUG, INFO, WARNING, ERROR, CRITICAL) level: DEBUG # Set log level (DEBUG, INFO, WARNING, ERROR, CRITICAL)
# level: INFO # Set log level (DEBUG, INFO, WARNING, ERROR, CRITICAL)
# Additional Configuration Options (Optional ignored for now) # Additional Configuration Options (Optional ignored for now)
# Cache Settings (Optional) # Cache Settings (Optional)
cache: cache:
enabled: true enabled: True
timeout: 60 # Seconds timeout: 60 # Seconds
test: test:
+261 -51
View File
@@ -1,64 +1,193 @@
# 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 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
import logging import logging
import os
import utils import utils
from utils import set_local_logger, run_flask from utils import GlobalState
from pathlib import Path
global log_level # Global variable to store the log level that is set in startservices.py # Create a logger for this module
logger = logging.getLogger(__name__) # Separate logger for this module global_state = GlobalState() # Import the singleton that holds global states (e.g., logger)
set_local_logger(logger) # Set log level for logger based on log_level 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)
logger.debug("Current working directory: %s", os.getcwd())
# Initialize a Flask application # Initialize a Flask application
app = Flask(__name__) app = Flask(__name__)
# CORS(app, resources={r"/api/chat": {"origins": "*", "headers": ["Origin", "Content-Type", "Authorization"]}}) # Allow requests from any origin app.config['STATIC_FOLDER'] = 'static' # Adjust if needed
CORS(app, resources={
r"/api/chat": { # Increase the maximum cookie size
"origins": "*", app.config['SESSION_COOKIE_SAMESITE'] = 'Lax'
"headers": ["Origin", "Content-Type", "Authorization"], 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?
# Optionally set other configuration options
app.config['SESSION_PERMANENT'] = False # Session will expire after each request
app.config['SESSION_TYPE'] = 'filesystem' # Store sessions on the filesystem
logger.debug("flask app template folder: %s", app.template_folder)
@app.route('/')
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
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 (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)
@app.route('/set_session')
def set_session():
resp = make_response()
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')
# if user_id:
# return f'User ID: {user_id}'
# else:
# return 'No user ID found'
@app.route('/<path: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": "*",
# "headers": ["Origin", "Content-Type", "Authorization"],
# }
# })
# CORS(app, resources={
# r"/api/chat": {
# "origins": "*"
# }
# })
@app.route('/api/tags', methods=['GET'])
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()
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']) @app.route('/api/chat', methods=['POST'])
def chat(url_server = "http://localhost:11434/api/generate", model = "phi3:mini"): def chat() -> dict[str, any]:
""" """
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', 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
# print(f"data = {data}\nmessage = {message}") # Get chat history from session storage (e.g., a dictionary)
logger.debug("data = %s\nmessage = %s", str(data), str(message)) chat_history = session.get('chat_history', [])
try:
# Alternative LLM: "model": "mannix/llama3-8b-ablitered-v3:latest", # Add the new message to the chat history
url = url_server chat_history.append({'role': 'user', 'message': message})
model_to_use = model
data = { # Update the session with the new chat history
"model": model_to_use, session['chat_history'] = chat_history
'prompt': message,
# Create the data dictionary with chat history
data_to_send = {
"model": model,
'prompt': '\n'.join([f"{item['role']}: {item['message']}" for item in chat_history]),
"stream": False "stream": False
} }
headers = { url = url_server
"Content-Type": "application/json", headers = get_auth_headers(url)
}
# With API key
# headers = {
# "Content-Type": "application/json",
# "Authorization": "Bearer YOUR_API_KEY" # Replace with your API key
# }
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}")
response = requests.post(url, response = requests.post(url,
headers=headers, headers=headers,
data=json.dumps(data)) data=json.dumps(data_to_send))
response.raise_for_status() # Raise an exception for bad status codes response.raise_for_status() # Raise an exception for bad status codes
llm_response = response.json()['response'] # Assuming the LLM's response is under 'response' key
chat_history.append({'role': 'assistant', 'message': llm_response}) # Add assistant response to chat history
logger.debug(f"Chat History: {chat_history}")
return response.json() return response.json()
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))
@@ -68,6 +197,60 @@ def chat(url_server = "http://localhost:11434/api/generate", 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)}")
# Reset the session
if (title != global_state.get_host_title()) or (llm != global_state.get_llm()): # A change in setting
session.clear()
logger.debug('Session cleared due to changed endpoint or changed LLM')
global_state.set_host_url(endpoints[0]['url'])
global_state.set_llm(llm)
logger.debug(f"Updated to host url {endpoints[0]['url']} and LLM {llm}")
return jsonify({'message': 'New endpoint and/or LLM detected, settings were changed successfully'})
else:
return jsonify({'message': 'Endpoint and LLM are untouched'})
@app.route('/smartassist', methods=["POST"]) @app.route('/smartassist', methods=["POST"])
def smartassist(): def smartassist():
# Extract the query from the incoming JSON data # Extract the query from the incoming JSON data
@@ -82,27 +265,54 @@ def smartassist():
return jsonify({"response": response}) return jsonify({"response": response})
def get_response(user_query): def get_response(user_query):
# Create a client object for interacting with OLLAMA API client = Client() # Create a client object for interacting with OLLAMA API
client = Client() response = client.generate_response(user_query) # Generate and retrieve the response based on user's query
# Generate and retrieve the response based on user's query
response = client.generate_response(user_query)
# Return the generated response
return response return response
# def run_flask(fport=5005): def get_auth_headers(url: str) -> dict:
# """ """
# Starts the Flask server Returns authentication headers for a given URL.
# """
# # Flask endpoint for user interaction This function checks if an endpoint with the provided URL exists in the global state,
# logger.debug("Entering run_flask()") and returns the corresponding authentication headers. If no such endpoint is found,
# # app.run(port = str(str(fport)), debug=False) it returns a default header.
# app.run(port = str(str(fport)), debug=True) """
# # app.run(port=5000, debug=True, use_reloader=False) # TODO: The full operation should only have to run when changing to new endpoint.
# logger.debug("Exiting run_flask()")
# 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):
"""
Starts the Flask server
"""
# Flask endpoint for user interaction
logger.debug("Entering run_flask()")
# app.run(port = str(str(fport)), debug=False)
app.run(port = str(str(fport)), debug=True)
# app.run(port=5000, debug=True, use_reloader=False)
logger.debug("Exiting run_flask()")
if __name__ == '__main__': if __name__ == '__main__':
# Run the Flask application # Run the Flask application
run_flask(app) run_flask()
-50
View File
@@ -1,50 +0,0 @@
<!--
Run a simple HTTP server in the directory containing your HTML file
python -m http.server 8000
Open your web browser and navigate to http://localhost:8000/smartassist/src/client.html
Note that the path starts from where the python venv was defined (project root)
-->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Ollama Interaction</title>
<style>
/* Basic styling */
body { font-family: Arial, sans-serif; }
#chatbox {
width: 80%; /* responsive width */
max-width: 400px;
height: 300px;
border: 1px solid #ccc;
overflow-y: scroll;
padding: 10px;
}
.message {
margin-bottom: 10px;
}
.user-message {
background-color: #eae2d5;
padding: 10px;
border-radius: 10px;
}
.ai-response {
background-color: #cfc68b;
padding: 10px;
border-radius: 10px;
}
</style>
</head>
<body>
<h1>Ollama Interaction</h1>
<div id="chatbox">
<!-- messages will be rendered here -->
</div>
<input type="text" id="userInput" placeholder="Type your message here...">
<button onclick="sendMessage()">Send</button>
<!-- Link to the external frontend.js script (relative or absolute path)-->
<script src="frontend.js"></script>
</body>
</html>
+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
}
-53
View File
@@ -1,53 +0,0 @@
// Get the user input element from the DOM
const chatbox = document.getElementById('chatbox');
const userInput = document.getElementById('userInput');
// 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 !== '') {
// Send a POST request to the /api/chat endpoint with the message
fetch('http://localhost:5005/api/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ query }),
})
.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;
// Append the message element to the chatbox
chatbox.appendChild(messageElement);
}
+72
View File
@@ -0,0 +1,72 @@
<!DOCTYPE html>
<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"> -->
</head>
<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>
<textarea id="userInput" placeholder="Type your message..." rows="5"></textarea>
<button id="sendButton" onclick="window.frontendApi.sendMessage()">Send</button>
<!-- Marked-it for markdown rendering -->
<script src="https://cdn.jsdelivr.net/npm/markdown-it@14.1.0/dist/markdown-it.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/markdown-it@14/dist/markdown-it.min.js"></script>
<!-- Include MathJax library to render mathematical notation -->
<script id="MathJax-script" async src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js"></script>
<script>
window.MathJax = {
loader: { load: ['input/tex', 'output/chtml'] },
tex: {
packages: ['base', 'ams'],
inlineMath: [['$', '$']]
}
};
</script>
<script>
const chatContainer = document.getElementById('chatbox');
// Handle resize events
window.addEventListener('resize',
function() {
chatContainer.style.height = 'auto';
});
const userInputElement = document.getElementById('userInput');
userInputElement.addEventListener('keydown', function(event) {
if (event.shiftKey && event.key === 'Enter') { // Shift+Enter for newline
event.preventDefault();
userInputElement.value += '\n';
} else if (event.key === 'Enter') { // Enter to send message
window.frontendApi.sendMessage();
userInputElement.value = ''; // Clear the input field after sending
event.preventDefault();
}
});
</script>
<!-- Get the javascript handling communication with the backend -->
<script src="/js/frontend.js"></script>
</body>
</html>
+66 -64
View File
@@ -3,39 +3,26 @@ import subprocess
import os import os
import yaml import yaml
import json import json
# from backend import run_flask
import socket import socket
import urllib.parse import urllib.parse
from backend import run_flask
import logging import logging
import requests
import utils import utils
from utils import set_local_logger, run_flask from utils import GlobalState
from enums import LogLevel
# global log_level 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
# logger = logging.getLogger(__name__)
# def set_local_logger(log_instance):
# """
# Configure logging based on the global variable log_level
# Logging is controlled by integer values, where DEBUG < INFO < WARNING < ERROR < CRITICAL.
# To turn off logging completely, set numeric_log_level to at least CRITICAL + 1.
# """
# global log_level
# numeric_log_level = getattr(logging, log_level, None)
# if not isinstance(numeric_log_level, int):
# raise ValueError('Invalid log level: %s' % log_level)
# logging.basicConfig(level=numeric_log_level) # Set the root logger level to the configured level
# log_instance.info('Log level set to {}'.format(log_level)) # Example usage of the logger
def configure(): def configure():
""" """
Reads YAML configruation file into dictionary, parse it and fill all referenceed Reads YAML configruation file into dictionary, parse it and fill all referenceed
environment variables with their values. environment variables with their values.
""" """
################## ####################################
# Read YAML config # Read YAML config
################## ####################################
# Load configuration file that defines parameters for services # Load configuration file that defines parameters for services
with open('./smartassist/config/smartassist.yaml') as f: with open('./smartassist/config/smartassist.yaml') as f:
config = yaml.safe_load(f) config = yaml.safe_load(f)
@@ -46,71 +33,86 @@ def configure():
return os.getenv(env_var_name, None) return os.getenv(env_var_name, None)
return value return value
def update_dict_with_env_vars(d): def update_value(value):
for key in d: if isinstance(value, dict): # Dictionaries need recursive check
if isinstance(d[key], dict): return update_dict_with_env_vars(value)
update_dict_with_env_vars(d[key]) # Recursively check nested dictionaries elif isinstance(value, list): # Lists must be traversed element by element
elif isinstance(d[key], str): return [update_value(item) for item in value]
d[key] = resolve_env_var(d[key]) 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): # 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 return d
# Update the config dictionary with resolved environment variables
updated_config = update_dict_with_env_vars(config) updated_config = update_dict_with_env_vars(config)
################## ####################################
# Extract global logging level # Extract global logging level
################## ####################################
# The log_level variable will be used by the logger module to set the log level
global log_level # REALLY NECESSARY TO DEFINE THIS GLOBAL AGAIN???
log_level = 'INFO' # Default value if not specified in the config file
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
log_level = logging_config['level'] # 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())
set_local_logger(utils.logger) # Set log level for logger based on log_level ####################################
# Extract models (server url, api_key, model, et cetera)
####################################
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
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 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))
utils.logger.info("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:
utils.logger.error("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:
utils.logger.error("Failed to start frontend: %s", str(e)) # Corresponds to print(f"Failed to start frontend: {e}")
def start_backend(config): def start_backend(config):
parsed_url = urllib.parse.urlparse(config['backend']['url']) parsed_url = urllib.parse.urlparse(config['backend']['url'])
# hostname = parsed_url.netloc.split(':')[0] # Split by ':' and take the first part, i.e., 'localhost', IP, or domain name # 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 port = parsed_url.port # This is the server port
utils.logger.debug('Backend parsed url set to {}'.format(parsed_url)) logger.debug('Backend parsed url set to {}'.format(parsed_url))
utils.logger.debug('Backend port set to {}'.format(port)) logger.debug('Backend port set to {}'.format(port))
try: try:
run_flask(fport = port) run_flask(fport = port)
except Exception as e: except Exception as e:
utils.logger.error("Failed to start backend: %s", str(e)) # Corresponds to print(f"Failed to start backend: {e}") logger.error("Failed to start backend: %s", str(e)) # Corresponds to print(f"Failed to start backend: {e}")
if __name__ == '__main__': if __name__ == '__main__':
conf = configure() # Read config from file and set up config dict conf = configure() # Read config from file and set up config dict
utils.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) # start_frontend(config=conf) # Not needed as we are using Flask for backend now
start_backend(config=conf) start_backend(config=conf)
+118
View File
@@ -0,0 +1,118 @@
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background-color: #f4f4f4;
display: flex;
flex-direction: column;
align-items: center;
min-height: 100vh;
margin: 0;
}
h1 {
color: #333;
margin-bottom: 20px;
}
#chatbox {
width: calc(50% - 60px); /* Adjust width for input and button */
/* max-width: 500px; */
height: 600px;
/* background-color: #fff8bc; */
background-color: #ffffff;
border-radius: 10px;
padding: 20px;
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
overflow: auto; /* Allow horizontal and vertical scrolling of the chatbox */
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 {
margin-bottom: 15px;
}
.user-message {
background-color: #9cc1ecbb;
padding: 10px 15px;
border-radius: 10px;
text-align: left; /* Align user messages to the left */
}
.ai-response {
/* background-color: #f0f8ff; */
background-color: #f5ecd0;
padding: 10px 15px;
border-radius: 10px;
text-align: left; /* Align AI responses to the left */
}
#userInput {
width: calc(50% - 60px); /* Adjust width for input and button */
padding: 10px;
border: 1px solid #ccc;
border-radius: 5px;
margin-bottom: 10px;
font-family: 'Courier New', Courier, monospace; /* Fixed width typeface */
}
#userInput:focus {
outline: none;
border-color: #66afe9; /* Blue outline on focus */
}
button[onclick="window.frontendApi.sendMessage()"] {
background-color: #4CAF50; /* Green */
border: none;
color: white;
padding: 10px 20px;
text-align: center;
text-decoration: none;
display: inline-block;
font-size: 16px;
border-radius: 5px;
cursor: pointer;
transition: background-color 0.3s; /* Smooth transition effect */
}
button[onclick="window.frontendApi.sendMessage()"]:hover {
background-color: #b2b2b2; /* Light Grey on hover */
}
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;
}
+150
View File
@@ -0,0 +1,150 @@
// 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']);
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
const frontendApi = {
// Define a function to send the user's message to the AI
sendMessage: function() {
if (!window.apiEndpoint || !window.useModel) { // Check if we're ready before proceeding
console.error("Not ready yet. Please wait for apiEndpoint and useModel to be set.");
return;
}
// 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(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())
.then(data => {
// Get the AI's response from the API data
const aiResponse = data.response;
// Render the user's original message in the chatbox
this.renderMessage(query, 'user-message');
// Render the AI's response in the chatbox
this.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
renderMessage: function(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;
// Use the markdown-it parser
const html = parser.render(text);
messageElement.innerHTML = html;
// 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) {
if (event.origin === 'http://localhost:5004') { // Make sure this matches your origin
const { apiEndpoint, useModel } = event.data;
console.log("fronend.js - API Endpoint: ", apiEndpoint);
console.log("fronend.js - use model: ", useModel);
window.apiEndpoint = apiEndpoint;
window.useModel = useModel;
}
});
// Wait for the DOM to be fully loaded before making the API available
document.addEventListener('DOMContentLoaded', function() {
window.frontendApi = frontendApi;
});
// 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 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);
+54
View File
@@ -0,0 +1,54 @@
<!DOCTYPE html>
<html>
<head>
<!-- <title>Frontend</title> -->
</head>
<body>
<!-- This iframe will hold the content from client.html -->
<!-- Passing the API endpoint as a query parameter to the srcdoc attribute -->
<!-- srcdoc="{{ client_content }}?apiEndpoint={{ api_endpoint }}/"> -->
<iframe id="client-frame"
style="width: 100%; height: 100vh;"
srcdoc="{{ client_content }}">
</iframe>
<!-- <script>
// Extract apiEndpoint for use in your frontend code...
const apiEndpoint = '{{ api_endpoint }}'; // Templating syntax (Jinja2)
const useModel = '{{ use_model }}'; // Templating syntax (Jinja2)
// Tell the iframe about the apiEndpoint
document.getElementById('client-frame').contentWindow.apiEndpoint = apiEndpoint;
document.getElementById('client-frame').contentWindow.useModel = useModel;
console.log("index.html - API Endpoint: ", apiEndpoint);
console.log("index.html - use model: ", useModel);
</script> -->
<script>
// Extract apiEndpoint for use in frontend.js
const apiEndpoint = '{{ api_endpoint }}'; // Templating syntax (Jinja2)
const useModel = '{{ use_model }}'; // Templating syntax (Jinja2)
window.addEventListener('load', function() {
const clientFrame = document.getElementById('client-frame').contentWindow;
clientFrame.postMessage({ apiEndpoint, useModel }, '*'); // Send the data to the iframe
});
console.log("index.html - API Endpoint: ", apiEndpoint);
console.log("index.html - use model: ", useModel);
</script>
<!-- Responsive scaling and some padding -->
<script>
const clientFrame = document.getElementById('client-frame');
function resizeIframe() {
clientFrame.style.height = window.innerHeight - 50 + 'px'; // Adjust the subtraction for padding/margins if needed
}
window.addEventListener('resize', resizeIframe);
resizeIframe(); // Call it once on page load
</script>
</body>
</html>
+286 -23
View File
@@ -2,34 +2,297 @@
# imported to more than one other module. The rational for defining these things here # 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. # is that it is easier to avoid circular imports when they are defined in a central location.
import logging import logging
import json
import requests
from typing import Optional
from enums import LogLevel, LOG_LEVEL_MAPPING
global log_level class GlobalState:
log_level = 'INFO'
logger = logging.getLogger(__name__)
def set_local_logger(log_instance):
""" """
Configure logging based on the global variable log_level This class holds various variables and methods which are accessible across
Logging is controlled by integer values, where DEBUG < INFO < WARNING < ERROR < CRITICAL. different modules in the Python project using the Singleton design pattern.
To turn off logging completely, set numeric_log_level to at least CRITICAL + 1. This ensures that only one instance of the class is created and shared among
all modules, preventing circular imports and providing a centralized location
for managing shared resources.
""" """
global log_level _instance = None # Private class attribute to hold the single instance of the class
numeric_log_level = getattr(logging, log_level, None)
if not isinstance(numeric_log_level, int):
raise ValueError('Invalid log level: %s' % log_level)
logging.basicConfig(level=numeric_log_level) # Set the root logger level to the configured level def __new__(cls) -> 'GlobalState':
log_instance.info('Log level set to {}'.format(log_level)) # Example usage of the logger """
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
cls._instance.logger = logging.getLogger() # Get root logger for the caller module
handler = logging.StreamHandler() # Or other handler (FileHandler for logs to file)
formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s")
handler.setFormatter(formatter)
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
# 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 run_flask(app, fport=5005): def configure_logging(self, level: Optional[LogLevel] = None) -> None:
""" """
Starts the Flask server 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.
""" """
# Flask endpoint for user interaction if level == None:
logger.debug("Entering run_flask()") level = self.log_level
# app.run(port = str(str(fport)), debug=False) if isinstance(level, LogLevel):
app.run(port = str(str(fport)), debug=True) logging.info(f"Trying to set up logging with level {level}")
# app.run(port=5000, debug=True, use_reloader=False) numeric_level = LOG_LEVEL_MAPPING[level]
logger.debug("Exiting run_flask()") 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.
"""
self.log_level = level
self.configure_logging()
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) -> int:
"""
Get the effective log level of the logger.
Returns:
int: The numeric value of the effective log level.
"""
return self.logger.getEffectiveLevel()
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_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 URL of the host currently used for LLMs.
Returns:
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.
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) -> str:
"""
Get the current LLM used for queries.
Returns:
str: The name of the current LLM.
"""
return self.llm
def set_backend(self, backend: Optional[dict] = None) -> None:
"""
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