Remove PySide dependency and deprecate desktop builds (#475)

* Remove PySide, gui option from code
* Remove pyside 6 dependency from code
* Remove workflows which build desktop applications
* Update unit tests and update line in documentation
* Remove additional references to pyinstaller, gui
* Add uninstall steps to normal uninstall instructions
This commit is contained in:
sabaimran 2023-09-07 11:36:27 -07:00 committed by GitHub
parent 76562f4250
commit dccfae3853
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 13 additions and 568 deletions

View file

@ -1,113 +0,0 @@
name: desktop_dev_build
on:
push:
branches:
- master
paths:
- src/khoj/**
- pyproject.toml
- Khoj.spec
- .github/workflows/build_desktop.yml
workflow_dispatch:
jobs:
publish_desktop_apps:
name: 🖥️ Publish Desktop Apps
strategy:
matrix:
include:
- os: ubuntu-20.04
extension: deb
- os: macos-latest
extension: dmg
- os: windows-latest
extension: exe
runs-on: ${{ matrix.os }}
permissions:
contents: write
steps:
- uses: actions/checkout@v3
- name: Set up Python 3.9
uses: actions/setup-python@v4
with:
python-version: '3.9'
- name: ⏬️ Install Dependencies
shell: bash
run: |
if [ "$RUNNER_OS" == "Linux" ]; then
sudo apt update && sudo apt install libegl1 libxcb-xinerama0 python3-tk -y
fi
python -m pip install --upgrade pip
pip install pyinstaller
- name: ⬇️ Install Khoj App
run: |
pip install --upgrade .
- name: 📦 Package Khoj App
shell: bash
run: |
# Setup Environment for Reproducible Builds
export PYTHONHASHSEED=42
export SOURCE_DATE_EPOCH=$(git log -1 --pretty=%ct)
pyinstaller --noconfirm Khoj.spec
if [ "$RUNNER_OS" == "Windows" ]; then
mv dist/Khoj.exe dist/khoj_dev_amd64.exe
fi
- name: 💻 Create Mac App DMG
if: matrix.os == 'macos-latest'
run: |
# Install Mac DMG Creator
brew install create-dmg
# Copy app to separate dmg folder
mkdir -p dist/dmg && cp -r dist/Khoj.app dist/dmg
# Create disk image with the app
create-dmg \
--volname "Khoj" \
--volicon "src/khoj/interface/web/assets/icons/favicon.icns" \
--window-pos 200 120 \
--window-size 600 300 \
--icon-size 100 \
--icon "Khoj.app" 175 120 \
--hide-extension "Khoj.app" \
--app-drop-link 425 120 \
"dist/khoj_dev_amd64.dmg" \
"dist/dmg/"
- uses: ruby/setup-ruby@v1
if: matrix.os == 'ubuntu-20.04'
with:
ruby-version: '3.0'
- name: 🐧 Create Debian Package
if: matrix.os == 'ubuntu-20.04'
shell: bash
run: |
# Install Debian Packager
gem install fpm
# Copy app files into expected output directory structure
mkdir -p package/opt package/usr/share/applications package/usr/share/icons/hicolor/128x128/apps
cp -r dist/Khoj package/opt/Khoj
cp src/khoj/interface/web/assets/icons/favicon-128x128.png package/usr/share/icons/hicolor/128x128/apps/Khoj.png
cp Khoj.desktop package/usr/share/applications
# Fix permissions to be usable by non-root users
find package/usr/share -type f -exec chmod 644 -- {} +
chmod 755 package/opt/Khoj
# Package the app
fpm -C package -s dir -t deb -n Khoj -p dist/khoj_dev_amd64.deb
- uses: actions/upload-artifact@v3
with:
name: khoj_dev_amd64.${{matrix.extension}}
path: dist/khoj_dev_amd64.${{matrix.extension}}
retention-days: 3

View file

@ -64,111 +64,3 @@ jobs:
src/interface/obsidian/main.js src/interface/obsidian/main.js
src/interface/obsidian/manifest.json src/interface/obsidian/manifest.json
src/interface/obsidian/styles.css src/interface/obsidian/styles.css
publish_desktop_apps:
name: 🖥️ Publish Desktop Apps
strategy:
matrix:
include:
- os: ubuntu-20.04
extension: deb
- os: macos-latest
extension: dmg
- os: windows-latest
extension: exe
runs-on: ${{ matrix.os }}
permissions:
contents: write
steps:
- uses: actions/checkout@v3
- name: Set up Python 3.9
uses: actions/setup-python@v4
with:
python-version: '3.9'
- name: ⏬️ Install Dependencies
shell: bash
run: |
if [ "$RUNNER_OS" == "Linux" ]; then
sudo apt update && sudo apt install libegl1 libxcb-xinerama0 python3-tk -y
fi
python -m pip install --upgrade pip
pip install pyinstaller
- name: ⬇️ Install Khoj App
run: |
pip install --upgrade .
- name: 📦 Package Khoj App
shell: bash
run: |
# Setup Environment for Reproducible Builds
export PYTHONHASHSEED=42
export SOURCE_DATE_EPOCH=$(git log -1 --pretty=%ct)
pyinstaller --noconfirm Khoj.spec
if [ "$RUNNER_OS" == "Windows" ]; then
mv dist/Khoj.exe dist/khoj_"$GITHUB_REF_NAME"_amd64.exe
fi
- name: 💻 Create Mac App DMG
if: matrix.os == 'macos-latest'
run: |
# Install Mac DMG Creator
brew install create-dmg
# Copy app to separate dmg folder
mkdir -p dist/dmg && cp -r dist/Khoj.app dist/dmg
# Create disk image with the app
create-dmg \
--volname "Khoj" \
--volicon "src/khoj/interface/web/assets/icons/favicon.icns" \
--window-pos 200 120 \
--window-size 600 300 \
--icon-size 100 \
--icon "Khoj.app" 175 120 \
--hide-extension "Khoj.app" \
--app-drop-link 425 120 \
"dist/khoj_"$GITHUB_REF_NAME"_amd64.dmg" \
"dist/dmg/"
- uses: ruby/setup-ruby@v1
if: matrix.os == 'ubuntu-20.04'
with:
ruby-version: '3.0'
- name: 🐧 Create Debian Package
if: matrix.os == 'ubuntu-20.04'
shell: bash
env:
DEBIAN_PACKAGE_VERSION: ${{ inputs.version }}
run: |
# Install Debian Packager
gem install fpm
# Copy app files into expected output directory structure
mkdir -p package/opt package/usr/share/applications package/usr/share/icons/hicolor/128x128/apps
cp -r dist/Khoj package/opt/Khoj
cp src/khoj/interface/web/assets/icons/favicon-128x128.png package/usr/share/icons/hicolor/128x128/apps/Khoj.png
cp Khoj.desktop package/usr/share/applications
# Fix permissions to be usable by non-root users
find package/usr/share -type f -exec chmod 644 -- {} +
chmod 755 package/opt/Khoj
# Package the app
if [ -z "$DEBIAN_PACKAGE_VERSION" ]; then
DEBIAN_PACKAGE_VERSION=$(echo $GITHUB_REF_NAME | sed -E 's/v(.*)/\1/g')
fi
fpm -C package -s dir -t deb -n Khoj --version $DEBIAN_PACKAGE_VERSION -p dist/khoj_"$GITHUB_REF_NAME"_amd64.deb
- uses: actions/upload-artifact@v3
with:
name: khoj_${{github.ref_name}}_amd64.${{matrix.extension}}
path: dist/khoj_${{github.ref_name}}_amd64.${{matrix.extension}}
- name: 🌈 Release
uses: softprops/action-gh-release@v1
if: startsWith(github.ref, 'refs/tags/')
with:
generate_release_notes: true
files: dist/khoj_${{github.ref_name}}_amd64.${{matrix.extension}}

View file

@ -1,7 +0,0 @@
[Desktop Entry]
Type=Application
Name=Khoj
Comment=An AI personal assistant for your Digital Brain
Path=/opt
Exec=/opt/Khoj
Icon=Khoj

123
Khoj.spec
View file

@ -1,123 +0,0 @@
# -*- mode: python ; coding: utf-8 -*-
from os.path import join
from platform import system
from PyInstaller.utils.hooks import copy_metadata
import sysconfig
datas = [
('src/khoj/interface/web', 'khoj/interface/web'),
(f'{sysconfig.get_paths()["purelib"]}/transformers', 'transformers'),
(f'{sysconfig.get_paths()["purelib"]}/langchain', 'langchain'),
(f'{sysconfig.get_paths()["purelib"]}/PIL', 'PIL'),
(f'{sysconfig.get_paths()["purelib"]}/gpt4all', 'gpt4all'),
]
datas += copy_metadata('torch')
datas += copy_metadata('tqdm')
datas += copy_metadata('regex')
datas += copy_metadata('requests')
datas += copy_metadata('packaging')
datas += copy_metadata('filelock')
datas += copy_metadata('numpy')
datas += copy_metadata('tokenizers')
datas += copy_metadata('pillow')
datas += copy_metadata('huggingface_hub')
datas += copy_metadata('safetensors')
datas += copy_metadata('pyyaml')
block_cipher = None
a = Analysis(
['src/khoj/main.py'],
pathex=[],
binaries=[],
datas=datas,
hiddenimports=['huggingface_hub.repository', 'PIL', 'PIL._tkinter_finder', 'tiktoken_ext', 'tiktoken_ext.openai_public'],
hookspath=[],
hooksconfig={},
runtime_hooks=[],
excludes=[],
win_no_prefer_redirects=False,
win_private_assemblies=False,
cipher=block_cipher,
noarchive=False,
)
# Filter out unused and/or duplicate shared libs
torch_lib_paths = {
join('torch', 'lib', 'libtorch_cuda.so'),
join('torch', 'lib', 'libtorch_cpu.so'),
}
a.datas = [entry for entry in a.datas if not entry[0] in torch_lib_paths]
os_path_separator = '\\' if system() == 'Windows' else '/'
a.datas = [entry for entry in a.datas if not f'torch{os_path_separator}_C.cp' in entry[0]]
a.datas = [entry for entry in a.datas if not f'torch{os_path_separator}_dl.cp' in entry[0]]
pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)
if system() != 'Darwin':
# Add Splash screen to show on app launch
splash = Splash(
'src/khoj/interface/web/assets/icons/favicon-128x128.png',
binaries=a.binaries,
datas=a.datas,
text_pos=(10, 160),
text_size=12,
text_color='black',
minify_script=True,
always_on_top=True
)
exe = EXE(
pyz,
a.scripts,
a.binaries,
a.zipfiles,
a.datas,
splash,
splash.binaries,
[],
name='Khoj',
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=True,
upx_exclude=[],
runtime_tmpdir=None,
console=False,
disable_windowed_traceback=False,
argv_emulation=False,
target_arch='x86_64',
codesign_identity=None,
entitlements_file=None,
icon='src/khoj/interface/web/assets/icons/favicon-128x128.ico',
)
else:
exe = EXE(
pyz,
a.scripts,
a.binaries,
a.zipfiles,
a.datas,
[],
name='Khoj',
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=True,
upx_exclude=[],
runtime_tmpdir=None,
console=False,
disable_windowed_traceback=False,
argv_emulation=False,
target_arch='x86_64',
codesign_identity=None,
entitlements_file=None,
icon='src/khoj/interface/web/assets/icons/favicon.icns',
)
app = BUNDLE(
exe,
name='Khoj.app',
icon='src/khoj/interface/web/assets/icons/favicon.icns',
bundle_identifier=None,
)

View file

@ -27,7 +27,7 @@ For more detailed Windows installation and troubleshooting, see [Windows Install
Run the following command from your terminal to start the Khoj backend and open Khoj in your browser. Run the following command from your terminal to start the Khoj backend and open Khoj in your browser.
```shell ```shell
khoj --gui khoj
``` ```
Note: To start Khoj automatically in the background use [Task scheduler](https://www.windowscentral.com/how-create-automated-task-using-task-scheduler-windows-10) on Windows or [Cron](https://en.wikipedia.org/wiki/Cron) on Mac, Linux (e.g with `@reboot khoj`) Note: To start Khoj automatically in the background use [Task scheduler](https://www.windowscentral.com/how-create-automated-task-using-task-scheduler-windows-10) on Windows or [Cron](https://en.wikipedia.org/wiki/Cron) on Mac, Linux (e.g with `@reboot khoj`)
@ -73,6 +73,9 @@ pip install --upgrade --pre khoj-assistant
## Uninstall ## Uninstall
1. (Optional) Hit `Ctrl-C` in the terminal running the khoj server to stop it 1. (Optional) Hit `Ctrl-C` in the terminal running the khoj server to stop it
2. Delete the khoj directory in your home folder (i.e `~/.khoj` on Linux, Mac or `C:\Users\<your-username>\.khoj` on Windows) 2. Delete the khoj directory in your home folder (i.e `~/.khoj` on Linux, Mac or `C:\Users\<your-username>\.khoj` on Windows)
5. You might want to `rm -rf` the following directories:
- `~/.khoj`
- `~/.cache/gpt4all`
3. Uninstall the khoj server with `pip uninstall khoj-assistant` 3. Uninstall the khoj server with `pip uninstall khoj-assistant`
4. (Optional) Uninstall khoj.el or the khoj obsidian plugin in the standard way on Emacs, Obsidian 4. (Optional) Uninstall khoj.el or the khoj obsidian plugin in the standard way on Emacs, Obsidian

View file

@ -4,7 +4,7 @@
"version": "0.11.4", "version": "0.11.4",
"minAppVersion": "0.15.0", "minAppVersion": "0.15.0",
"description": "An AI Personal Assistant for your Digital Brain", "description": "An AI Personal Assistant for your Digital Brain",
"author": "Debanjum Singh Solanky", "author": "Khoj",
"authorUrl": "https://github.com/debanjum", "authorUrl": "https://github.com/khoj-ai/",
"isDesktopOnly": true "isDesktopOnly": true
} }

View file

@ -46,7 +46,6 @@ dependencies = [
"tenacity >= 8.2.2", "tenacity >= 8.2.2",
"pillow == 9.3.0", "pillow == 9.3.0",
"pydantic >= 1.10.10", "pydantic >= 1.10.10",
"pyside6 >= 6.5.1",
"pyyaml == 6.0", "pyyaml == 6.0",
"rich >= 13.3.1", "rich >= 13.3.1",
"schedule == 1.1.0", "schedule == 1.1.0",

View file

@ -1,75 +0,0 @@
# Standard Packages
import webbrowser
import os
import signal
# External Packages
from PySide6 import QtGui, QtWidgets
from PySide6.QtCore import Qt
# Internal Packages
from khoj.utils import constants
from PySide6.QtCore import QThread
class ServerThread(QThread):
def __init__(self, start_server_func, parent=None):
super(ServerThread, self).__init__(parent)
self.start_server_func = start_server_func
def __del__(self):
self.wait()
def run(self):
self.start_server_func()
def exit(self):
os.kill(os.getpid(), signal.SIGTERM)
super(ServerThread, self).exit()
class MainWindow(QtWidgets.QMainWindow):
"""Create Window to Navigate users to the web UI"""
def __init__(self, host: str, port: int):
super(MainWindow, self).__init__()
# Initialize Configure Window
self.setWindowTitle("Khoj")
# Set Window Icon
icon_path = constants.web_directory / "assets/icons/favicon-128x128.png"
self.setWindowIcon(QtGui.QIcon(f"{icon_path.absolute()}"))
# Initialize Configure Window Layout
self.wlayout = QtWidgets.QVBoxLayout()
# Add a Label that says "Khoj Configuration" to the Window
self.wlayout.addWidget(QtWidgets.QLabel("Welcome to Khoj"))
# Add a Button to open the Web UI at http://host:port/config
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
# to take up all the space in the window by default.
self.config_window = QtWidgets.QWidget()
self.config_window.setLayout(self.wlayout)
self.setCentralWidget(self.config_window)
self.position_window()
def position_window(self):
"Position the window at center of X axis and near top on Y axis"
window_rectangle = self.geometry()
screen_center = self.screen().availableGeometry().center()
window_rectangle.moveCenter(screen_center)
self.move(window_rectangle.topLeft().x(), 25)
def show_on_top(self):
"Bring Window on Top"
self.show()
self.setWindowState(Qt.WindowState.WindowActive)
self.activateWindow() # For Bringing to Top on Windows
self.raise_() # For Bringing to Top from Minimized State on OSX

View file

@ -1,43 +0,0 @@
# Standard Packages
import webbrowser
# External Packages
from PySide6 import QtGui, QtWidgets
# Internal Packages
from khoj.utils import constants, state
from khoj.interface.desktop.main_window import MainWindow
def create_system_tray(gui: QtWidgets.QApplication, main_window: MainWindow):
"""Create System Tray with Menu. Menu contain options to
1. Open Search Page on the Web Interface
2. Open App Configuration Screen
3. Quit Application
"""
# Create the system tray with icon
icon_path = constants.web_directory / "assets/icons/favicon-128x128.png"
icon = QtGui.QIcon(f"{icon_path.absolute()}")
tray = QtWidgets.QSystemTrayIcon(icon)
tray.setVisible(True)
# Create the menu and menu actions
menu = QtWidgets.QMenu()
menu_actions = [
("Search", lambda: webbrowser.open(f"http://{state.host}:{state.port}/")),
("Configure", lambda: webbrowser.open(f"http://{state.host}:{state.port}/config")),
("App", main_window.show),
("Quit", gui.quit),
]
# Add the menu actions to the menu
for action_text, action_function in menu_actions:
menu_action = QtGui.QAction(action_text, menu)
menu_action.triggered.connect(action_function) # type: ignore[attr-defined]
menu.addAction(menu_action)
# Add the menu to the system tray
tray.setContextMenu(menu)
return tray

View file

@ -1,6 +1,5 @@
# Standard Packages # Standard Packages
import os import os
import signal
import sys import sys
import locale import locale
@ -12,8 +11,6 @@ if sys.stderr is None:
import logging import logging
import threading import threading
import warnings import warnings
from platform import system
import webbrowser
from importlib.metadata import version from importlib.metadata import version
# Ignore non-actionable warnings # Ignore non-actionable warnings
@ -70,81 +67,13 @@ def run():
logger.info("🌘 Starting Khoj") logger.info("🌘 Starting Khoj")
if not args.gui: # Setup task scheduler
# Setup task scheduler poll_task_scheduler()
poll_task_scheduler()
# Start Server # Start Server
configure_routes(app) configure_routes(app)
initialize_server(args.config, required=False) initialize_server(args.config, required=False)
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:
from PySide6 import QtWidgets
from PySide6.QtCore import QTimer
from khoj.interface.desktop.main_window import MainWindow, ServerThread
from khoj.interface.desktop.system_tray import create_system_tray
# Setup GUI
gui = QtWidgets.QApplication([])
main_window = MainWindow(args.host, args.port)
# System tray is only available on Windows, MacOS.
# On Linux (Gnome) the System tray is not supported.
# Since only the Main Window is available
# Quitting it should quit the application
if system() in ["Windows", "Darwin"]:
gui.setQuitOnLastWindowClosed(False)
tray = create_system_tray(gui, main_window)
tray.show()
# Setup Server
initialize_server(args.config, required=False)
configure_routes(app)
server = ServerThread(start_server_func=lambda: start_server(app, host=args.host, port=args.port), parent=gui)
url = f"http://{args.host}:{args.port}"
logger.info(f"🌗 Khoj is running at {url}")
try:
startup_url = url if args.config else f"{url}/config"
webbrowser.open(startup_url)
except:
logger.warning(f"🚧 Unable to open browser. Please open {url} manually to configure or use Khoj.")
# Show Main Window on First Run Experience or if on Linux
if args.config is None or system() not in ["Windows", "Darwin"]:
main_window.show()
# Setup Signal Handlers
signal.signal(signal.SIGINT, sigint_handler)
# Invoke Python interpreter every 500ms to handle signals, run scheduled tasks
timer = QTimer()
timer.start(500)
timer.timeout.connect(schedule.run_pending)
# Start Application
server.start()
gui.aboutToQuit.connect(server.exit)
# Close Splash Screen if still open
if system() != "Darwin":
try:
import pyi_splash
# Update the text on the splash screen
pyi_splash.update_text("Khoj setup complete")
# Close Splash Screen
pyi_splash.close()
except:
pass
gui.exec()
def sigint_handler(*args):
from PySide6 import QtWidgets
QtWidgets.QApplication.quit()
def set_state(args): def set_state(args):
@ -171,12 +100,3 @@ def poll_task_scheduler():
timer_thread.daemon = True timer_thread.daemon = True
timer_thread.start() timer_thread.start()
schedule.run_pending() schedule.run_pending()
def run_gui():
sys.argv += ["--gui"]
run()
if __name__ == "__main__":
run_gui()

View file

@ -17,7 +17,6 @@ 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("--gui", action="store_true", default=False, help="Show native desktop GUI. Default: false")
parser.add_argument( parser.add_argument(
"--regenerate", "--regenerate",
action="store_true", action="store_true",

View file

@ -106,11 +106,6 @@ def load_model(
return model return model
def is_pyinstaller_app():
"Returns true if the app is running from Native GUI created by PyInstaller"
return getattr(sys, "frozen", False) and hasattr(sys, "_MEIPASS")
def get_class_by_name(name: str) -> object: def get_class_by_name(name: str) -> object:
"Returns the class object from name string" "Returns the class object from name string"
module_name, class_name = name.rsplit(".", 1) module_name, class_name = name.rsplit(".", 1)

View file

@ -16,7 +16,6 @@ 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.gui == False
assert actual_args.verbose == 0 assert actual_args.verbose == 0
@ -36,11 +35,10 @@ 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", "--gui", "-vvv"]) actual_args = cli(["-c=tests/data/config.yml", "--regenerate", "-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.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