diff --git a/.gitignore b/.gitignore index 0dbf2f2..b302234 100644 --- a/.gitignore +++ b/.gitignore @@ -168,3 +168,4 @@ cython_debug/ # option (not recommended) you can uncomment the following to ignore the entire idea folder. #.idea/ +app_database/ \ No newline at end of file diff --git a/src/semeion/services/__init__.py b/src/semeion/services/__init__.py index 4716bca..caa7949 100644 --- a/src/semeion/services/__init__.py +++ b/src/semeion/services/__init__.py @@ -8,5 +8,6 @@ from semeion.services.llm_service import LLMService from semeion.services.qdrant_service import QdrantService +from semeion.services.localdb import DatabaseService, AppSettings -__all__ = ["LLMService", "QdrantService"] \ No newline at end of file +__all__ = ["LLMService", "QdrantService", "DatabaseService", "AppSettings"] \ No newline at end of file diff --git a/src/semeion/services/localdb.py b/src/semeion/services/localdb.py new file mode 100644 index 0000000..131bd54 --- /dev/null +++ b/src/semeion/services/localdb.py @@ -0,0 +1,84 @@ +# +# Copyright (c) 2025, mstoeck3 +# All rights reserved. +# +# This source code is licensed under the BSD-3-Clause license found in the +# LICENSE file in the root directory of this source tree. +# + +import sqlite3 +import os +from dotenv import dotenv_values +DB_DIR = "app_database" +DB_FILE = os.path.join(DB_DIR, "management_db.sqlite") + +class DatabaseService(): + def __init__(self) -> None: + print("DBSV: INITIALIZE DATABASE") + self.conn = self.connect_db() + + def connect_db(self) -> sqlite3.Connection: + os.makedirs(DB_DIR, exist_ok=True) + try: + conn = sqlite3.connect(DB_FILE) + print("DBSV: DATABASE AVAILABLE") + return conn + except sqlite3.Error as e: + raise + + def check_exist(self) -> bool: + if os.path.exists(DB_FILE): + print("DBSV: DATABASE EXISTS") + return True + else: + return False + +class AppSettings(DatabaseService): + def __init__(self)->None: + print("DBSV:APPSETTINGS: SETTINGS AVAILABLE") + self.conn = self.connect_db() + self.create_initial_configs() + + def check_exist(self): + try: + with self.conn as conn: + cur = conn.cursor() + statement = """ + SELECT key,val FROM settings; + """ + cur.execute(statement) + rows = cur.fetchall() + if len(rows)==6: # expecting 6 initial settings + print("DBSV:APPSETTINGS: SETTINGS EXIST") + return True + else: + return False + except sqlite3.DatabaseError as e: + print("DBSV: DB NOT FOUND. CREATING...") + return e + + def create_initial_configs(self)->None: + if self.check_exist()==True: + + return + else: + print("DBSV:APPSETTINGS: CREATING INITIAL SETTINGS") + initial_config =dotenv_values() + with self.conn as conn: + cur = conn.cursor() + statement = """ + CREATE TABLE IF NOT EXISTS settings ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + key text NOT NULL, + val text NOT NULL + ); + """ + cur.execute(statement) + for key,val in initial_config.items(): + task = (key,val) + statement = f""" + INSERT INTO settings(key,val) VALUES(?,?); + """ + cur.execute(statement, task) + conn.commit() + \ No newline at end of file diff --git a/src/semeion/services/workers.py b/src/semeion/services/workers.py deleted file mode 100644 index 1415396..0000000 --- a/src/semeion/services/workers.py +++ /dev/null @@ -1,49 +0,0 @@ -# -# Copyright (c) 2025, mstoeck3 -# All rights reserved. -# -# This source code is licensed under the BSD-3-Clause license found in the -# LICENSE file in the root directory of this source tree. -# - - - -class LLMQueryWorker(QThread): - query_finished = Signal(object) - - def __init__(self, llm_client, prompt: str): - super().__init__() - self.llm_client = llm_client - self.prompt = prompt - - def run(self): - response = self.llm_client.query(self.prompt) - self.query_finished.emit(response) - -class LLMQueryService(QObject): - query_finished = Signal(object) - - def __init__(self): - super().__init__() - print("SRVC:LLMQUERYSERVICE INIT") - self.llm_client = LLMInterface() - print("SRVC:LLMQUERYSERVICE TARGET") - self._worker = None - - @Slot(str) - def query_llm(self, prompt: str): - self._worker = LLMQueryWorker(self.llm_client, prompt) - self._worker.query_finished.connect(self.query_finished.emit) - self._worker.finished.connect(self._worker.deleteLater) - self._worker.start() - - -class QDrantService(QObject): - collections_discovered = Signal(dict) - - def __init__(self): - super().__init__() - self.qdrant_client = QdrantInterface() - - - \ No newline at end of file diff --git a/src/semeion/ui/main_window.py b/src/semeion/ui/main_window.py index 29ed6fa..cb82789 100644 --- a/src/semeion/ui/main_window.py +++ b/src/semeion/ui/main_window.py @@ -5,44 +5,53 @@ # This source code is licensed under the BSD-3-Clause license found in the # LICENSE file in the root directory of this source tree. # -from PySide6.QtWidgets import QWidget, QVBoxLayout, QLabel, QPushButton, QLineEdit, QToolBar -from PySide6.QtGui import QAction +from PySide6.QtWidgets import QMainWindow, QWidget, QVBoxLayout, QLabel, QPushButton, QLineEdit, QMenu, QHBoxLayout from PySide6.QtCore import Qt, Signal from . import styling -from semeion.services import LLMService, QdrantService +from semeion.services import LLMService, QdrantService, DatabaseService, AppSettings -class MainWindow(QWidget): +class MainWindow(QMainWindow): main_exec_start = Signal(str) def __init__(self): super().__init__() - print("MAIN: INITIALIZING GUI") - self.create_ui() + self.db = DatabaseService() + self.settings = AppSettings() print("MAIN: CREATING SERVICES") self.create_services() + print("MAIN: INITIALIZING GUI") + self.create_ui() + self.connect_ui_elements() + def create_ui(self): self.setWindowTitle("Semeion Interface") self.setGeometry(100, 100, 800, 600) self.setStyleSheet(styling.main_window_style) + central_widget = QWidget() + self.setCentralWidget(central_widget) + self.main_layout = QVBoxLayout() - self.setLayout(self.main_layout) + central_widget.setLayout(self.main_layout) # Toolbar - self.toolbar = QToolBar() - self.main_layout.addWidget(self.toolbar) - action_CaseManager = QAction("Case", self) - action_IngestionManager = QAction("Ingest Artifacts", self) - action_ConnManager = QAction("Server Connections", self) - action_SettingsMenu = QAction("Settings", self) - action_AboutMenu = QAction("About", self) - self.toolbar.addAction(action_CaseManager) - self.toolbar.addAction(action_IngestionManager) - self.toolbar.addAction(action_ConnManager) - self.toolbar.addAction(action_SettingsMenu) - self.toolbar.addAction(action_AboutMenu) + self.create_menuBar() + + self.conn_status_layout = QHBoxLayout() + conn_widget_qdrant = QLabel("Qdrant Interface available") + conn_widget_llm = QLabel("LLM Interface available") + conn_widget_embeddings = QLabel("EMB_PLACEHOLDER") + status_widgets = [] + status_widgets.append(conn_widget_qdrant) + status_widgets.append(conn_widget_llm) + status_widgets.append(conn_widget_embeddings) + for w in status_widgets: + w.setStyleSheet(styling.status_widget_style) + self.conn_status_layout.addWidget(w) + self.main_layout.addLayout(self.conn_status_layout) + # spacing self.main_layout.addStretch(1) @@ -58,7 +67,6 @@ class MainWindow(QWidget): self.searchInput.setFixedWidth(400) self.searchInput.setMinimumHeight(30) self.searchInput.setStyleSheet(styling.input_style) - self.searchInput.returnPressed.connect(self.execute_from_input) self.main_layout.addWidget(self.searchInput, alignment=Qt.AlignmentFlag.AlignCenter) self.main_layout.addSpacing(10) @@ -66,10 +74,24 @@ class MainWindow(QWidget): self.executeButton.setAttribute(Qt.WidgetAttribute.WA_Hover) self.executeButton.setStyleSheet(styling.button_style) self.main_layout.addWidget(self.executeButton, alignment=Qt.AlignmentFlag.AlignCenter) - self.executeButton.clicked.connect(self.execute_from_input) self.main_layout.addStretch(1) + def create_menuBar(self): + menuBar = self.menuBar() + action_caseManager = menuBar.addAction("&Case Manager", self.open_case_manager) + action_caseManager.setStatusTip("Manage Cases") + action_ingestionManager = menuBar.addAction("&Ingestion Manager", self.open_ingestion_manager) + action_ingestionManager.setStatusTip("Ingest various Artifacts into dataset") + menuBar_settingsMenu = QMenu("&Settings", self) + action_connectionManager = menuBar_settingsMenu.addAction("&Manage Server Connections", self.open_connection_manager) + menuBar.addMenu(menuBar_settingsMenu) + menuBar_settingsMenu.setStatusTip("Application Settings") + menuBar_about = QMenu("&About", self) + action_aboutModal = menuBar_about.addAction("&About...", self.open_about_window) + menuBar.addMenu(menuBar_about) + menuBar.setStyleSheet(styling.toolbar_style) + def create_services(self): self.llm_service = LLMService() self.qdrant_service = QdrantService() @@ -84,15 +106,40 @@ class MainWindow(QWidget): # Qdrant: execute collection discovery on startup self.qdrant_service.discover_collections() + def connect_ui_elements(self): + self.searchInput.returnPressed.connect(self.execute_from_input) + self.executeButton.clicked.connect(self.execute_from_input) + def execute_from_input(self) -> str|None: query = self.searchInput.text().strip() if not query: return self.main_exec_start.emit(query) + self.executeButton.setDisabled(True) + self.searchInput.setDisabled(True) + self.reenable_list = [self.executeButton, self.searchInput] print("MAIN: LLM QUERY SUBMIT:", query) def handle_llm_response(self, response): + self.enable_input(self.reenable_list) print("MAIN: LLM RESPONSE:", response) def handle_qdrant_response(self, response): - print("MAIN: QDRANT COLLECTIONS:", response) \ No newline at end of file + print("MAIN: QDRANT COLLECTIONS:", response) + + def enable_input(self, elements: list): + for e in elements: + e.setEnabled(True) + + def open_case_manager(self): + print("clicked case manager") + + def open_ingestion_manager(self): + print("clicked ingestion manager") + + def open_connection_manager(self): + print("clicked open connection manager") + + def open_about_window(self): + print("clicked open about") + \ No newline at end of file diff --git a/src/semeion/ui/styling/__init__.py b/src/semeion/ui/styling/__init__.py index 35c56d6..5e61986 100644 --- a/src/semeion/ui/styling/__init__.py +++ b/src/semeion/ui/styling/__init__.py @@ -6,6 +6,8 @@ # LICENSE file in the root directory of this source tree. # +## DISCLAIMER: Styles are mostly AI-generated + main_window_style = """ QWidget { background-color: #f0f0f0; @@ -39,8 +41,35 @@ QLineEdit { padding: 10px 20px; font-size: 16px; } - QLineEdit:focus { +QLineEdit:focus { border: 1px solid #aaa; outline: none; } +""" + +toolbar_style = """ +QMenuBar::item { + padding: 6px 12px; + border-radius: 4px; +} +QMenuBar::item:selected { + background-color: #e8e8e8; +} +QMenu { + border: 2px solid #c0c0c0; + border-radius: 6px; + padding: 6px 4px; +} +QMenu::item { + padding: 8px 24px 8px 12px; + border-radius: 4px; + margin: 2px; +} +QMenu::item:selected { + background-color: #e8e8e8; +} +""" + +status_widget_style = """ + """ \ No newline at end of file