mirror of
https://github.com/khoaliber/khoj.git
synced 2026-03-09 13:25:11 +00:00
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 -b891347Fix condition in router to trigger markdown search - Improve Desktop GUI -67ab40bRegenerate embeddings everytime user clicks configure in Desktop GUI -7f479b0Improve Displaying Error to User on Khoj window in Desktop GUI -873bb9dDo not force the Khoj window to always be on top. It's needlessly annoying -9bc4fd5Set Web Interface URL from loaded state in Desktop GUIs. Not hard-coded
This commit is contained in:
52
Readme.md
52
Readme.md
@@ -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
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
@@ -65,30 +70,31 @@
|
|||||||
|
|
||||||
## Setup
|
## Setup
|
||||||
### 1. Install
|
### 1. Install
|
||||||
``` shell
|
``` shell
|
||||||
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)
|
``` shell
|
||||||
- Set `input-directories` field in `content-type.image` section
|
khoj
|
||||||
- Delete `content-type`, `processor` sub-sections irrelevant for your use-case
|
```
|
||||||
|
|
||||||
### 3. Run
|
### 3. Configure
|
||||||
``` shell
|
|
||||||
khoj -c=config/khoj_sample.yml -vv
|
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
|
||||||
Loads ML model, generates embeddings and exposes API to search notes, images, transactions etc specified in config YAML
|
|
||||||
|

|
||||||
|
|
||||||
## 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
BIN
docs/interfaces.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 606 KiB |
@@ -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,11 +153,21 @@ 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"
|
||||||
error_message = QtWidgets.QLabel()
|
# Remove any existing error messages
|
||||||
error_message.setWordWrap(True)
|
for message_prefix in ErrorType:
|
||||||
error_message.setText(message)
|
for i in reversed(range(self.layout.count())):
|
||||||
error_message.setStyleSheet("color: red")
|
current_widget = self.layout.itemAt(i).widget()
|
||||||
self.layout.addWidget(error_message)
|
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):
|
def update_search_settings(self):
|
||||||
"Update config with search settings from UI"
|
"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
|
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"
|
||||||
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()
|
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"
|
||||||
|
|||||||
@@ -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),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
Reference in New Issue
Block a user