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
+
+
## 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
+
+ 
## 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