Merge pull request #263 from khoj-ai/fix/remove-guidance-for-desktop-gui

[Fix] Remove the default behavior of using GUI for Khoj
This commit is contained in:
sabaimran 2023-07-01 21:37:11 -07:00 committed by GitHub
commit 4915b7214d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 66 additions and 482 deletions

View file

@ -73,13 +73,17 @@ https://github.com/khoj-ai/khoj/assets/6413477/3e33d8ea-25bb-46c8-a3bf-c92f78d0f
<details><summary>Description</summary> <details><summary>Description</summary>
- Install Khoj via `pip` and start Khoj backend in non-gui mode 1. Install Khoj via `pip` and start Khoj backend in a terminal (Run `khoj`)
- Install Khoj plugin via Community Plugins settings pane on Obsidian app ```
- Check the new Khoj plugin settings python -m pip install khoj-assistant
- Let Khoj backend index the markdown, pdf, Github markdown files in the current Vault khoj
- Open Khoj plugin on Obsidian via Search button on Left Pane ```
- Search \"*Announce plugin to folks*\" in the [Obsidian Plugin docs](https://marcus.se.net/obsidian-plugin-docs/) 2. Install Khoj plugin via Community Plugins settings pane on Obsidian app
- Jump to the [search result](https://marcus.se.net/obsidian-plugin-docs/publishing/submit-your-plugin) - Check the new Khoj plugin settings
- Let Khoj backend index the markdown, pdf, Github markdown files in the current Vault
- Open Khoj plugin on Obsidian via Search button on Left Pane
- Search \"*Announce plugin to folks*\" in the [Obsidian Plugin docs](https://marcus.se.net/obsidian-plugin-docs/)
- Jump to the [search result](https://marcus.se.net/obsidian-plugin-docs/publishing/submit-your-plugin)
</details> </details>
### Khoj in Emacs, Browser ### Khoj in Emacs, Browser
@ -160,7 +164,7 @@ The optional steps below allow using Khoj from within an existing application li
- **Khoj via Emacs** - **Khoj via Emacs**
- Run `M-x khoj <user-query>` - Run `M-x khoj <user-query>`
- **Khoj via Web** - **Khoj via Web**
- Open <http://localhost:8000/> via desktop interface or directly - Open <http://localhost:8000/> directly
- **Khoj via API** - **Khoj via API**
- See the Khoj FastAPI [Swagger Docs](http://localhost:8000/docs), [ReDocs](http://localhost:8000/redocs) - See the Khoj FastAPI [Swagger Docs](http://localhost:8000/docs), [ReDocs](http://localhost:8000/redocs)
@ -308,8 +312,7 @@ pip install --upgrade --pre khoj-assistant
### Set your OpenAI API key in Khoj ### Set your OpenAI API key in Khoj
If you want, Khoj can be configured to use OpenAI for search and chat.<br /> If you want, Khoj can be configured to use OpenAI for search and chat.<br />
Add your OpenAI API to Khoj by using either of the two options below: Add your OpenAI API to Khoj by using either of the two options below:
- Open the Khoj desktop GUI, add your [OpenAI API key](https://beta.openai.com/account/api-keys) and click *Configure* - Open your [Khoj settings](http://localhost:8000/config/processor/conversation), add your OpenAI API key, and click *Save*. Then go to your [Khoj settings](http://localhost:8000/config) and click `Configure`. This will refresh Khoj with your OpenAI API key.
Ensure khoj is started **without** the `--no-gui` flag. Check your system tray to see if Khoj is minimized there.
- Set `openai-api-key` field under `processor.conversation` section in your `khoj.yml`[^1] to your [OpenAI API key](https://beta.openai.com/account/api-keys) and restart khoj: - Set `openai-api-key` field under `processor.conversation` section in your `khoj.yml`[^1] to your [OpenAI API key](https://beta.openai.com/account/api-keys) and restart khoj:
```diff ```diff
processor: processor:
@ -388,7 +391,7 @@ pip install -e .[dev]
khoj -vv khoj -vv
``` ```
2. Configure Khoj 2. Configure Khoj
- **Via GUI**: Add files, directories to index in the GUI window that pops up on starting Khoj, then Click Configure - **Via the Settings UI**: Add files, directories to index the [Khoj settings](http://localhost:8000/config) UI once Khoj has started up. Once you've saved all your settings, click `Configure`.
- **Manually**: - **Manually**:
- Copy the `config/khoj_sample.yml` to `~/.khoj/khoj.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-files` or `input-filter` in each relevant `content-type` section of `~/.khoj/khoj.yml`

View file

@ -27,4 +27,4 @@ services:
- ./tests/data/embeddings/:/data/embeddings/ - ./tests/data/embeddings/:/data/embeddings/
- ./tests/data/models/:/data/models/ - ./tests/data/models/:/data/models/
# Use 0.0.0.0 to explicitly set the host ip for the service on the container. https://pythonspeed.com/articles/docker-connection-refused/ # Use 0.0.0.0 to explicitly set the host ip for the service on the container. https://pythonspeed.com/articles/docker-connection-refused/
command: --no-gui --host="0.0.0.0" --port=8000 -c=config/khoj_docker.yml -vv command: --host="0.0.0.0" --port=8000 -c=config/khoj_docker.yml -vv

View file

@ -197,7 +197,7 @@ Use `which-key` if available, else display simple message in echo area"
:type 'string :type 'string
:group 'khoj) :group 'khoj)
(defcustom khoj-server-args '("--no-gui") (defcustom khoj-server-args '()
"Arguments to pass to Khoj server on startup." "Arguments to pass to Khoj server on startup."
:type '(repeat string) :type '(repeat string)
:group 'khoj) :group 'khoj)

View file

@ -40,13 +40,16 @@ https://github.com/khoj-ai/khoj/assets/6413477/3e33d8ea-25bb-46c8-a3bf-c92f78d0f
<details><summary>Description</summary> <details><summary>Description</summary>
1. Install Khoj via `pip` and start Khoj backend in non-gui mode 1. Install Khoj via `pip` and start Khoj backend
```shell
python -m pip install khoj-assistant && khoj
```
2. Install Khoj plugin via Community Plugins settings pane on Obsidian app 2. Install Khoj plugin via Community Plugins settings pane on Obsidian app
3. Check the new Khoj plugin settings - Check the new Khoj plugin settings
4. Wait for Khoj backend to index markdown, PDF files in the current Vault - Wait for Khoj backend to index markdown, PDF files in the current Vault
5. Open Khoj plugin on Obsidian via Search button on Left Pane - Open Khoj plugin on Obsidian via Search button on Left Pane
6. Search \"*Announce plugin to folks*\" in the [Obsidian Plugin docs](https://marcus.se.net/obsidian-plugin-docs/) - Search \"*Announce plugin to folks*\" in the [Obsidian Plugin docs](https://marcus.se.net/obsidian-plugin-docs/)
7. Jump to the [search result](https://marcus.se.net/obsidian-plugin-docs/publishing/submit-your-plugin) - Jump to the [search result](https://marcus.se.net/obsidian-plugin-docs/publishing/submit-your-plugin)
</details> </details>
@ -65,12 +68,12 @@ https://github.com/khoj-ai/khoj/assets/6413477/3e33d8ea-25bb-46c8-a3bf-c92f78d0f
Open terminal/cmd and run below command to install and start the khoj backend Open terminal/cmd and run below command to install and start the khoj backend
- On Linux/MacOS - On Linux/MacOS
```shell ```shell
python -m pip install khoj-assistant && khoj --no-gui python -m pip install khoj-assistant && khoj
``` ```
- On Windows - On Windows
```shell ```shell
py -m pip install khoj-assistant && khoj --no-gui py -m pip install khoj-assistant && khoj
``` ```
### 2. Setup Plugin ### 2. Setup Plugin
@ -96,7 +99,7 @@ See [Khoj Chat](https://github.com/khoj-ai/khoj/tree/master/#Khoj-Chat) for more
### Search ### Search
Click the *Khoj search* icon 🔎 on the [Ribbon](https://help.obsidian.md/User+interface/Workspace/Ribbon) or run *Khoj: Search* from the [Command Palette](https://help.obsidian.md/Plugins/Command+palette) Click the *Khoj search* icon 🔎 on the [Ribbon](https://help.obsidian.md/User+interface/Workspace/Ribbon) or run *Khoj: Search* from the [Command Palette](https://help.obsidian.md/Plugins/Command+palette)
*Note: Ensure the khoj server is running in the background before searching. Execute `khoj --no-gui` in your terminal if it is not already running* *Note: Ensure the khoj server is running in the background before searching. Execute `khoj` in your terminal if it is not already running*
https://user-images.githubusercontent.com/6413477/218801155-cd67e8b4-a770-404a-8179-d6b61caa0f93.mp4 https://user-images.githubusercontent.com/6413477/218801155-cd67e8b4-a770-404a-8179-d6b61caa0f93.mp4

View file

@ -34,11 +34,13 @@ logger = logging.getLogger(__name__)
def configure_server(args, required=False): def configure_server(args, required=False):
if args.config is None: if args.config is None:
if required: if required:
logger.error(f"Exiting as Khoj is not configured.\nConfigure it via GUI or by editing {state.config_file}.") logger.error(
f"Exiting as Khoj is not configured.\nConfigure it via http://localhost:8000/config or by editing {state.config_file}."
)
sys.exit(1) sys.exit(1)
else: else:
logger.warning( logger.warning(
f"Khoj is not configured.\nConfigure it via khoj GUI, plugins or by editing {state.config_file}." f"Khoj is not configured.\nConfigure it via http://localhost:8000/config, plugins or by editing {state.config_file}."
) )
return return
else: else:

View file

@ -1,77 +0,0 @@
# External Packages
from PyQt6 import QtWidgets
from PyQt6.QtCore import QDir
# Internal Packages
from khoj.utils.config import SearchType
from khoj.utils.helpers import is_none_or_empty
class FileBrowser(QtWidgets.QWidget):
def __init__(self, title, search_type: SearchType = None, default_files: list = []):
QtWidgets.QWidget.__init__(self)
layout = QtWidgets.QHBoxLayout()
self.setLayout(layout)
self.search_type = search_type
self.filter_name = self.getFileFilter(search_type)
self.dirpath = QDir.homePath()
self.label = QtWidgets.QLabel()
self.label.setText(title)
self.label.setFixedWidth(95)
self.label.setWordWrap(True)
layout.addWidget(self.label)
self.lineEdit = QtWidgets.QPlainTextEdit(self)
self.lineEdit.setFixedWidth(330)
self.setFiles(default_files)
self.lineEdit.setFixedHeight(min(7 + 20 * len(self.lineEdit.toPlainText().split("\n")), 90))
self.lineEdit.textChanged.connect(self.updateFieldHeight) # type: ignore[attr-defined]
layout.addWidget(self.lineEdit)
self.button = QtWidgets.QPushButton("Add")
self.button.clicked.connect(self.storeFilesSelectedInFileDialog) # type: ignore[attr-defined]
layout.addWidget(self.button)
layout.addStretch()
def getFileFilter(self, search_type):
if search_type == SearchType.Org:
return "Org-Mode Files (*.org)"
elif search_type == SearchType.Ledger:
return "Beancount Files (*.bean *.beancount)"
elif search_type == SearchType.Markdown:
return "Markdown Files (*.md *.markdown)"
elif search_type == SearchType.Pdf:
return "Pdf Files (*.pdf)"
elif search_type == SearchType.Music:
return "Org-Music Files (*.org)"
elif search_type == SearchType.Image:
return "Images (*.jp[e]g)"
def storeFilesSelectedInFileDialog(self):
filepaths = self.getPaths()
if self.search_type == SearchType.Image:
filepaths.append(
QtWidgets.QFileDialog.getExistingDirectory(self, caption="Choose Folder", directory=self.dirpath)
)
else:
filepaths.extend(
QtWidgets.QFileDialog.getOpenFileNames(
self, caption="Choose Files", directory=self.dirpath, filter=self.filter_name
)[0]
)
self.setFiles(filepaths)
def setFiles(self, paths: list):
self.filepaths = [path for path in paths if not is_none_or_empty(path)]
self.lineEdit.setPlainText("\n".join(self.filepaths))
def getPaths(self) -> list:
if self.lineEdit.toPlainText() == "":
return []
else:
return self.lineEdit.toPlainText().split("\n")
def updateFieldHeight(self):
self.lineEdit.setFixedHeight(min(7 + 20 * len(self.lineEdit.toPlainText().split("\n")), 90))

View file

@ -1,31 +0,0 @@
# External Packages
from PyQt6 import QtWidgets
# Internal Packages
from khoj.utils.config import ProcessorType
from khoj.utils.config import SearchType
class LabelledTextField(QtWidgets.QWidget):
def __init__(
self, title, search_type: SearchType = None, processor_type: ProcessorType = None, default_value: str = None
):
QtWidgets.QWidget.__init__(self)
layout = QtWidgets.QHBoxLayout()
self.setLayout(layout)
self.processor_type = processor_type
self.search_type = search_type
self.label = QtWidgets.QLabel()
self.label.setText(title)
self.label.setFixedWidth(95)
self.label.setWordWrap(True)
layout.addWidget(self.label)
self.input_field = QtWidgets.QTextEdit(self)
self.input_field.setFixedWidth(410)
self.input_field.setFixedHeight(27)
self.input_field.setText(default_value)
layout.addWidget(self.input_field)
layout.addStretch()

View file

@ -1,52 +1,22 @@
# Standard Packages # Standard Packages
from enum import Enum
from pathlib import Path
from copy import deepcopy
import webbrowser import webbrowser
# External Packages # External Packages
from PyQt6 import QtGui, QtWidgets from PyQt6 import QtGui, QtWidgets
from PyQt6.QtCore import Qt, QThread, QObject, pyqtSignal from PyQt6.QtCore import Qt
# Internal Packages # Internal Packages
from khoj.configure import configure_server from khoj.utils import constants
from khoj.interface.desktop.file_browser import FileBrowser
from khoj.interface.desktop.labelled_text_field import LabelledTextField
from khoj.utils import constants, state, yaml as yaml_utils
from khoj.utils.cli import cli
from khoj.utils.config import SearchType, ProcessorType
from khoj.utils.helpers import merge_dicts, resolve_absolute_path
class MainWindow(QtWidgets.QMainWindow): class MainWindow(QtWidgets.QMainWindow):
"""Create Window to Configure Khoj """Create Window to Navigate users to the web UI"""
Allow user to
1. Configure content types to search
2. Configure conversation processor
3. Save the configuration to khoj.yml
"""
def __init__(self, config_file: Path): def __init__(self, host: str, port: int):
super(MainWindow, self).__init__() 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():
self.first_run = False
self.current_config = yaml_utils.load_config_from_file(self.config_file)
else:
self.first_run = True
self.current_config = deepcopy(constants.default_config)
self.new_config = self.current_config
# Initialize Configure Window # Initialize Configure Window
self.setWindowTitle("Khoj") self.setWindowTitle("Khoj")
self.setFixedWidth(600)
# Set Window Icon # Set Window Icon
icon_path = constants.web_directory / "assets/icons/favicon-128x128.png" icon_path = constants.web_directory / "assets/icons/favicon-128x128.png"
@ -55,24 +25,14 @@ class MainWindow(QtWidgets.QMainWindow):
# Initialize Configure Window Layout # Initialize Configure Window Layout
self.wlayout = QtWidgets.QVBoxLayout() self.wlayout = QtWidgets.QVBoxLayout()
# Add Settings Panels for each Search Type to Configure Window Layout # Add a Label that says "Khoj Configuration" to the Window
self.search_settings_panels = [] self.wlayout.addWidget(QtWidgets.QLabel("Welcome to Khoj"))
for search_type in SearchType:
current_content_config = self.current_config["content-type"].get(
search_type, None
) or self.get_default_config(search_type=search_type)
self.search_settings_panels += [self.add_settings_panel(current_content_config, search_type)]
# Add Conversation Processor Panel to Configure Screen
self.processor_settings_panels = []
conversation_type = ProcessorType.Conversation
if self.current_config["processor"] and conversation_type in self.current_config["processor"]:
current_conversation_config = self.current_config["processor"][conversation_type]
else:
current_conversation_config = self.get_default_config(processor_type=conversation_type)
self.processor_settings_panels += [self.add_processor_panel(current_conversation_config, conversation_type)]
# Add Action Buttons Panel # Add a Button to open the Web UI at http://host:port/config
self.add_action_panel() self.open_web_ui_button = QtWidgets.QPushButton("Open Web UI")
self.open_web_ui_button.clicked.connect(lambda: webbrowser.open(f"http://{host}:{port}/config"))
self.wlayout.addWidget(self.open_web_ui_button)
# Set the central widget of the Window. Widget will expand # Set the central widget of the Window. Widget will expand
# to take up all the space in the window by default. # to take up all the space in the window by default.
@ -81,250 +41,6 @@ class MainWindow(QtWidgets.QMainWindow):
self.setCentralWidget(self.config_window) self.setCentralWidget(self.config_window)
self.position_window() self.position_window()
def add_settings_panel(self, current_content_config: dict, search_type: SearchType):
"Add Settings Panel for specified Search Type. Toggle Editable Search Types"
# Get current files from config for given search type
if search_type == SearchType.Image:
current_content_files = current_content_config.get("input-directories", [])
file_input_text = f"{search_type.name} Folders"
elif search_type == SearchType.Github:
return self.add_github_settings_panel(current_content_config, SearchType.Github)
else:
current_content_files = current_content_config.get("input-files", [])
file_input_text = f"{search_type.name} Files"
# Create widgets to display settings for given search type
search_type_settings = QtWidgets.QWidget()
search_type_layout = QtWidgets.QVBoxLayout(search_type_settings)
enable_search_type = SearchCheckBox(f"Search {search_type.name}", search_type)
# Add file browser to set input files for given search type
input_files = FileBrowser(file_input_text, search_type, current_content_files or [])
# Set enabled/disabled based on checkbox state
enable_search_type.setChecked(current_content_files is not None and len(current_content_files) > 0)
input_files.setEnabled(enable_search_type.isChecked())
enable_search_type.stateChanged.connect(lambda _: input_files.setEnabled(enable_search_type.isChecked())) # type: ignore[attr-defined]
# Add setting widgets for given search type to panel
search_type_layout.addWidget(enable_search_type)
search_type_layout.addWidget(input_files)
self.wlayout.addWidget(search_type_settings)
return search_type_settings
def add_github_settings_panel(self, current_content_config: dict, search_type: SearchType):
search_type_settings = QtWidgets.QWidget()
search_type_layout = QtWidgets.QVBoxLayout(search_type_settings)
enable_search_type = SearchCheckBox(f"Search {search_type.name}", search_type)
# Add labelled text input field
input_fields = []
fields = ["pat-token", "repo-name", "repo-owner", "repo-branch"]
active = False
for field in fields:
field_value = current_content_config.get(field, None)
input_field = LabelledTextField(field, search_type=search_type, default_value=field_value)
search_type_layout.addWidget(input_field)
input_fields += [input_field]
if field_value:
active = True
# Set enabled/disabled based on checkbox state
enable_search_type.setChecked(active)
for input_field in input_fields:
input_field.setEnabled(enable_search_type.isChecked())
enable_search_type.stateChanged.connect(lambda _: [input_field.setEnabled(enable_search_type.isChecked()) for input_field in input_fields]) # type: ignore[attr-defined]
# Add setting widgets for given search type to panel
search_type_layout.addWidget(enable_search_type)
for input_field in input_fields:
search_type_layout.addWidget(input_field)
self.wlayout.addWidget(search_type_settings)
return search_type_settings
def add_processor_panel(self, current_conversation_config: dict, processor_type: ProcessorType):
"Add Conversation Processor Panel"
# Get current settings from config for given processor type
current_openai_api_key = current_conversation_config.get("openai-api-key", None)
# Create widgets to display settings for given processor type
processor_type_settings = QtWidgets.QWidget()
processor_type_layout = QtWidgets.QVBoxLayout(processor_type_settings)
enable_conversation = ProcessorCheckBox(f"Conversation", processor_type)
# Add file browser to set input files for given processor type
input_field = LabelledTextField(
"OpenAI API Key", processor_type=processor_type, default_value=current_openai_api_key
)
# Set enabled/disabled based on checkbox state
enable_conversation.setChecked(current_openai_api_key is not None)
input_field.setEnabled(enable_conversation.isChecked())
enable_conversation.stateChanged.connect(lambda _: input_field.setEnabled(enable_conversation.isChecked())) # type: ignore[attr-defined]
# Add setting widgets for given processor type to panel
processor_type_layout.addWidget(enable_conversation)
processor_type_layout.addWidget(input_field)
self.wlayout.addWidget(processor_type_settings)
return processor_type_settings
def add_action_panel(self):
"Add Action Panel"
# Button to Save Settings
action_bar = QtWidgets.QWidget()
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(f"http://{state.host}:{state.port}/")
)
self.search_button.setEnabled(not self.first_run)
action_bar_layout.addWidget(self.configure_button)
action_bar_layout.addWidget(self.search_button)
self.wlayout.addWidget(action_bar)
def get_default_config(self, search_type: SearchType = None, processor_type: ProcessorType = None):
"Get default config"
config = constants.default_config
if search_type:
return config["content-type"][search_type] # type: ignore
elif processor_type:
return config["processor"][processor_type] # type: ignore
else:
return config
def add_error_message(self, message: str):
"Add Error Message to Configure Screen"
# Remove any existing error messages
for message_prefix in ErrorType:
for i in reversed(range(self.wlayout.count())):
current_widget = self.wlayout.itemAt(i).widget()
if isinstance(current_widget, QtWidgets.QLabel) and current_widget.text().startswith(
message_prefix.value
):
self.wlayout.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.wlayout.addWidget(error_message)
def update_search_settings(self):
"Update config with search settings from UI"
for settings_panel in self.search_settings_panels:
for child in settings_panel.children():
if not isinstance(child, (SearchCheckBox, FileBrowser, LabelledTextField)):
continue
if isinstance(child, SearchCheckBox):
# Search Type Disabled
if not child.isChecked() and child.search_type in self.new_config["content-type"]:
del self.new_config["content-type"][child.search_type]
# Search Type (re)-Enabled
if child.isChecked():
current_search_config = self.current_config["content-type"].get(child.search_type, {})
if current_search_config == None:
current_search_config = {}
default_search_config = self.get_default_config(search_type=child.search_type)
self.new_config["content-type"][child.search_type.value] = merge_dicts(
current_search_config, default_search_config
)
elif isinstance(child, FileBrowser) and child.search_type in self.new_config["content-type"]:
if child.search_type.value == SearchType.Image:
self.new_config["content-type"][child.search_type.value]["input-directories"] = (
child.getPaths() if child.getPaths() != [] else None
)
else:
self.new_config["content-type"][child.search_type.value]["input-files"] = (
child.getPaths() if child.getPaths() != [] else None
)
elif isinstance(child, LabelledTextField):
self.new_config["content-type"][child.search_type.value][
child.label.text()
] = child.input_field.toPlainText()
def update_processor_settings(self):
"Update config with conversation settings from UI"
for settings_panel in self.processor_settings_panels:
for child in settings_panel.children():
if not isinstance(child, (ProcessorCheckBox, LabelledTextField)):
continue
if isinstance(child, ProcessorCheckBox):
# Processor Type Disabled
if not child.isChecked() and child.processor_type in self.new_config["processor"]:
del self.new_config["processor"][child.processor_type]
# Processor Type (re)-Enabled
if child.isChecked():
current_processor_config = self.current_config["processor"].get(child.processor_type, {})
default_processor_config = self.get_default_config(processor_type=child.processor_type)
self.new_config["processor"][child.processor_type.value] = merge_dicts(
current_processor_config, default_processor_config
)
elif isinstance(child, LabelledTextField) and child.processor_type in self.new_config["processor"]:
if child.processor_type == ProcessorType.Conversation:
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"{ErrorType.ConfigValidationError.value}: {e}")
return False
# 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
def load_updated_settings(self):
"Hot swap to use the updated config from config file"
# Load parsed, validated config from app config file
args = cli(state.cli_args)
self.current_config = self.new_config
# Configure server with loaded config
configure_server(args, required=True)
def configure_app(self):
"Save the new settings to khoj.yml. Reload app with updated settings"
self.update_search_settings()
self.update_processor_settings()
if self.save_settings_to_file():
# Setup thread to load updated settings in background
self.thread = QThread()
self.settings_loader = SettingsLoader(self.load_updated_settings)
self.settings_loader.moveToThread(self.thread)
# Connect slots and signals for thread
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
self.thread.start()
# Disable Save Button
self.search_button.setEnabled(False)
self.configure_button.setEnabled(False)
self.configure_button.setText("Configuring...")
# Reset UI
self.thread.finished.connect(lambda: self.configure_button.setText("Configure"))
self.thread.finished.connect(lambda: self.configure_button.setEnabled(True))
self.thread.finished.connect(lambda: self.search_button.setEnabled(True))
def position_window(self): def position_window(self):
"Position the window at center of X axis and near top on Y axis" "Position the window at center of X axis and near top on Y axis"
window_rectangle = self.geometry() window_rectangle = self.geometry()
@ -338,41 +54,3 @@ class MainWindow(QtWidgets.QMainWindow):
self.setWindowState(Qt.WindowState.WindowActive) self.setWindowState(Qt.WindowState.WindowActive)
self.activateWindow() # For Bringing to Top on Windows self.activateWindow() # For Bringing to Top on Windows
self.raise_() # For Bringing to Top from Minimized State on OSX self.raise_() # For Bringing to Top from Minimized State on OSX
class SettingsLoader(QObject):
"Load Settings Thread"
finished = pyqtSignal()
error = pyqtSignal(str)
def __init__(self, load_settings_func):
super(SettingsLoader, self).__init__()
self.load_settings_func = load_settings_func
def run(self):
"Load Settings"
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()
class SearchCheckBox(QtWidgets.QCheckBox):
def __init__(self, text, search_type: SearchType, parent=None):
self.search_type = search_type
super(SearchCheckBox, self).__init__(text, parent=parent)
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

@ -26,7 +26,8 @@ def create_system_tray(gui: QtWidgets.QApplication, main_window: MainWindow):
menu = QtWidgets.QMenu() menu = QtWidgets.QMenu()
menu_actions = [ menu_actions = [
("Search", lambda: webbrowser.open(f"http://{state.host}:{state.port}/")), ("Search", lambda: webbrowser.open(f"http://{state.host}:{state.port}/")),
("Configure", main_window.show_on_top), ("Configure", lambda: webbrowser.open(f"http://{state.host}:{state.port}/config")),
("App", main_window.show),
("Quit", gui.quit), ("Quit", gui.quit),
] ]

View file

@ -6,6 +6,7 @@ import logging
import threading import threading
import warnings import warnings
from platform import system from platform import system
import webbrowser
# Ignore non-actionable warnings # Ignore non-actionable warnings
warnings.filterwarnings("ignore", message=r"snapshot_download.py has been made private", category=FutureWarning) warnings.filterwarnings("ignore", message=r"snapshot_download.py has been made private", category=FutureWarning)
@ -63,17 +64,20 @@ def run():
logger.info("🌘 Starting Khoj") logger.info("🌘 Starting Khoj")
if args.no_gui: if not args.gui:
if not state.demo:
# Setup task scheduler # Setup task scheduler
poll_task_scheduler() poll_task_scheduler()
# Start Server # Start Server
configure_server(args, required=False) configure_server(args, required=False)
configure_routes(app) configure_routes(app)
start_server(app, host=args.host, port=args.port, socket=args.socket) start_server(app, host=args.host, port=args.port, socket=args.socket)
else: else:
logger.warning("🚧 GUI is being deprecated and may not work as expected. Starting...")
# Setup GUI # Setup GUI
gui = QtWidgets.QApplication([]) gui = QtWidgets.QApplication([])
main_window = MainWindow(args.config_file) main_window = MainWindow(args.host, args.port)
# System tray is only available on Windows, MacOS. # System tray is only available on Windows, MacOS.
# On Linux (Gnome) the System tray is not supported. # On Linux (Gnome) the System tray is not supported.
@ -89,6 +93,13 @@ def run():
configure_routes(app) configure_routes(app)
server = ServerThread(app, args.host, args.port, args.socket) server = ServerThread(app, args.host, args.port, args.socket)
url = f"http://{args.host}:{args.port}"
logger.info(f"🌗 Khoj is running at {url}")
try:
webbrowser.open(url)
except:
logger.warning("🚧 Unable to open browser. Please open it manually to configure Khoj.")
# Show Main Window on First Run Experience or if on Linux # Show Main Window on First Run Experience or if on Linux
if args.config is None or system() not in ["Windows", "Darwin"]: if args.config is None or system() not in ["Windows", "Darwin"]:
main_window.show() main_window.show()

View file

@ -39,17 +39,13 @@ class GithubToJsonl(TextToJsonl):
return return
def process(self, previous_entries=None): def process(self, previous_entries=None):
# If demo mode is enabled, don't re-process any of the repositories. This is resource intensive.
if state.demo and previous_entries is not None:
return self.update_entries_with_ids(previous_entries, previous_entries)
current_entries = [] current_entries = []
for repo in self.config.repos: for repo in self.config.repos:
current_entries += self.process_repo(repo, previous_entries) current_entries += self.process_repo(repo)
return self.update_entries_with_ids(current_entries, previous_entries) return self.update_entries_with_ids(current_entries, previous_entries)
def process_repo(self, repo: GithubRepoConfig, previous_entries=None): def process_repo(self, repo: GithubRepoConfig):
repo_url = f"https://api.github.com/repos/{repo.owner}/{repo.name}" repo_url = f"https://api.github.com/repos/{repo.owner}/{repo.name}"
repo_shorthand = f"{repo.owner}/{repo.name}" repo_shorthand = f"{repo.owner}/{repo.name}"
logger.info(f"Processing github repo {repo_shorthand}") logger.info(f"Processing github repo {repo_shorthand}")

View file

@ -16,9 +16,7 @@ def cli(args=None):
parser.add_argument( parser.add_argument(
"--config-file", "-c", default="~/.khoj/khoj.yml", type=pathlib.Path, help="YAML file to configure Khoj" "--config-file", "-c", default="~/.khoj/khoj.yml", type=pathlib.Path, help="YAML file to configure Khoj"
) )
parser.add_argument( parser.add_argument("--gui", action="store_true", default=False, help="Show native desktop GUI. Default: false")
"--no-gui", action="store_true", default=False, help="Do not show native desktop GUI. Default: false"
)
parser.add_argument( parser.add_argument(
"--regenerate", "--regenerate",
action="store_true", action="store_true",

View file

@ -16,7 +16,7 @@ def test_cli_minimal_default():
# Assert # Assert
assert actual_args.config_file == resolve_absolute_path(Path("~/.khoj/khoj.yml")) assert actual_args.config_file == resolve_absolute_path(Path("~/.khoj/khoj.yml"))
assert actual_args.regenerate == False assert actual_args.regenerate == False
assert actual_args.no_gui == False assert actual_args.gui == False
assert actual_args.verbose == 0 assert actual_args.verbose == 0
@ -36,11 +36,11 @@ def test_cli_invalid_config_file_path():
# ---------------------------------------------------------------------------------------------------- # ----------------------------------------------------------------------------------------------------
def test_cli_config_from_file(): def test_cli_config_from_file():
# Act # Act
actual_args = cli(["-c=tests/data/config.yml", "--regenerate", "--no-gui", "-vvv"]) actual_args = cli(["-c=tests/data/config.yml", "--regenerate", "--gui", "-vvv"])
# Assert # Assert
assert actual_args.config_file == resolve_absolute_path(Path("tests/data/config.yml")) assert actual_args.config_file == resolve_absolute_path(Path("tests/data/config.yml"))
assert actual_args.no_gui == True assert actual_args.gui == True
assert actual_args.regenerate == True assert actual_args.regenerate == True
assert actual_args.config is not None assert actual_args.config is not None
assert actual_args.verbose == 3 assert actual_args.verbose == 3