Improve Desktop GUI and Documentation

- Improve Documentation
  - 7866add Add Interface Screenshots to Docs
  - 8ad3482 Update Readme Instructions to use Desktop GUI to configure App
- Fix Markdown Search Bug on Backend
  - b891347 Fix condition in router to trigger markdown search
- Improve Desktop GUI
  - 67ab40b Regenerate embeddings everytime user clicks configure in Desktop GUI
  - 7f479b0 Improve Displaying Error to User on Khoj window in Desktop GUI
  - 873bb9d Do not force the Khoj window to always be on top. It's needlessly annoying
  - 9bc4fd5 Set Web Interface URL from loaded state in Desktop GUIs. Not hard-coded
This commit is contained in:
Debanjum
2022-08-15 23:01:37 +00:00
committed by GitHub
7 changed files with 75 additions and 41 deletions

View File

@@ -12,6 +12,7 @@
- [Demo](#Demo) - [Demo](#Demo)
- [Description](#Description) - [Description](#Description)
- [Analysis](#Analysis) - [Analysis](#Analysis)
- [Interfaces](#Interfaces)
- [Architecture](#Architecture) - [Architecture](#Architecture)
- [Setup](#Setup) - [Setup](#Setup)
- [Install](#1-Install) - [Install](#1-Install)
@@ -57,7 +58,11 @@
- The results do not have any words used in the query - 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?* - *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 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 ## Architecture
@@ -69,26 +74,27 @@
pip install khoj-assistant pip install khoj-assistant
``` ```
### 2. Configure ### 2. Start App
- 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
### 3. Run
``` shell ``` shell
khoj -c=config/khoj_sample.yml -vv khoj
``` ```
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 ## Use
- **Khoj via Web** - **Khoj via Web**
- Open <http://localhost:8000/> - Open <http://localhost:8000/> via desktop interface or directly
- **Khoj via Emacs** - **Khoj via Emacs**
- [Install](https://github.com/debanjum/khoj/tree/master/src/interface/emacs#installation) [khoj.el](./src/interface/emacs/khoj.el) - [Install](https://github.com/debanjum/khoj/tree/master/src/interface/emacs#installation) [khoj.el](./src/interface/emacs/khoj.el)
- Run `M-x khoj <user-query>` - Run `M-x khoj <user-query>`
- **Khoj via API** - **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 ## Upgrade
``` shell ``` shell
@@ -98,7 +104,7 @@ pip install --upgrade khoj-assistant
## Troubleshoot ## Troubleshoot
- Symptom: Errors out complaining about Tensors mismatch, null etc - 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 - Symptom: Errors out with \"Killed\" in error message in Docker
- Fix: Increase RAM available to Docker Containers in Docker Settings - 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/) - The experimental [chat](localhost:8000/chat) API endpoint uses the [OpenAI API](https://openai.com/api/)
- It is disabled by default - 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 ## Performance
@@ -140,13 +146,14 @@ pip install --upgrade khoj-assistant
pip install -e . pip install -e .
``` ```
##### 2. Configure ##### 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 - 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 ##### 3. Run
``` shell ``` 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 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 ##### 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 - Set `input-directories` field in `image` `content-type` section
- Delete `content-type`, `processor` sub-sections irrelevant for your use-case - Delete `content-type`, `processor` sub-sections irrelevant for your use-case
##### 4. Run ##### 4. Run
``` shell ``` 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 Load ML model, generate embeddings and expose API to query notes, images, transactions etc specified in config YAML

BIN
docs/interfaces.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 606 KiB

View File

@@ -1,4 +1,5 @@
# Standard Packages # Standard Packages
from enum import Enum
from pathlib import Path from pathlib import Path
from copy import deepcopy from copy import deepcopy
import webbrowser import webbrowser
@@ -29,6 +30,11 @@ class MainWindow(QtWidgets.QMainWindow):
def __init__(self, config_file: Path): def __init__(self, config_file: Path):
super(MainWindow, self).__init__() super(MainWindow, self).__init__()
self.config_file = config_file 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 # Load config from existing config, if exists, else load from default config
if resolve_absolute_path(self.config_file).exists(): if resolve_absolute_path(self.config_file).exists():
@@ -40,7 +46,6 @@ class MainWindow(QtWidgets.QMainWindow):
self.new_config = self.current_config self.new_config = self.current_config
# Initialize Configure Window # Initialize Configure Window
self.setWindowFlags(Qt.WindowType.WindowStaysOnTopHint)
self.setWindowTitle("Khoj") self.setWindowTitle("Khoj")
self.setFixedWidth(600) self.setFixedWidth(600)
@@ -129,7 +134,7 @@ class MainWindow(QtWidgets.QMainWindow):
action_bar_layout = QtWidgets.QHBoxLayout(action_bar) action_bar_layout = QtWidgets.QHBoxLayout(action_bar)
self.configure_button = QtWidgets.QPushButton("Configure", clicked=self.configure_app) 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) self.search_button.setEnabled(not self.first_run)
action_bar_layout.addWidget(self.configure_button) action_bar_layout.addWidget(self.configure_button)
@@ -148,6 +153,16 @@ class MainWindow(QtWidgets.QMainWindow):
def add_error_message(self, message: str): def add_error_message(self, message: str):
"Add Error Message to Configure Screen" "Add Error Message to Configure Screen"
# 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 = QtWidgets.QLabel()
error_message.setWordWrap(True) error_message.setWordWrap(True)
error_message.setText(message) error_message.setText(message)
@@ -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 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: def save_settings_to_file(self) -> bool:
"Save validated settings to file"
# Validate config before writing to file # Validate config before writing to file
try: try:
yaml_utils.parse_config_from_string(self.new_config) yaml_utils.parse_config_from_string(self.new_config)
except Exception as e: except Exception as e:
print(f"Error validating config: {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 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 # Save the config to app config file
self.add_error_message(None)
yaml_utils.save_config_to_file(self.new_config, self.config_file) yaml_utils.save_config_to_file(self.new_config, self.config_file)
return True return True
@@ -234,6 +244,7 @@ class MainWindow(QtWidgets.QMainWindow):
self.thread.started.connect(self.settings_loader.run) self.thread.started.connect(self.settings_loader.run)
self.settings_loader.finished.connect(self.thread.quit) self.settings_loader.finished.connect(self.thread.quit)
self.settings_loader.finished.connect(self.settings_loader.deleteLater) 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) self.thread.finished.connect(self.thread.deleteLater)
# Start thread # Start thread
@@ -253,6 +264,7 @@ class MainWindow(QtWidgets.QMainWindow):
class SettingsLoader(QObject): class SettingsLoader(QObject):
"Load Settings Thread" "Load Settings Thread"
finished = pyqtSignal() finished = pyqtSignal()
error = pyqtSignal(str)
def __init__(self, load_settings_func): def __init__(self, load_settings_func):
super(SettingsLoader, self).__init__() super(SettingsLoader, self).__init__()
@@ -260,7 +272,12 @@ class SettingsLoader(QObject):
def run(self): def run(self):
"Load Settings" "Load Settings"
try:
self.load_settings_func() self.load_settings_func()
except FileNotFoundError as e:
self.error.emit(f"{ErrorType.ConfigLoadingError.value}: {e}")
else:
self.error.emit(None)
self.finished.emit() self.finished.emit()
@@ -274,3 +291,8 @@ class ProcessorCheckBox(QtWidgets.QCheckBox):
def __init__(self, text, processor_type: ProcessorType, parent=None): def __init__(self, text, processor_type: ProcessorType, parent=None):
self.processor_type = processor_type self.processor_type = processor_type
super(ProcessorCheckBox, self).__init__(text, parent=parent) super(ProcessorCheckBox, self).__init__(text, parent=parent)
class ErrorType(Enum):
"Error Types"
ConfigLoadingError = "Config Loading Error"
ConfigValidationError = "Config Validation Error"

View File

@@ -5,7 +5,7 @@ import webbrowser
from PyQt6 import QtGui, QtWidgets from PyQt6 import QtGui, QtWidgets
# Internal Packages # Internal Packages
from src.utils import constants from src.utils import constants, state
def create_system_tray(gui: QtWidgets.QApplication, main_window: QtWidgets.QMainWindow): 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 # Create the menu and menu actions
menu = QtWidgets.QMenu() menu = QtWidgets.QMenu()
menu_actions = [ menu_actions = [
('Search', lambda: webbrowser.open('http://localhost:8000/')), ('Search', lambda: webbrowser.open(f'http://{state.host}:{state.port}/')),
('Configure', main_window.show), ('Configure', main_window.show),
('Quit', gui.quit), ('Quit', gui.quit),
] ]

View File

@@ -79,6 +79,8 @@ def set_state(args):
state.config_file = args.config_file state.config_file = args.config_file
state.config = args.config state.config = args.config
state.verbose = args.verbose state.verbose = args.verbose
state.host = args.host
state.port = args.port
def start_server(app, host=None, port=None, socket=None): def start_server(app, host=None, port=None, socket=None):

View File

@@ -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) results = text_search.collate_results(hits, entries, results_count)
collate_end = time.time() 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 markdown files
query_start = time.time() 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) hits, entries = text_search.query(user_query, state.model.markdown_search, rank_results=r, device=state.device, filters=[ExplicitFilter(), DateFilter()], verbose=state.verbose)

View File

@@ -13,4 +13,6 @@ processor_config = ProcessorConfigModel()
config_file: Path = "" config_file: Path = ""
verbose: int = 0 verbose: int = 0
device = torch.device("cuda:0") if torch.cuda.is_available() else torch.device("cpu") # Set device to GPU if available 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 cli_args = None