57 Commits

Author SHA1 Message Date
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
10 changed files with 663 additions and 228 deletions
+7 -4
View File
@@ -1,24 +1,27 @@
# Frontend Configuration # Frontend Configuration
frontend: frontend:
url: "http://localhost:8000" url: "http://localhost:5004"
# Backend Configuration # Backend Configuration
backend: backend:
url: "http://localhost:5005" url: "http://localhost:5004"
api: "/api/chat" api: "/api/chat"
# Ollama Server Configuration # Ollama Server Configuration
ollama: ollama:
url: "http://localhost:11434" url: "http://localhost:11434"
api_key: "${OLLAMA_API_KEY}" # Refer to environment variable api_key: "${OLLAMA_API_KEY}" # Refer to environment variable
# model: "phi3:mini" # Select a model supported by the Ollama server model: "phi3:mini" # Select a model supported by the Ollama server
# model: "llama3:70b" # Select a model supported by the Ollama server # model: "llama3:70b" # Select a model supported by the Ollama server
# model: "llama3:latest" # Select a model supported by the Ollama server # model: "llama3:latest" # Select a model supported by the Ollama server
model: "mannix/llama3-8b-ablitered-v3:latest" # Select a model supported by the Ollama server # model: "mannix/llama3-8b-ablitered-v3:latest" # Select a model supported by the Ollama server
# model: "mistral-nemo:latest" # Select a model supported by the Ollama server
# model: "gemma2:27b"
# 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)
+110 -39
View File
@@ -1,28 +1,93 @@
# 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
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
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.getLogger(__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
# 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():
"""
This route serves index.html to connecting clients
"""
session['chat_history'] = [] # The session object (actually, a dictonary) holds the chat session
logger.debug("Entering route '/'")
# api_endpoint = os.environ['BE_API_ENDPOINT'] # Retrieve the environment variable
api_endpoint = global_state.get_backend_api_ep() # Retrieve the environment variable
logger.debug("Backend API endpoint: %s", api_endpoint)
use_model = global_state.get_llm()
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):
return send_from_directory(app.config['STATIC_FOLDER'], filename)
# CORS(app, resources={
# r"/api/chat": {
# "origins": "*",
# "headers": ["Origin", "Content-Type", "Authorization"],
# }
# })
CORS(app, resources={ CORS(app, resources={
r"/api/chat": { r"/api/chat": {
"origins": "*", "origins": "*"
"headers": ["Origin", "Content-Type", "Authorization"],
} }
}) })
@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(url_server = "http://localhost:11434/api/generate", model = "phi3:mini"):
""" """
@@ -34,31 +99,38 @@ def chat(url_server = "http://localhost:11434/api/generate", model = "phi3:mini"
# 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', url_server) # Use provided URL or default
model = data.get('model', model) # Use provided model or default
# 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
}
# print(f"data = {data}\nmessage = {message}")
logger.debug("data = %s\nmessage = %s", str(data), str(message))
try: try:
# Alternative LLM: "model": "mannix/llama3-8b-ablitered-v3:latest",
url = url_server url = url_server
model_to_use = model
data = {
"model": model_to_use,
'prompt': message,
"stream": False
}
headers = { headers = {
"Content-Type": "application/json", "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))
@@ -68,6 +140,7 @@ 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('/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 +155,25 @@ 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 run_flask(fport=5005):
# Starts the Flask server """
# """ Starts the Flask server
# # Flask endpoint for user interaction """
# logger.debug("Entering run_flask()") # Flask endpoint for user interaction
# # app.run(port = str(str(fport)), debug=False) logger.debug("Entering run_flask()")
# app.run(port = str(str(fport)), debug=True) # app.run(port = str(str(fport)), debug=False)
# # app.run(port=5000, debug=True, use_reloader=False) app.run(port = str(str(fport)), debug=True)
# logger.debug("Exiting run_flask()") # 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>
-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);
}
+88
View File
@@ -0,0 +1,88 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<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 id="chatbox">
<!-- messages will be rendered here -->
</div>
<textarea id="userInput" placeholder="Type your message..." rows="5"></textarea>
<button id="sendButton" onclick="sendMessage()">Send</button>
<!-- Get the apiEndpoint and the useModel -->
<!-- <script>
const apiEndpoint = window.apiEndpoint;
const useModel = window.useModel;
console.log("client.html - API Endpoint: ", apiEndpoint);
console.log("client.html - use model: ", useModel);
</script> -->
<!-- <script>
let apiEndpoint; // Make variable available outside of the scope of the event listener
let useModel; // Make variable available outside of the scope of the event listener
window.addEventListener('message', function(event) {
if (event.origin === 'http://localhost:5004') { // Make sure this matches your origin
const { apiEndpoint, useModel } = event.data;
console.log("client.html - API Endpoint: ", apiEndpoint);
console.log("client.html - use model: ", useModel);
window.apiEndpoint = apiEndpoint;
window.useModel = useModel;
}
});
</script> -->
<!-- Marked-it for markdown rendering -->
<script src="https://cdn.jsdelivr.net/npm/markdown-it@14.1.0/dist/markdown-it.min.js"></script>
<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>
+57 -52
View File
@@ -3,39 +3,24 @@ 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 utils import utils
from utils import set_local_logger, run_flask from utils import GlobalState
# global log_level global_state = GlobalState() # Configure root logger. The level will be adjusted later based on config file
logger = global_state.getLogger(__name__) # Logger for this module, inherit properties of the root logger
# logger = 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)
@@ -57,42 +42,62 @@ def configure():
# Update the config dictionary with resolved environment variables # 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'])
logger.debug("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 and export backend API
# endpoint as global state variable
####################################
if isinstance(updated_config.get('backend'), dict): # Look for 'backend' key
if isinstance(updated_config['backend'].get('url'), str): # Look for 'url' key
url = updated_config['backend'].get('url')
if isinstance(updated_config['backend'].get('api'), str): # Look for 'api' key
api = updated_config['backend'].get('api')
# backend_api_ep = url+api # Extract API endpoint if defined
logger.debug(f"Constructing endpoint address as url+api: {url+api}")
global_state.set_backend_api_ep(url+api) # Extract API endpoint if defined and set in global_state
logger.debug(f"Backend API endpoint is set to {global_state.get_backend_api_ep()}")
# os.environ['BE_API_ENDPOINT'] = backend_api_ep # Look into alternative way to share this with backend.py
####################################
# Extract Ollama parameters (url, api_key, model)
####################################
if isinstance(updated_config.get('ollama'), dict): # Look for 'ollama' key
if isinstance(updated_config['ollama'].get('model'), str): # Look for 'model' key
model_to_use = updated_config['ollama'].get('model')
global_state.set_llm(model_to_use)
logger.debug("configure(): LLM is set to: %s",global_state.get_llm())
return updated_config return updated_config
def start_frontend(config): # def start_frontend(config):
parsed_url = urllib.parse.urlparse(config['frontend']['url']) # 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 # 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
# Use the socket module in Python to check whether a port is in use, # # 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. # # which would indicate that a server is already running on that port.
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: # with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
try: # try:
s.bind((hostname, port)) # s.bind((hostname, port))
utils.logger.info("No server is running on %s -— starting one.", parsed_url.netloc) # logger.debug("No server is running on %s -— starting one.", parsed_url.netloc)
# Start frontend (web server) as a separate process # # Start frontend (web server) as a separate process
subprocess.Popen(["python", "-m", "http.server", str(port)]) # subprocess.Popen(["python", "-m", "http.server", str(port)])
except socket.error as e: # except socket.error as e:
if e.errno == 48: # if e.errno == 48:
utils.logger.error("A server is already running on %s -— will use this.", parsed_url.netloc) # logger.debug("A server is already running on %s -— will use this.", parsed_url.netloc)
else: # else:
raise # Unexpected error, re-raise it so we can see the traceback # raise # Unexpected error, re-raise it so we can see the traceback
except Exception as e: # except Exception as e:
utils.logger.error("Failed to start frontend: %s", str(e)) # Corresponds to print(f"Failed to start frontend: {e}") # logger.error("Failed to start frontend: %s", str(e)) # Corresponds to print(f"Failed to start frontend: {e}")
@@ -100,17 +105,17 @@ 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 {}'.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)
@@ -0,0 +1,85 @@
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 */
}
.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="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="sendMessage()"]:hover {
background-color: #b2b2b2; /* Light Grey on hover */
}
button[onclick="sendMessage()"]:active {
background-color: #6f6f6f; /* Dark Grey when clicked */
}
+192
View File
@@ -0,0 +1,192 @@
// // Get the user input element from the DOM
// const chatbox = document.getElementById('chatbox');
// const userInput = document.getElementById('userInput');
// const parser = window.markdownit({
// linkify: true,
// strikethrough: true,
// });
// parser.enable(['table']);
// // const apiEndpoint = window.apiEndpoint; // Get the API endpoint
// // const useModel = window.useModel; // Get whether to use a model or not
// // console.log("frontend.js - API Endpoint: ", window.apiEndpoint);
// // console.log("frontend.js - Use model: ", window.useModel);
// let apiEndpoint; // Make variable available outside of the scope of the event listener
// let useModel; // Make variable available outside of the scope of the event listener
// window.addEventListener('message', function(event) {
// if (event.origin === 'http://localhost:5004') { // Make sure this matches your origin
// const { apiEndpoint, useModel } = event.data;
// console.log("client.html - API Endpoint: ", apiEndpoint);
// console.log("client.html - use model: ", useModel);
// window.apiEndpoint = apiEndpoint;
// window.useModel = useModel;
// }
// });
// console.log("frontend.js - API Endpoint: ", window.apiEndpoint);
// console.log("frontend.js - Use model: ", window.useModel);
// // Define a function to send the user's message to the AI
// function sendMessage() {
// // Get the user's input message and trim any whitespace
// const query = userInput.value.trim();
// // Check if the message is not empty
// if (query !== '') {
// // fetch(`${apiEndpoint}`, {
// fetch(apiEndpoint, {
// method: 'POST',
// headers: { 'Content-Type': 'application/json' },
// body: JSON.stringify({ query, model: useModel }), // Add these parameters here
// // body: JSON.stringify({ query, url_server: "http://your-custom-url", model: "phi3:mini" }), // Add these parameters here
// })
// .then(response => response.json())
// .then(data => {
// // Get the AI's response from the API data
// const aiResponse = data.response;
// // Render the user's original message in the chatbox
// renderMessage(query, 'user-message');
// // Render the AI's response in the chatbox
// renderMessage(aiResponse, 'ai-response');
// // Clear the user input field for the next message
// userInput.value = '';
// })
// .catch(error => console.error('Error sending message:', error));
// }
// }
// // Define a function to render a message in the chatbox with a specific class name
// function renderMessage(text, className) {
// // Create a new div element to hold the message
// const messageElement = document.createElement('div');
// // Add the specified class name to the element
// messageElement.className = className;
// // // Set the text content of the element to the message text
// // messageElement.textContent = text;
// // Use the markdown-it parser
// const html = parser.render(text);
// messageElement.innerHTML = html;
// // Append the message element to the chatbox immediately
// // chatbox.appendChild(messageElement);
// // Typeset math in the message element
// MathJax.typesetPromise([messageElement]).then(() => {
// // No need to append anything here, it's already appended above
// chatbox.appendChild(messageElement);
// });
// }
// // Make the button toggle colour when user presses Enter on keyboard
// const sendButton = document.getElementById('sendButton');
// document.addEventListener('keydown', function(event) {
// if (event.key === 'Enter') {
// sendButton.style.backgroundColor = '#6f6f6f'; // Dark Grey when Enter is pressed
// }
// });
// document.addEventListener('keyup', function() {
// sendButton.style.backgroundColor = ''; // Restore the original style when any key is released
// });
// Get the user input element from the DOM
const chatbox = document.getElementById('chatbox');
const userInput = document.getElementById('userInput');
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, 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);
},
};
// 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
});
+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>
+65 -25
View File
@@ -3,33 +3,73 @@
# 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 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):
log_instance.info('Log level set to {}'.format(log_level)) # Example usage of the logger 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.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
return cls._instance
def configure_logging(self, level=None):
"""Set up logging for the project."""
if level is None:
level = self.log_level
# numeric_level = getattr(logging, level.upper()) # Convert string to numeric level
numeric_level = getattr(logging, level.upper()) # Convert string to numeric level
self.logger.setLevel(numeric_level)
self.logger.debug(f"utils.py -- configure_logging(): effective log level is {level} which is {self.logger.getEffectiveLevel()}")
def set_log_level(self, level = 'INFO'):
"""Set the logging level."""
self.log_level = level
self.configure_logging()
def get_log_level(self):
"""Getter for log_level attribute."""
return self.log_level
def get_effective_log_level(self):
"""Getter for effective log level of loggerattribute."""
return self.logger.getEffectiveLevel()
def getLogger(self, module_name = None):
"""Return a logger based on the module name."""
if module_name is None:
module_name = __name__
logger = logging.getLogger(module_name)
return logger
def set_llm(self, model_name="phi3:mini"):
"""Set LLM for queries"""
self.llm = model_name
def get_llm(self):
"""Getter for which LLM is used for queries"""
return self.llm
def set_backend_api_ep(self, be_api_ep=None):
"""Set backend API endpoint"""
self.backend_api_ep = be_api_ep
def get_backend_api_ep(self):
"""Getter for backend API endpoint"""
return self.backend_api_ep
def run_flask(app, 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()")