diff --git a/Readme.md b/Readme.md index 8d4f811b..e67b200f 100644 --- a/Readme.md +++ b/Readme.md @@ -12,6 +12,7 @@ - [Demo](#Demo) - [Description](#Description) - [Analysis](#Analysis) + - [Interfaces](#Interfaces) - [Architecture](#Architecture) - [Setup](#Setup) - [Install](#1-Install) @@ -57,7 +58,11 @@ - The results do not have any words used in the query - *Based on the top result it seems the re-ranking model understands that Emacs is an editor?* - The results incrementally update as the query is entered -- The results are re-ranked, for better accuracy, once user is idle +- The results are re-ranked, for better accuracy, once user hits enter + +### Interfaces + +![](https://github.com/debanjum/khoj/blob/master/docs/interfaces.png) ## Architecture @@ -65,30 +70,31 @@ ## Setup ### 1. Install - ``` shell - pip install khoj-assistant - ``` + ``` shell + pip install khoj-assistant + ``` -### 2. Configure - - Set `input-files` or `input-filter` in each relevant `content-type` section of [khoj_sample.yml](./config/khoj_sample.yml) - - Set `input-directories` field in `content-type.image` section - - Delete `content-type`, `processor` sub-sections irrelevant for your use-case +### 2. Start App + ``` shell + khoj + ``` -### 3. Run - ``` shell - khoj -c=config/khoj_sample.yml -vv - ``` - Loads ML model, generates embeddings and exposes API to search notes, images, transactions etc specified in config YAML +### 3. Configure + + 1. Enable content types and point to files to search in the First Run Screen that pops up on app start* + 2. Click configure* and wait. The app will load ML model, generates embeddings and exposes the search API + + ![](https://github.com/debanjum/khoj/blob/master/docs/desktop_interface.png) ## Use - **Khoj via Web** - - Open + - Open via desktop interface or directly - **Khoj via Emacs** - [Install](https://github.com/debanjum/khoj/tree/master/src/interface/emacs#installation) [khoj.el](./src/interface/emacs/khoj.el) - Run `M-x khoj ` - **Khoj via API** - - See [Khoj FastAPI Docs](http://localhost:8000/docs), [Khoj FastAPI ReDocs](http://localhost:8000/redocs) + - See the FastAPI [Swagger Docs](http://localhost:8000/docs), [ReDocs](http://localhost:8000/redocs) ## Upgrade ``` shell @@ -98,7 +104,7 @@ pip install --upgrade khoj-assistant ## Troubleshoot - Symptom: Errors out complaining about Tensors mismatch, null etc - - Mitigation: Delete `content-type` > `image` section from `khoj_sample.yml` + - Mitigation: Disable `image` section on the desktop GUI - Symptom: Errors out with \"Killed\" in error message in Docker - Fix: Increase RAM available to Docker Containers in Docker Settings @@ -108,7 +114,7 @@ pip install --upgrade khoj-assistant - The experimental [chat](localhost:8000/chat) API endpoint uses the [OpenAI API](https://openai.com/api/) - It is disabled by default - - To use it add your `openai-api-key` to config.yml + - To use it add your `openai-api-key` via the app configure screen ## Performance @@ -140,13 +146,14 @@ pip install --upgrade khoj-assistant pip install -e . ``` ##### 2. Configure - - Set `input-files` or `input-filter` in each relevant `content-type` section of `khoj_sample.yml` + - Copy the `config/khoj_sample.yml` to `~/.khoj/khoj.yml` + - Set `input-files` or `input-filter` in each relevant `content-type` section of `~/.khoj/khoj.yml` - Set `input-directories` field in `image` `content-type` section - - Delete `content-type`, `processor` sub-sections irrelevant for your use-case + - Delete `content-type` and `processor` sub-section(s) irrelevant for your use-case ##### 3. Run ``` shell - khoj -c=config/khoj_sample.yml -vv + khoj -vv ``` Load ML model, generate embeddings and expose API to query notes, images, transactions etc specified in config YAML @@ -209,13 +216,14 @@ docker-compose build --pull ``` ##### 3. Configure - - Set `input-files` or `input-filter` in each relevant `content-type` section of `khoj_sample.yml` + - Copy the `config/khoj_sample.yml` to `~/.khoj/khoj.yml` + - Set `input-files` or `input-filter` in each relevant `content-type` section of `~/.khoj/khoj.yml` - Set `input-directories` field in `image` `content-type` section - Delete `content-type`, `processor` sub-sections irrelevant for your use-case ##### 4. Run ``` shell - python3 -m src.main config/khoj_sample.yml -vv + python3 -m src.main -vv ``` Load ML model, generate embeddings and expose API to query notes, images, transactions etc specified in config YAML diff --git a/docs/interfaces.png b/docs/interfaces.png new file mode 100644 index 00000000..eb585ddd Binary files /dev/null and b/docs/interfaces.png differ diff --git a/src/interface/desktop/main_window.py b/src/interface/desktop/main_window.py index c67f1c92..fbe0fa00 100644 --- a/src/interface/desktop/main_window.py +++ b/src/interface/desktop/main_window.py @@ -1,4 +1,5 @@ # Standard Packages +from enum import Enum from pathlib import Path from copy import deepcopy import webbrowser @@ -29,6 +30,11 @@ class MainWindow(QtWidgets.QMainWindow): def __init__(self, config_file: Path): super(MainWindow, self).__init__() self.config_file = config_file + # Set regenerate flag to regenerate embeddings everytime user clicks configure + if state.cli_args: + state.cli_args += ['--regenerate'] + else: + state.cli_args = ['--regenerate'] # Load config from existing config, if exists, else load from default config if resolve_absolute_path(self.config_file).exists(): @@ -40,7 +46,6 @@ class MainWindow(QtWidgets.QMainWindow): self.new_config = self.current_config # Initialize Configure Window - self.setWindowFlags(Qt.WindowType.WindowStaysOnTopHint) self.setWindowTitle("Khoj") self.setFixedWidth(600) @@ -129,7 +134,7 @@ class MainWindow(QtWidgets.QMainWindow): action_bar_layout = QtWidgets.QHBoxLayout(action_bar) self.configure_button = QtWidgets.QPushButton("Configure", clicked=self.configure_app) - self.search_button = QtWidgets.QPushButton("Search", clicked=lambda: webbrowser.open('http://localhost:8000/')) + self.search_button = QtWidgets.QPushButton("Search", clicked=lambda: webbrowser.open(f'http://{state.host}:{state.port}/')) self.search_button.setEnabled(not self.first_run) action_bar_layout.addWidget(self.configure_button) @@ -148,11 +153,21 @@ class MainWindow(QtWidgets.QMainWindow): def add_error_message(self, message: str): "Add Error Message to Configure Screen" - error_message = QtWidgets.QLabel() - error_message.setWordWrap(True) - error_message.setText(message) - error_message.setStyleSheet("color: red") - self.layout.addWidget(error_message) + # Remove any existing error messages + for message_prefix in ErrorType: + for i in reversed(range(self.layout.count())): + current_widget = self.layout.itemAt(i).widget() + if isinstance(current_widget, QtWidgets.QLabel) and current_widget.text().startswith(message_prefix.value): + self.layout.removeWidget(current_widget) + current_widget.deleteLater() + + # Add new error message + if message: + error_message = QtWidgets.QLabel() + error_message.setWordWrap(True) + error_message.setText(message) + error_message.setStyleSheet("color: red") + self.layout.addWidget(error_message) def update_search_settings(self): "Update config with search settings from UI" @@ -192,22 +207,17 @@ class MainWindow(QtWidgets.QMainWindow): self.new_config['processor'][child.processor_type.value]['openai-api-key'] = child.input_field.toPlainText() if child.input_field.toPlainText() != '' else None def save_settings_to_file(self) -> bool: + "Save validated settings to file" # Validate config before writing to file try: yaml_utils.parse_config_from_string(self.new_config) except Exception as e: print(f"Error validating config: {e}") - self.add_error_message(f"Error validating config: {e}") + self.add_error_message(f"{ErrorType.ConfigValidationError.value}: {e}") return False - else: - # Remove error message if present - for i in range(self.layout.count()): - current_widget = self.layout.itemAt(i).widget() - if isinstance(current_widget, QtWidgets.QLabel) and current_widget.text().startswith("Error validating config:"): - self.layout.removeWidget(current_widget) - current_widget.deleteLater() # Save the config to app config file + self.add_error_message(None) yaml_utils.save_config_to_file(self.new_config, self.config_file) return True @@ -234,6 +244,7 @@ class MainWindow(QtWidgets.QMainWindow): self.thread.started.connect(self.settings_loader.run) self.settings_loader.finished.connect(self.thread.quit) self.settings_loader.finished.connect(self.settings_loader.deleteLater) + self.settings_loader.error.connect(self.add_error_message) self.thread.finished.connect(self.thread.deleteLater) # Start thread @@ -253,6 +264,7 @@ class MainWindow(QtWidgets.QMainWindow): class SettingsLoader(QObject): "Load Settings Thread" finished = pyqtSignal() + error = pyqtSignal(str) def __init__(self, load_settings_func): super(SettingsLoader, self).__init__() @@ -260,7 +272,12 @@ class SettingsLoader(QObject): def run(self): "Load Settings" - self.load_settings_func() + try: + self.load_settings_func() + except FileNotFoundError as e: + self.error.emit(f"{ErrorType.ConfigLoadingError.value}: {e}") + else: + self.error.emit(None) self.finished.emit() @@ -274,3 +291,8 @@ class ProcessorCheckBox(QtWidgets.QCheckBox): def __init__(self, text, processor_type: ProcessorType, parent=None): self.processor_type = processor_type super(ProcessorCheckBox, self).__init__(text, parent=parent) + +class ErrorType(Enum): + "Error Types" + ConfigLoadingError = "Config Loading Error" + ConfigValidationError = "Config Validation Error" diff --git a/src/interface/desktop/system_tray.py b/src/interface/desktop/system_tray.py index 39d257e8..c24a9911 100644 --- a/src/interface/desktop/system_tray.py +++ b/src/interface/desktop/system_tray.py @@ -5,7 +5,7 @@ import webbrowser from PyQt6 import QtGui, QtWidgets # Internal Packages -from src.utils import constants +from src.utils import constants, state def create_system_tray(gui: QtWidgets.QApplication, main_window: QtWidgets.QMainWindow): @@ -24,7 +24,7 @@ def create_system_tray(gui: QtWidgets.QApplication, main_window: QtWidgets.QMain # Create the menu and menu actions menu = QtWidgets.QMenu() menu_actions = [ - ('Search', lambda: webbrowser.open('http://localhost:8000/')), + ('Search', lambda: webbrowser.open(f'http://{state.host}:{state.port}/')), ('Configure', main_window.show), ('Quit', gui.quit), ] diff --git a/src/main.py b/src/main.py index 45c00e77..ef8f90ae 100644 --- a/src/main.py +++ b/src/main.py @@ -79,6 +79,8 @@ def set_state(args): state.config_file = args.config_file state.config = args.config state.verbose = args.verbose + state.host = args.host + state.port = args.port def start_server(app, host=None, port=None, socket=None): diff --git a/src/router.py b/src/router.py index 53cf7bd0..191f9362 100644 --- a/src/router.py +++ b/src/router.py @@ -81,7 +81,7 @@ def search(q: str, n: Optional[int] = 5, t: Optional[SearchType] = None, r: Opti results = text_search.collate_results(hits, entries, results_count) collate_end = time.time() - if (t == SearchType.Markdown or t == None) and state.model.orgmode_search: + if (t == SearchType.Markdown or t == None) and state.model.markdown_search: # query markdown files query_start = time.time() hits, entries = text_search.query(user_query, state.model.markdown_search, rank_results=r, device=state.device, filters=[ExplicitFilter(), DateFilter()], verbose=state.verbose) diff --git a/src/utils/state.py b/src/utils/state.py index 90f9296a..4f194331 100644 --- a/src/utils/state.py +++ b/src/utils/state.py @@ -13,4 +13,6 @@ processor_config = ProcessorConfigModel() config_file: Path = "" verbose: int = 0 device = torch.device("cuda:0") if torch.cuda.is_available() else torch.device("cpu") # Set device to GPU if available +host: str = None +port: int = None cli_args = None \ No newline at end of file