mirror of
https://github.com/khoj-ai/khoj.git
synced 2024-11-24 07:55:07 +01:00
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:
commit
4915b7214d
13 changed files with 66 additions and 482 deletions
25
README.md
25
README.md
|
@ -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`
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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))
|
|
|
@ -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()
|
|
|
@ -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"
|
|
||||||
|
|
|
@ -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),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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}")
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in a new issue