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
commit 7fc8672666
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 75 additions and 41 deletions

View file

@ -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 <http://localhost:8000/>
- Open <http://localhost:8000/> 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 <user-query>`
- **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

BIN
docs/interfaces.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 606 KiB

View file

@ -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"

View file

@ -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),
]

View file

@ -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):

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)
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)

View file

@ -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