mirror of
https://github.com/khoj-ai/khoj.git
synced 2025-02-17 08:04:21 +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 -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:
commit
7fc8672666
7 changed files with 75 additions and 41 deletions
52
Readme.md
52
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 <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
BIN
docs/interfaces.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 606 KiB |
|
@ -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"
|
||||
|
|
|
@ -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),
|
||||
]
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
Loading…
Add table
Reference in a new issue