Skip to content

Commit

Permalink
[RSPEED-279] Add Command Line Assistant Daemon (#58)
Browse files Browse the repository at this point in the history
* Refactor config module

This patch introduces a small refactor to the config module to split it
and handle the different schemas in a different module. The idea behind
this is to keep the modules unclogged and simple.

* Refactor CLI to have separate commands

This patch will introduce a refactor/split of our CLI module. The
intention here is to make sure that each (sub)command will be treated
independently and have their own logic.

Currently, it is being divided into two categories:
* history
* query

The history subcommand will handle everything that is related to the
user history of conversation.

The query subcommand will handle anything that is related to user
queries. This is also the default subcommand.

* [RSPEED-299] Initial setup for clad (#40)

* Initial setup for clad

This patch introduces an initial setup for clad (Command Line Assistant
Daemon).

* Fix unit-tests workflow

* Update CONTRIBUTING.md

* Drop python3-gobject-base from specfile

* Remove version lock from pyproject.toml

* Refactor the build specfile (#56)

In this patch, we refactored how we did the build for the CLA project.
This was needed, because of a couple of reasons:

- We were not including the submodules under `command_line_assistant` source package

- The binaries we were shipping were not working properly with root
-- Any time that we tried to execute the binary through `$ c`, it tried
to load from `lib64` instead of `lib`

To achieve those modifications, we created a new folder called `bin`,
where it has `c` and `clad`.

We also modified the pyproject.toml to include the submodules using the
Find Directive.

* Add authentication with identity certificate (#59)

This was not tested yet, but the code is all here.

* Add the python3-colorama dependency in specfile (#64)

* Change DBUS service name (#63)

* Improve rendering libary (#65)
  • Loading branch information
r0x0d authored Dec 12, 2024
1 parent 9891475 commit 95b28cc
Show file tree
Hide file tree
Showing 89 changed files with 3,146 additions and 955 deletions.
3 changes: 3 additions & 0 deletions .github/workflows/sanity.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ jobs:

- name: Install dependencies
run: |
# TODO(r0x0d): Refactor this https://issues.redhat.com/browse/RSPEED-339
sudo apt update -y
sudo apt install libgirepository1.0-dev gcc libcairo2-dev pkg-config python3-dev gir1.2-gtk-4.0 -y
pdm install -v
pdm info
echo "$(pdm venv --path in-project)/bin" >> $GITHUB_PATH
Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/unit-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ jobs:

- name: Install dependencies
run: |
# TODO(r0x0d): Refactor this https://issues.redhat.com/browse/RSPEED-339
sudo apt update -y
sudo apt install libgirepository1.0-dev gcc libcairo2-dev pkg-config python3-dev gir1.2-gtk-4.0 -y
make install
echo "$(pdm venv --path in-project)/bin" >> $GITHUB_PATH
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,6 @@ htmlcov/
.tox
coverage*
junit.xml

# systemd
data/development/systemd/clad-user.service
3 changes: 3 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@ repos:
- pyright[nodejs]
- pytest
- requests
- dasbus
- setuptools
- responses
- tomli; python_version<"3.11"
- setuptools
- colorama
Expand Down
2 changes: 2 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ Required packages:
- Python 3.9 or greater
- pip

Before installing the dependencies with `pdm`, make sure to follow the installation instructions from [PyGObject](https://pygobject.gnome.org/getting_started.html#fedora-logo-fedora). This is required for running `clad`, and installing the rest of the dependencies.

```sh
make install
```
Expand Down
68 changes: 63 additions & 5 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,6 +1,32 @@
.PHONY: install-tools install install-dev unit-test help
.PHONY:
install-tools \
install \
install-dev \
unit-test \
help \
clean \
link-systemd-units \
unlink-systemd-units \
run-clad status-clad \
reload-clad \

PROJECT_DIR = $(shell pwd)
# Project directory path - /home/<user>/.../command-line-assistant
PROJECT_DIR := $(shell pwd)
DATA_DEVELOPMENT_PATH := $(PROJECT_DIR)/data/development

## Systemd specifics
# https://www.gnu.org/software/make/manual/html_node/Text-Functions.html#index-subst-1
# Virtualenv bin path for clad
CLAD_VENV_BIN := $(subst /,\/,$(PROJECT_DIR)/.venv/bin/clad)
# Systemd development unit
CLAD_SYSTEMD_DEVEL_PATH := $(DATA_DEVELOPMENT_PATH)/systemd/clad-devel.service
# Systemd user unit, which is generated from devel unit
CLAD_SYSTEMD_USER_PATH := $(DATA_DEVELOPMENT_PATH)/systemd/clad-user.service
# Systemd path on the system to place the user unit
SYSTEMD_USER_UNITS := ~/.config/systemd/user
# SYSTEMD_USER_UNITS := /etc/systemd/user
# Path to local XDG_CONFIG_DIRS to load config file
XDG_CONFIG_DIRS := $(subst /,\/,$(DATA_DEVELOPMENT_PATH)/config)

default: help

Expand All @@ -24,7 +50,6 @@ unit-test-coverage: ## Unit test cla with coverage
@pytest --cov --junitxml=junit.xml -o junit_family=legacy
@echo "Tests completed."


coverage: ## Generate coverage report from unit-tests
@coverage xml

Expand All @@ -45,5 +70,38 @@ clean: ## Clean project files
@find . -name '__pycache__' -exec rm -fr {} +
@find . -name '*.pyc' -exec rm -f {} +
@find . -name '*.pyo' -exec rm -f {} +
@rm -rf .pdm-build .ruff_cache .coverage .pdm-python dist .tox junit.xml coverage.xml
@coverage erase
@rm -rf htmlcov \
.pytest_cache \
command_line_assistant.egg-info \
.pdm-build \
.ruff_cache \
.coverage \
.pdm-python \
dist \
.tox \
junit.xml \
coverage.xml

link-systemd-units: ## Link the systemd units to /etc/systemd/user
@echo "Linking the systemd units from $(CLAD_SYSTEMD_DEVEL_PATH) to $(SYSTEMD_USER_UNITS)/clad.service"
@sed -e 's/{{ EXEC_START }}/$(CLAD_VENV_BIN)/' \
-e 's/{{ CONFIG_FILE_PATH }}/$(XDG_CONFIG_DIRS)/' \
$(CLAD_SYSTEMD_DEVEL_PATH) > $(CLAD_SYSTEMD_USER_PATH)
@ln -s $(CLAD_SYSTEMD_USER_PATH) $(SYSTEMD_USER_UNITS)/clad.service

unlink-systemd-units: ## Unlink the systemd units from /etc/systemd/user
@echo "Unlinking the systemd units from $(SYSTEMD_USER_UNITS)/clad.service"
@unlink $(SYSTEMD_USER_UNITS)/clad.service

clad: ## Run clad on the system
@XDG_CONFIG_DIRS=$(XDG_CONFIG_DIRS) $(CLAD_VENV_BIN)

run-clad: ## Run the clad under systemd
@systemctl start --user clad

status-clad: ## Check the status for clad
@systemctl status -f --user clad

reload-clad: ## Reload clad systemd unit
@systemctl --user daemon-reload
@systemctl restart --user clad
14 changes: 14 additions & 0 deletions bin/c
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
#!/usr/bin/env python

import sys

from command_line_assistant import initialize


def main():
"""Main entrypoint for c."""
initialize.initialize()


if __name__ == '__main__':
sys.exit(main())
14 changes: 14 additions & 0 deletions bin/clad
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
#!/usr/bin/env python

import sys

from command_line_assistant.daemon import clad


def main():
"""Main entrypoint for clad."""
return clad.daemonize()


if __name__ == '__main__':
sys.exit(main())
41 changes: 0 additions & 41 deletions command_line_assistant/__main__.py

This file was deleted.

18 changes: 8 additions & 10 deletions command_line_assistant/commands/history.py
Original file line number Diff line number Diff line change
@@ -1,26 +1,26 @@
import logging
from argparse import Namespace
from pathlib import Path

from command_line_assistant.config import Config
from command_line_assistant.history import handle_history_write
from command_line_assistant.utils.cli import BaseCLICommand, SubParsersAction

logger = logging.getLogger(__name__)


class HistoryCommand(BaseCLICommand):
def __init__(self, clear: bool, config: Config) -> None:
def __init__(self, clear: bool) -> None:
self._clear = clear
self._config = config
super().__init__()

def run(self) -> None:
if self._clear:
logger.info("Clearing history of conversation")
handle_history_write(self._config, [], "")
# TODO(r0x0d): Rewrite this.
handle_history_write(Path("/tmp/test_history.json"), [], "")


def register_subcommand(parser: SubParsersAction, config: Config):
def register_subcommand(parser: SubParsersAction):
"""
Register this command to argparse so it's available for the datasets-cli
Expand All @@ -34,10 +34,8 @@ def register_subcommand(parser: SubParsersAction, config: Config):
history_parser.add_argument(
"--clear", action="store_true", help="Clear the history."
)
history_parser.set_defaults(func=_command_factory)

# TODO(r0x0d): This is temporary as it will get removed
history_parser.set_defaults(func=lambda args: _command_factory(args, config))


def _command_factory(args: Namespace, config: Config) -> HistoryCommand:
return HistoryCommand(args.clear, config)
def _command_factory(args: Namespace) -> HistoryCommand:
return HistoryCommand(args.clear)
94 changes: 84 additions & 10 deletions command_line_assistant/commands/query.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,96 @@
from argparse import Namespace

from command_line_assistant.config import Config
from command_line_assistant.handlers import handle_query
from dasbus.error import DBusError

from command_line_assistant.dbus.constants import SERVICE_IDENTIFIER
from command_line_assistant.dbus.definitions import MessageInput, MessageOutput
from command_line_assistant.rendering.decorators.colors import ColorDecorator
from command_line_assistant.rendering.decorators.text import (
EmojiDecorator,
TextWrapDecorator,
WriteOnceDecorator,
)
from command_line_assistant.rendering.renders.spinner import SpinnerRenderer
from command_line_assistant.rendering.renders.text import TextRenderer
from command_line_assistant.rendering.stream import StderrStream, StdoutStream
from command_line_assistant.utils.cli import BaseCLICommand, SubParsersAction

LEGAL_NOTICE = (
"RHEL Lightspeed Command Line Assistant can answer questions related to RHEL."
" Do not include personal or business sensitive information in your input."
"Interactions with RHEL Lightspeed may be reviewed and used to improve our "
"products and service."
)
ALWAYS_LEGAL_MESSAGE = (
"Always check AI/LLM-generated responses for accuracy prior to use."
)


def _initialize_spinner_renderer() -> SpinnerRenderer:
spinner = SpinnerRenderer(
message="Requesting knowledge from AI", stream=StdoutStream(end="")
)

spinner.update(EmojiDecorator(emoji="U+1F916")) # Robot emoji
spinner.update(TextWrapDecorator())

return spinner


def _initialize_text_renderer() -> TextRenderer:
text = TextRenderer(stream=StdoutStream(end="\n"))
text.update(ColorDecorator(foreground="green")) # Robot emoji
text.update(TextWrapDecorator())

return text


def _initialize_legal_renderer(write_once: bool = False) -> TextRenderer:
text = TextRenderer(stream=StderrStream())
text.update(ColorDecorator(foreground="lightyellow"))
text.update(TextWrapDecorator())

if write_once:
text.update(WriteOnceDecorator(state_filename="legal"))

return text


class QueryCommand(BaseCLICommand):
def __init__(self, query_string: str, config: Config) -> None:
def __init__(self, query_string: str) -> None:
self._query = query_string
self._config = config

self._spinner_renderer: SpinnerRenderer = _initialize_spinner_renderer()
self._text_renderer: TextRenderer = _initialize_text_renderer()
self._legal_renderer: TextRenderer = _initialize_legal_renderer(write_once=True)
self._warning_renderer: TextRenderer = _initialize_legal_renderer()

super().__init__()

def run(self) -> None:
handle_query(self._query, self._config)
proxy = SERVICE_IDENTIFIER.get_proxy()

input_query = MessageInput()
input_query.message = self._query

output = "Nothing to see here..."
try:
with self._spinner_renderer:
proxy.ProcessQuery(MessageInput.to_structure(input_query))
output = MessageOutput.from_structure(proxy.RetrieveAnswer).message

self._legal_renderer.render(LEGAL_NOTICE)
self._text_renderer.render(output)
self._warning_renderer.render(ALWAYS_LEGAL_MESSAGE)
except DBusError:
self._text_renderer.update(ColorDecorator(foreground="red"))
self._text_renderer.update(EmojiDecorator(emoji="U+1F641"))
self._text_renderer.render(
"Uh oh... Something went wrong. Try again later."
)


def register_subcommand(parser: SubParsersAction, config: Config) -> None:
def register_subcommand(parser: SubParsersAction) -> None:
"""
Register this command to argparse so it's available for the datasets-cli
Expand All @@ -31,9 +106,8 @@ def register_subcommand(parser: SubParsersAction, config: Config) -> None:
"query_string", nargs="?", help="Query string to be processed."
)

# TODO(r0x0d): This is temporary as it will get removed
query_parser.set_defaults(func=lambda args: _command_factory(args, config))
query_parser.set_defaults(func=_command_factory)


def _command_factory(args: Namespace, config: Config) -> QueryCommand:
return QueryCommand(args.query_string, config)
def _command_factory(args: Namespace) -> QueryCommand:
return QueryCommand(args.query_string)
Loading

0 comments on commit 95b28cc

Please sign in to comment.