Compare commits
98 Commits
ccc8e73f48
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 82c7887cf7 | |||
| 5f546a848e | |||
| 7eb889a889 | |||
| 6dab09c861 | |||
| 8288450662 | |||
| ae67b9c7b3 | |||
| 97ee179b29 | |||
| 942f9f78c9 | |||
| 3f39e11b10 | |||
| 32098e3452 | |||
| 5b143e75e0 | |||
| 168b8b13c1 | |||
| f7f6ce2e49 | |||
| fa98c7b162 | |||
| fd5f6199e9 | |||
| c26dbc5612 | |||
| 7f557fadd6 | |||
| 5ce92a5602 | |||
| 6dc93b66be | |||
| 606becc5c3 | |||
| 9717202bb4 | |||
| dc209b3595 | |||
| 8ac365862a | |||
| ab9bb1324c | |||
| 17c20a4ce8 | |||
| 210a75e8bf | |||
| 01d4a5f314 | |||
| 4621cf6cbf | |||
| 56f9038e6c | |||
| c7630bf6b3 | |||
| 0db750358e | |||
| 8452c7569b | |||
| cb1caceee7 | |||
| 9e86617ae9 | |||
| c0b97871e7 | |||
| 44893fce39 | |||
| a4189360d1 | |||
| 8e983919e5 | |||
| 9e2cebd6cc | |||
| 352e704537 | |||
| 76f20e5cf8 | |||
| cf9fcc46dd | |||
| f160092b2a | |||
| 06628d5c19 | |||
| 2e24be1e44 | |||
| 41c0c86d82 | |||
| 5aa47d11ea | |||
| 5f2b71c965 | |||
| 3659308675 | |||
| a72a46d777 | |||
| a7ce92524a | |||
| b52c98c6b3 | |||
| c8269f3152 | |||
| d553deed13 | |||
| ab23585711 | |||
| 916b6f9e52 | |||
| cfed550a3e | |||
| 5ee7ae520f | |||
| 1f6f0a72d5 | |||
| 9e6e6048f3 | |||
| 538f02e6a7 | |||
| 810b369721 | |||
| 9203641e6a | |||
| 14eaf57335 | |||
| 9ae5840e8b | |||
| 659b0937d3 | |||
| 7bc8a129c6 | |||
| a12c11c058 | |||
| 5771e52c49 | |||
| d038f1ac24 | |||
| 86374c91fe | |||
| 0adbb9c222 | |||
| c3d7f3ba4f | |||
| d864d7529a | |||
| 0b971dffc4 | |||
| ecf45bd2e7 | |||
| b88e573761 | |||
| ecf2ddf46e | |||
| b4a35bafef | |||
| de3e59afea | |||
| 9f5bdddb8f | |||
| 854ebbdd94 | |||
| 7d954e7380 | |||
| d843a2b74c | |||
| ab94c06e75 | |||
| 1757542077 | |||
| 941b426574 | |||
| 534874e810 | |||
| 0cccd02050 | |||
| e69a131875 | |||
| d797440288 | |||
| 1b5624018c | |||
| 807a766a63 | |||
| 5245e53954 | |||
| d1b0554748 | |||
| f8c3bcd156 | |||
| 25e5c6acea | |||
| 20fa94f533 |
+2
-1
@@ -161,4 +161,5 @@ cython_debug/
|
|||||||
#.idea/
|
#.idea/
|
||||||
|
|
||||||
# Exclude venv from smartassist
|
# Exclude venv from smartassist
|
||||||
smartassist/smartassist_dev_venv
|
smartassist/smartassist_dev_venv
|
||||||
|
.DS_Store
|
||||||
|
|||||||
@@ -1,22 +1,25 @@
|
|||||||
# Frontend Configuration
|
|
||||||
frontend:
|
|
||||||
url: "http://localhost:5004"
|
|
||||||
|
|
||||||
# Backend Configuration
|
# Backend Configuration
|
||||||
backend:
|
backend:
|
||||||
url: "http://localhost:5004"
|
url: "http://localhost:5004"
|
||||||
api: "/api/chat"
|
api: "/api/chat"
|
||||||
|
|
||||||
# Ollama Server Configuration
|
preferred_ep: "Ollama-WARA"
|
||||||
ollama:
|
|
||||||
url: "http://localhost:11434"
|
endpoints:
|
||||||
api_key: "${OLLAMA_API_KEY}" # Refer to environment variable
|
- model: "AUTODETECT"
|
||||||
# model: "phi3:mini" # Select a model supported by the Ollama server
|
title: "Ollama-local" # Must be a unique identifier
|
||||||
# model: "llama3:70b" # Select a model supported by the Ollama server
|
url: "http://localhost:11434"
|
||||||
# model: "llama3:latest" # Select a model supported by the Ollama server
|
provider: "ollama"
|
||||||
# model: "mannix/llama3-8b-ablitered-v3:latest" # Select a model supported by the Ollama server
|
# - model: "AUTODETECT"
|
||||||
# model: "mistral-nemo:latest" # Select a model supported by the Ollama server
|
- model: "llava:13b"
|
||||||
model: "gemma2:27b"
|
title: "Ollama-WARA" # Must be a unique identifier
|
||||||
|
url: "https://ollama-test.wara-ops.org"
|
||||||
|
requestOptions:
|
||||||
|
headers:
|
||||||
|
Authorization: "${OLLAMA_API_KEY}" # on MacOS: echo "Authorization: Basic $(echo -n 'user:password' | gbase64 -w 0)"
|
||||||
|
provider: "ollama"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# Logging – comment out the whole section for default level which is INFO
|
# Logging – comment out the whole section for default level which is INFO
|
||||||
logging:
|
logging:
|
||||||
@@ -27,7 +30,7 @@ logging:
|
|||||||
|
|
||||||
# Cache Settings (Optional)
|
# Cache Settings (Optional)
|
||||||
cache:
|
cache:
|
||||||
enabled: true
|
enabled: True
|
||||||
timeout: 60 # Seconds
|
timeout: 60 # Seconds
|
||||||
|
|
||||||
test:
|
test:
|
||||||
|
|||||||
+225
-46
@@ -1,17 +1,20 @@
|
|||||||
|
|
||||||
# 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
|
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 os
|
||||||
# from utils import set_local_logger
|
import utils
|
||||||
|
from utils import GlobalState
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
# Create a logger for this module
|
# Create a logger for this module
|
||||||
logger = logging.getLogger(__name__) # This logger will be used to log messages from this module
|
global_state = GlobalState() # Import the singleton that holds global states (e.g., logger)
|
||||||
logger.debug("Logging level of backend logger has been configured")
|
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)
|
||||||
logger.debug("Current working directory: %s", os.getcwd())
|
logger.debug("Current working directory: %s", os.getcwd())
|
||||||
@@ -20,29 +23,81 @@ logger.debug("Current working directory: %s", os.getcwd())
|
|||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
app.config['STATIC_FOLDER'] = 'static' # Adjust if needed
|
app.config['STATIC_FOLDER'] = 'static' # Adjust if needed
|
||||||
|
|
||||||
|
# Increase the maximum cookie size
|
||||||
|
app.config['SESSION_COOKIE_SAMESITE'] = 'Lax'
|
||||||
|
app.config['SESSION_COOKIE_SIZE_LIMIT'] = 4096 * 2 # Allow up to 8KB cookies
|
||||||
|
|
||||||
|
# Set the secret key for session management
|
||||||
|
secret_key = os.urandom(24)
|
||||||
|
app.config['SECRET_KEY'] = secret_key # When do I need this. How is it retained between sessions?
|
||||||
|
|
||||||
|
# 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)
|
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
|
||||||
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
|
||||||
logger.debug("API endpoint: %s", api_endpoint)
|
host_url = global_state.get_host_url()
|
||||||
|
use_model = global_state.get_llm()
|
||||||
|
logger.debug("Backend API endpoint:\t%s", api_endpoint)
|
||||||
|
logger.debug("Host of LLMs:\t\t%s", host_url)
|
||||||
|
logger.debug("LLM to use:\t\t\t%s", use_model)
|
||||||
with open('smartassist/src/html/client.html', 'r') as f:
|
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 (first few 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)
|
||||||
|
|
||||||
|
@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'
|
||||||
|
|
||||||
|
|
||||||
return render_template('index.html', api_endpoint=api_endpoint, client_content=client_html)
|
|
||||||
|
|
||||||
@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": "*",
|
||||||
@@ -50,46 +105,89 @@ def serve_static(filename):
|
|||||||
# }
|
# }
|
||||||
# })
|
# })
|
||||||
|
|
||||||
CORS(app, resources={
|
# CORS(app, resources={
|
||||||
r"/api/chat": {
|
# r"/api/chat": {
|
||||||
"origins": "*"
|
# "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
|
||||||
|
|
||||||
|
# Get chat history from session storage (e.g., a dictionary)
|
||||||
|
chat_history = session.get('chat_history', [])
|
||||||
|
|
||||||
|
# Add the new message to the chat history
|
||||||
|
chat_history.append({'role': 'user', 'message': message})
|
||||||
|
|
||||||
|
# Update the session with the new chat history
|
||||||
|
session['chat_history'] = chat_history
|
||||||
|
|
||||||
|
# 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
|
||||||
|
}
|
||||||
|
url = url_server
|
||||||
|
headers = get_auth_headers(url)
|
||||||
|
|
||||||
logger.debug("data = %s\nmessage = %s", str(data), str(message))
|
logger.debug(f"Sending request to:\n\turl:\t{url}\n\tmodel:\t{model}")
|
||||||
try:
|
try:
|
||||||
url = url_server
|
url = url + "/api/generate"
|
||||||
model_to_use = model
|
logger.debug(f"url: {url} headers: {headers}")
|
||||||
data = {
|
|
||||||
"model": model_to_use,
|
|
||||||
'prompt': message,
|
|
||||||
"stream": False
|
|
||||||
}
|
|
||||||
headers = {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
}
|
|
||||||
# With API key
|
|
||||||
# headers = {
|
|
||||||
# "Content-Type": "application/json",
|
|
||||||
# "Authorization": "Bearer YOUR_API_KEY" # Replace with your API key
|
|
||||||
# }
|
|
||||||
|
|
||||||
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))
|
||||||
@@ -99,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
|
||||||
@@ -113,12 +265,39 @@ 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
|
return response
|
||||||
response = client.generate_response(user_query)
|
|
||||||
# Return the generated response
|
def get_auth_headers(url: str) -> dict:
|
||||||
return response
|
"""
|
||||||
|
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):
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -2,25 +2,52 @@
|
|||||||
<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"> -->
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<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>
|
||||||
<textarea id="userInput" placeholder="Type your message..." rows="5"></textarea>
|
<textarea id="userInput" placeholder="Type your message..." rows="5"></textarea>
|
||||||
<button onclick="sendMessage()">Send</button>
|
<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 src="/js/frontend.js"></script>
|
|
||||||
<script>
|
<script>
|
||||||
const chatContainer = document.getElementById('chatbox');
|
const chatContainer = document.getElementById('chatbox');
|
||||||
|
|
||||||
// Handle resize events
|
// Handle resize events
|
||||||
window.addEventListener('resize', function() {
|
window.addEventListener('resize',
|
||||||
chatContainer.style.height = 'auto';
|
function() {
|
||||||
|
chatContainer.style.height = 'auto';
|
||||||
});
|
});
|
||||||
|
|
||||||
const userInputElement = document.getElementById('userInput');
|
const userInputElement = document.getElementById('userInput');
|
||||||
@@ -30,13 +57,16 @@
|
|||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
userInputElement.value += '\n';
|
userInputElement.value += '\n';
|
||||||
} else if (event.key === 'Enter') { // Enter to send message
|
} else if (event.key === 'Enter') { // Enter to send message
|
||||||
sendMessage();
|
window.frontendApi.sendMessage();
|
||||||
userInputElement.value = ''; // Clear the input field after sending
|
userInputElement.value = ''; // Clear the input field after sending
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<!-- Get the javascript handling communication with the backend -->
|
||||||
|
<script src="/js/frontend.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
||||||
|
|||||||
@@ -5,13 +5,15 @@ import yaml
|
|||||||
import json
|
import json
|
||||||
import socket
|
import socket
|
||||||
import urllib.parse
|
import urllib.parse
|
||||||
import logging
|
|
||||||
import utils
|
|
||||||
from utils import configure_logging
|
|
||||||
from backend import run_flask
|
from backend import run_flask
|
||||||
|
import logging
|
||||||
|
import requests
|
||||||
|
import utils
|
||||||
|
from utils import GlobalState
|
||||||
|
from enums import LogLevel
|
||||||
|
|
||||||
configure_logging() # 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 = logging.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():
|
||||||
"""
|
"""
|
||||||
@@ -30,72 +32,71 @@ def configure():
|
|||||||
env_var_name = value[2:-1] # Extract name between ${}
|
env_var_name = value[2:-1] # Extract name between ${}
|
||||||
return os.getenv(env_var_name, None)
|
return os.getenv(env_var_name, None)
|
||||||
return value
|
return value
|
||||||
|
|
||||||
|
def update_value(value):
|
||||||
|
if isinstance(value, dict): # Dictionaries need recursive check
|
||||||
|
return update_dict_with_env_vars(value)
|
||||||
|
elif isinstance(value, list): # Lists must be traversed element by element
|
||||||
|
return [update_value(item) for item in value]
|
||||||
|
elif isinstance(value, str): # If value is a string it might be an environmnet variable
|
||||||
|
return resolve_env_var(value)
|
||||||
|
else: # Anything else, just keep the old value
|
||||||
|
return value
|
||||||
|
|
||||||
def update_dict_with_env_vars(d):
|
def update_dict_with_env_vars(d): # Check all keys in d
|
||||||
for key in d:
|
for key in d: # Iterate over all keys in the dictionary. The keys seen are all at the top-level of d
|
||||||
if isinstance(d[key], dict):
|
# logger.info(f"key investigated now: {key}")
|
||||||
update_dict_with_env_vars(d[key]) # Recursively check nested dictionaries
|
d[key] = update_value(d[key])
|
||||||
elif isinstance(d[key], str):
|
|
||||||
d[key] = resolve_env_var(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
|
||||||
####################################
|
####################################
|
||||||
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.info("found key 'logging' 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
|
||||||
# logging.info("found key 'level' in config file")
|
# global_state.set_log_level(logging_config['level'])
|
||||||
utils.log_level = logging_config['level']
|
global_state.set_log_level(LogLevel(logging_config['level']))
|
||||||
numeric_log_level = getattr(logging, utils.log_level, None)
|
logger.info("configure(): This logger now has effective log level %s", logger.getEffectiveLevel())
|
||||||
|
|
||||||
rlogger = logging.getLogger() # Get the root logger
|
|
||||||
rlogger.setLevel(numeric_log_level)
|
|
||||||
# set_local_logger(logger) # Set log level for logger based on log_level
|
|
||||||
logger.info("Global variable utils.log_level set to: {}".format(utils.log_level))
|
|
||||||
|
|
||||||
####################################
|
####################################
|
||||||
# Extract and export API endpoint as
|
# Extract models (server url, api_key, model, et cetera)
|
||||||
# envrionment variable
|
|
||||||
####################################
|
####################################
|
||||||
backend_api_ep = 'http://localhost:5005/api/chat' # Default API endpoint
|
if isinstance(updated_config.get('backend'),dict): # Extract backend info from dictionary
|
||||||
if isinstance(updated_config.get('backend'), dict): # Look for 'backend' key in config file
|
global_state.set_backend(backend=updated_config.get('backend'))
|
||||||
if isinstance(updated_config['backend'].get('url'), str): # Look for 'url' key in config file
|
logger.debug("backend = \n{}".format(json.dumps(global_state.get_backend(), indent=4)))
|
||||||
url = updated_config['backend'].get('url')
|
logger.debug(f"Backend API endpoint is set to: {global_state.get_backend_api_ep()}")
|
||||||
if isinstance(updated_config['backend'].get('api'), str): # Look for 'api' key in config file
|
|
||||||
api = updated_config['backend'].get('api')
|
preferred_ep = updated_config.get('preferred_ep', None) # Get the preferred endpoint if specified, otherwise None
|
||||||
backend_api_ep = url+api # Extract API endpoint if defined
|
|
||||||
logger.debug("BE_API_ENDPOINT is set to '{}'".format(backend_api_ep))
|
|
||||||
os.environ['BE_API_ENDPOINT'] = backend_api_ep
|
|
||||||
|
|
||||||
return updated_config
|
if isinstance(updated_config.get('endpoints'), list): # Extract info on endpoint, model, url, provider et cetera from list
|
||||||
|
global_state.set_endpoints(endpoints=updated_config.get('endpoints')) # Extract and set list of endpoints
|
||||||
|
# logger.debug("endpoints = \n{}".format(json.dumps(global_state.get_endpoints(), indent=4)))
|
||||||
|
global_state.fetch_models()
|
||||||
|
endpoints = global_state.get_endpoints()
|
||||||
|
for endpoint in endpoints: # Set default LLM for each endpoint
|
||||||
|
available_llms = global_state.get_list_of_available_llms(endpoint=endpoint)
|
||||||
|
llm = next(iter(available_llms),None) # First available LLM or None. Default for AUTODETECT and requests for non-existing LLMs
|
||||||
|
logger.debug(f"url {endpoint['url']} = {available_llms}")
|
||||||
|
if endpoint["model"] in available_llms: # Check if specific LLM requested, AUTODETECT evaluates to False
|
||||||
|
llm = endpoint["model"]
|
||||||
|
endpoint["default_llm"] = llm
|
||||||
|
|
||||||
|
if preferred_ep: # If preferred_ep is specified, set it as the default endpoint
|
||||||
|
list_of_eps = global_state.get_endpoints_with_key_value("title", preferred_ep) # Should only be one element in the list...
|
||||||
|
default_endpoint = next(iter(list_of_eps),None) # Same as default_endpoint = list_of_eps[0] if list_of_eps else None
|
||||||
|
else:
|
||||||
|
default_endpoint = next(iter(endpoints),None) # Set default_endpoint to first endpoint from list of all endpoints
|
||||||
|
default_llm = default_endpoint["default_llm"] # Get default LLM for default_endpoint
|
||||||
|
default_ulr = default_endpoint["url"] # Get ulr of default_endpoint
|
||||||
|
global_state.set_host_url(default_ulr) # Set initial host to the first item in endpoints (or None)
|
||||||
|
global_state.set_llm(default_llm) # Set which llm to use
|
||||||
|
logger.debug(f"Desired default endpoint: {default_ulr},\tDesired default LLM: {default_llm}")
|
||||||
|
logger.debug(f"Returned default endpoint: {global_state.get_host_url()},\tReturned default LLM: {global_state.get_llm()}")
|
||||||
|
|
||||||
|
return updated_config
|
||||||
def start_frontend(config):
|
|
||||||
parsed_url = urllib.parse.urlparse(config['frontend']['url'])
|
|
||||||
hostname = parsed_url.netloc.split(':')[0] # Split by ':' and take the first part, i.e., 'localhost', IP, or domain name
|
|
||||||
port = parsed_url.port # This is the server port
|
|
||||||
|
|
||||||
# Use the socket module in Python to check whether a port is in use,
|
|
||||||
# which would indicate that a server is already running on that port.
|
|
||||||
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
|
||||||
try:
|
|
||||||
s.bind((hostname, port))
|
|
||||||
logger.debug("No server is running on %s -— starting one.", parsed_url.netloc)
|
|
||||||
# Start frontend (web server) as a separate process
|
|
||||||
subprocess.Popen(["python", "-m", "http.server", str(port)])
|
|
||||||
except socket.error as e:
|
|
||||||
if e.errno == 48:
|
|
||||||
logger.debug("A server is already running on %s -— will use this.", parsed_url.netloc)
|
|
||||||
else:
|
|
||||||
raise # Unexpected error, re-raise it so we can see the traceback
|
|
||||||
except Exception as e:
|
|
||||||
logger.error("Failed to start frontend: %s", str(e)) # Corresponds to print(f"Failed to start frontend: {e}")
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def start_backend(config):
|
def start_backend(config):
|
||||||
@@ -112,7 +113,6 @@ def start_backend(config):
|
|||||||
|
|
||||||
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
|
||||||
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)
|
||||||
|
|
||||||
|
|||||||
@@ -14,14 +14,19 @@ h1 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#chatbox {
|
#chatbox {
|
||||||
width: 80%;
|
width: calc(50% - 60px); /* Adjust width for input and button */
|
||||||
max-width: 500px;
|
/* max-width: 500px; */
|
||||||
background-color: #fff;
|
height: 600px;
|
||||||
|
/* background-color: #fff8bc; */
|
||||||
|
background-color: #ffffff;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
|
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
|
||||||
overflow: auto; /* Allow horizontal and vertical scrolling of the chatbox */
|
overflow: auto; /* Allow horizontal and vertical scrolling of the chatbox */
|
||||||
resize: both; /* Allow resizing vertically */
|
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 {
|
.message {
|
||||||
@@ -36,7 +41,8 @@ h1 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.ai-response {
|
.ai-response {
|
||||||
background-color: #f0f8ff;
|
/* background-color: #f0f8ff; */
|
||||||
|
background-color: #f5ecd0;
|
||||||
padding: 10px 15px;
|
padding: 10px 15px;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
text-align: left; /* Align AI responses to the left */
|
text-align: left; /* Align AI responses to the left */
|
||||||
@@ -48,6 +54,7 @@ h1 {
|
|||||||
border: 1px solid #ccc;
|
border: 1px solid #ccc;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
|
font-family: 'Courier New', Courier, monospace; /* Fixed width typeface */
|
||||||
}
|
}
|
||||||
|
|
||||||
#userInput:focus {
|
#userInput:focus {
|
||||||
@@ -55,7 +62,7 @@ h1 {
|
|||||||
border-color: #66afe9; /* Blue outline on focus */
|
border-color: #66afe9; /* Blue outline on focus */
|
||||||
}
|
}
|
||||||
|
|
||||||
button[onclick="sendMessage()"] {
|
button[onclick="window.frontendApi.sendMessage()"] {
|
||||||
background-color: #4CAF50; /* Green */
|
background-color: #4CAF50; /* Green */
|
||||||
border: none;
|
border: none;
|
||||||
color: white;
|
color: white;
|
||||||
@@ -66,4 +73,46 @@ button[onclick="sendMessage()"] {
|
|||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
cursor: pointer;
|
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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,60 +1,150 @@
|
|||||||
|
|
||||||
// Get the user input element from the DOM
|
// Get the user input element from the DOM
|
||||||
const chatbox = document.getElementById('chatbox');
|
const chatbox = document.getElementById('chatbox');
|
||||||
const userInput = document.getElementById('userInput');
|
const userInput = document.getElementById('userInput');
|
||||||
|
const parser = window.markdownit({
|
||||||
|
linkify: true,
|
||||||
|
strikethrough: true,
|
||||||
|
});
|
||||||
|
parser.enable(['table']);
|
||||||
|
|
||||||
// Get API endpoint for chat from environment variable
|
let apiEndpoint; // Make variable available outside of the scope of the event listener
|
||||||
const apiEndpoint = process.env.BE_API_ENDPOINT || 'http://localhost:5005/api/chat'; // Default if not found
|
let useModel; // Make variable available outside of the scope of the event listener
|
||||||
console.log("The API Endpoint is:", apiEndpoint);
|
|
||||||
|
|
||||||
|
const frontendApi = {
|
||||||
|
// Define a function to send the user's message to the AI
|
||||||
// Define a function to send the user's message to the AI
|
sendMessage: function() {
|
||||||
function sendMessage() {
|
if (!window.apiEndpoint || !window.useModel) { // Check if we're ready before proceeding
|
||||||
// Get the user's input message and trim any whitespace
|
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();
|
const query = userInput.value.trim();
|
||||||
|
|
||||||
// Check if the message is not empty
|
// Check if the message is not empty
|
||||||
if (query !== '') {
|
if (query !== '') {
|
||||||
// Send a POST request to the /api/chat endpoint with the message
|
fetch(window.apiEndpoint, {
|
||||||
// fetch('http://localhost:5005/api/chat', {
|
method: 'POST',
|
||||||
fetch(apiEndpoint, {
|
headers: { 'Content-Type': 'application/json' },
|
||||||
method: 'POST',
|
// body: JSON.stringify({ query }), // Add these parameters here
|
||||||
headers: { 'Content-Type': 'application/json' },
|
body: JSON.stringify({ query, model: window.useModel }), // Add these parameters here
|
||||||
body: JSON.stringify({ query }),
|
})
|
||||||
})
|
.then(response => response.json())
|
||||||
.then(response => response.json())
|
.then(data => {
|
||||||
.then(data => {
|
// Get the AI's response from the API data
|
||||||
// Get the AI's response from the API data
|
const aiResponse = data.response;
|
||||||
const aiResponse = data.response;
|
// Render the user's original message in the chatbox
|
||||||
|
this.renderMessage(query, 'user-message');
|
||||||
// Render the user's original message in the chatbox
|
// Render the AI's response in the chatbox
|
||||||
renderMessage(query, 'user-message');
|
this.renderMessage(aiResponse, 'ai-response');
|
||||||
|
// Clear the user input field for the next message
|
||||||
// Render the AI's response in the chatbox
|
userInput.value = '';
|
||||||
renderMessage(aiResponse, 'ai-response');
|
})
|
||||||
|
.catch(error => console.error('Error sending message:', error));
|
||||||
// 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) {
|
||||||
|
|
||||||
|
|
||||||
// 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
|
// Create a new div element to hold the message
|
||||||
const messageElement = document.createElement('div');
|
const messageElement = document.createElement('div');
|
||||||
|
|
||||||
// Add the specified class name to the element
|
// Add the specified class name to the element
|
||||||
messageElement.className = className;
|
messageElement.className = className;
|
||||||
|
// Use the markdown-it parser
|
||||||
// Set the text content of the element to the message text
|
const html = parser.render(text);
|
||||||
messageElement.textContent = text;
|
messageElement.innerHTML = html;
|
||||||
|
// Append the message element to the chatbox immediately
|
||||||
// Append the message element to the chatbox
|
|
||||||
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
|
||||||
|
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);
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -4,25 +4,51 @@
|
|||||||
<!-- <title>Frontend</title> -->
|
<!-- <title>Frontend</title> -->
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
|
|
||||||
<!-- This iframe will hold the content from client.html -->
|
<!-- This iframe will hold the content from client.html -->
|
||||||
<iframe id="client-frame" style="width: 100%; height: 100vh;" srcdoc="{{ client_content }}"></iframe>
|
<!-- 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 -->
|
<!-- Responsive scaling and some padding -->
|
||||||
<script>
|
<script>
|
||||||
const clientFrame = document.getElementById('client-frame');
|
const clientFrame = document.getElementById('client-frame');
|
||||||
|
|
||||||
function resizeIframe() {
|
function resizeIframe() {
|
||||||
clientFrame.style.height = window.innerHeight - 50 + 'px'; // Adjust the subtraction for padding/margins if needed
|
clientFrame.style.height = window.innerHeight - 50 + 'px'; // Adjust the subtraction for padding/margins if needed
|
||||||
}
|
}
|
||||||
|
|
||||||
window.addEventListener('resize', resizeIframe);
|
window.addEventListener('resize', resizeIframe);
|
||||||
resizeIframe(); // Call it once on page load
|
resizeIframe(); // Call it once on page load
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script>
|
|
||||||
const apiEndpoint = '<%= api_endpoint %>'; // Templating syntax (Jinja2)
|
|
||||||
console.log("API Endpoint:", apiEndpoint);
|
|
||||||
// Use apiEndpoint in your frontend code...
|
|
||||||
</script>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
+286
-25
@@ -2,36 +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
|
||||||
global log_level # Remember, in Python globals are only global in the module it is defined in
|
import json
|
||||||
log_level = 'INFO' # Default logging level if not specified in config file
|
import requests
|
||||||
|
from typing import Optional
|
||||||
|
from enums import LogLevel, LOG_LEVEL_MAPPING
|
||||||
|
|
||||||
def configure_logging(level=log_level):
|
class GlobalState:
|
||||||
"""
|
"""
|
||||||
Set up logging for the project. This is the root logger instance.
|
This class holds various variables and methods which are accessible across
|
||||||
All child loggers inherit from this logger.
|
different modules in the Python project using the Singleton design pattern.
|
||||||
|
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.
|
||||||
"""
|
"""
|
||||||
numeric_level = getattr(logging, level.upper()) # Convert string to numeric level
|
_instance = None # Private class attribute to hold the single instance of the class
|
||||||
|
|
||||||
logger = logging.getLogger() # Get the root logger
|
def __new__(cls) -> 'GlobalState':
|
||||||
logger.setLevel(numeric_level)
|
"""
|
||||||
|
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
|
||||||
|
|
||||||
handler = logging.StreamHandler() # Or other handler (FileHandler for logs to file)
|
return cls._instance
|
||||||
formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s")
|
|
||||||
handler.setFormatter(formatter)
|
|
||||||
logger.addHandler(handler)
|
|
||||||
|
|
||||||
# To be removed?
|
|
||||||
# def set_local_logger(log_instance):
|
def configure_logging(self, level: Optional[LogLevel] = None) -> None:
|
||||||
# """
|
"""
|
||||||
# Configure logging based on the global variable log_level
|
Configure the logging system for this project.
|
||||||
# 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.
|
Args:
|
||||||
# """
|
level (LogLevel): The log level to use. If None, uses the default log level set in `self.log_level`.
|
||||||
# global log_level
|
|
||||||
# numeric_log_level = getattr(logging, log_level, None)
|
Notes:
|
||||||
# if not isinstance(numeric_log_level, int):
|
This method sets up logging for the project and logs a message at the debug level indicating the effective log level.
|
||||||
# raise ValueError('Invalid log level: %s' % log_level)
|
"""
|
||||||
|
if level == None:
|
||||||
|
level = self.log_level
|
||||||
|
if isinstance(level, LogLevel):
|
||||||
|
logging.info(f"Trying to set up logging with level {level}")
|
||||||
|
numeric_level = LOG_LEVEL_MAPPING[level]
|
||||||
|
if numeric_level is None:
|
||||||
|
raise ValueError("Invalid log level")
|
||||||
|
self.logger.setLevel(numeric_level)
|
||||||
|
self.logger.debug(f"utils.py -- configure_logging(): effective log level is {level} which is {self.logger.getEffectiveLevel()}")
|
||||||
|
|
||||||
|
|
||||||
|
def set_log_level(self, level: LogLevel) -> None:
|
||||||
|
"""
|
||||||
|
Set the log level for this project.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
level (LogLevel): The new log level to use. Can be one of the evels defined in enum.py (e.g., DEBUG, INFO, WARNING, CRITICAL etc.).
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
This method updates the `self.log_level` attribute and calls `configure_logging()` to apply the change.
|
||||||
|
"""
|
||||||
|
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
|
||||||
|
|
||||||
# log_instance.basicConfig(level=numeric_log_level) # Set the root logger level to the configured level
|
def get_effective_log_level(self) -> int:
|
||||||
# log_instance.info('Current log level set to {}'.format(log_instance.getLogger().getEffectiveLevel())) # Example usage of the logger
|
"""
|
||||||
|
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
|
||||||
|
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
import os
|
||||||
|
import requests
|
||||||
|
from bs4 import BeautifulSoup
|
||||||
|
from urllib.parse import urljoin, urlparse
|
||||||
|
import base64
|
||||||
|
import re
|
||||||
|
|
||||||
|
def download_image(url, folder_path):
|
||||||
|
if not os.path.isdir(folder_path):
|
||||||
|
os.makedirs(folder_path)
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = requests.get(url, stream=True)
|
||||||
|
response.raise_for_status() # Kontrollera om förfrågan lyckades
|
||||||
|
except requests.RequestException as e:
|
||||||
|
print(f"Failed to retrieve image {url}: {e}")
|
||||||
|
return
|
||||||
|
|
||||||
|
filename = os.path.join(folder_path, os.path.basename(urlparse(url).path))
|
||||||
|
with open(filename, 'wb') as file:
|
||||||
|
for chunk in response.iter_content(1024):
|
||||||
|
file.write(chunk)
|
||||||
|
print(f"Downloaded: {filename}")
|
||||||
|
|
||||||
|
def save_base64_image(data_url, folder_path, count):
|
||||||
|
if not os.path.isdir(folder_path):
|
||||||
|
os.makedirs(folder_path)
|
||||||
|
|
||||||
|
match = re.match(r'data:image/(?P<ext>[^;]+);base64,(?P<data>.+)', data_url)
|
||||||
|
if match:
|
||||||
|
ext = match.group('ext')
|
||||||
|
data = match.group('data')
|
||||||
|
img_data = base64.b64decode(data)
|
||||||
|
filename = os.path.join(folder_path, f'image_{count}.{ext}')
|
||||||
|
with open(filename, 'wb') as file:
|
||||||
|
file.write(img_data)
|
||||||
|
print(f"Downloaded: {filename}")
|
||||||
|
else:
|
||||||
|
print(f"Invalid base64 image data: {data_url}")
|
||||||
|
|
||||||
|
def download_all_images(html_content, base_url, folder_path):
|
||||||
|
soup = BeautifulSoup(html_content, 'html.parser')
|
||||||
|
img_tags = soup.find_all('img')
|
||||||
|
|
||||||
|
count = 0
|
||||||
|
for img in img_tags:
|
||||||
|
img_url = img.get('src')
|
||||||
|
if not img_url:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if img_url.startswith(('http://', 'https://')):
|
||||||
|
img_url = urljoin(base_url, img_url)
|
||||||
|
print(f"Attempting to download image: {img_url}")
|
||||||
|
download_image(img_url, folder_path)
|
||||||
|
elif img_url.startswith('data:image/'):
|
||||||
|
print(f"Attempting to save base64 image: {img_url[:30]}...") # Print only the start of the data URL
|
||||||
|
count += 1
|
||||||
|
save_base64_image(img_url, folder_path, count)
|
||||||
|
else:
|
||||||
|
print(f"Ignoring non-http URL: {img_url}")
|
||||||
|
|
||||||
|
def main():
|
||||||
|
url = input("Enter the URL of the webpage: ")
|
||||||
|
folder_path = os.path.expanduser("~/Downloads/downloaded_images")
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = requests.get(url)
|
||||||
|
response.raise_for_status() # Kontrollera om förfrågan lyckades
|
||||||
|
except requests.RequestException as e:
|
||||||
|
print(f"Failed to retrieve webpage {url}: {e}")
|
||||||
|
return
|
||||||
|
|
||||||
|
download_all_images(response.content, url, folder_path)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
import os
|
||||||
|
import re
|
||||||
|
from bs4 import BeautifulSoup
|
||||||
|
import requests
|
||||||
|
from urllib.parse import urljoin
|
||||||
|
import base64
|
||||||
|
|
||||||
|
def ladda_ner_bilder(url):
|
||||||
|
# Hämta HTML-sidan
|
||||||
|
svar = requests.get(url)
|
||||||
|
soup = BeautifulSoup(svar.text, 'html.parser')
|
||||||
|
|
||||||
|
# Hitta alla bilder
|
||||||
|
bilder = []
|
||||||
|
for img in soup.find_all('img'):
|
||||||
|
src = img.get('src')
|
||||||
|
if src:
|
||||||
|
bilder.append(src)
|
||||||
|
|
||||||
|
# Hantera inline-bilder i base64
|
||||||
|
INLINE_BILD_MÖNSTER = r'data:image/(.*?);base64,(.*)'
|
||||||
|
matcher = re.compile(INLINE_BILD_MÖNSTER)
|
||||||
|
for match in matcher.finditer(svar.text):
|
||||||
|
bild_typ = match.group(1)
|
||||||
|
bild_data = match.group(2)
|
||||||
|
bilder.append(f"data:{bild_typ};base64,{bild_data}")
|
||||||
|
|
||||||
|
# Ladda ner bilderna
|
||||||
|
bild_katalog = os.path.expanduser("~/Downloads/bilder")
|
||||||
|
if not os.path.exists(bild_katalog):
|
||||||
|
os.makedirs(bild_katalog)
|
||||||
|
|
||||||
|
for bild_url in bilder:
|
||||||
|
if not bild_url.startswith('http'):
|
||||||
|
bild_url = urljoin(url, bild_url)
|
||||||
|
|
||||||
|
if bild_url.startswith('data:'):
|
||||||
|
# Dekodera base64-strängen och spara den som en bild
|
||||||
|
format, data = bild_url.split(';base64,')
|
||||||
|
data = base64.b64decode(data)
|
||||||
|
filnamn = 'inline_' + str(len(bilder)) + '.gif'
|
||||||
|
with open(os.path.join(bild_katalog, filnamn), 'wb') as f:
|
||||||
|
f.write(data)
|
||||||
|
else:
|
||||||
|
svar = requests.get(bild_url)
|
||||||
|
if svar.status_code == 200:
|
||||||
|
filnamn = os.path.basename(bild_url).split('?')[0]
|
||||||
|
with open(os.path.join(bild_katalog, filnamn), 'wb') as f:
|
||||||
|
f.write(svar.content)
|
||||||
|
print(f"Bilden {filnamn} har laddats ner till {bild_katalog}.")
|
||||||
|
|
||||||
|
def main():
|
||||||
|
url = input("Ange URL till sidan från vilken du vill hämta bilder: ")
|
||||||
|
if not url.startswith('http'):
|
||||||
|
url = 'http://' + url
|
||||||
|
try:
|
||||||
|
ladda_ner_bilder(url)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Fel inträffade: {e}")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
BeautifulSoup4
|
||||||
|
requests
|
||||||
Reference in New Issue
Block a user