From a08af8473af51ccfd30648b544c605937e3e6ef6 Mon Sep 17 00:00:00 2001 From: Rodolfo Olivieri Date: Fri, 18 Oct 2024 15:24:34 -0300 Subject: [PATCH 1/3] Rework config file handler and history Introduced two new modules, one for handling code related to config files The second, to handle history where we read and append to new history. Dropped the support for pypyaml as well. We will move on with tomllib --- command_line_assistant/__main__.py | 20 ++-- command_line_assistant/config.py | 132 ++++++++++++++++++++++++++ command_line_assistant/handlers.py | 68 +++---------- command_line_assistant/history.py | 16 ++-- config.toml | 16 ++++ packaging/command-line-assistant.spec | 43 +++++++++ packaging/shellai.spec | 43 +++++++++ pdm.lock | 2 +- pyproject.toml | 1 - 9 files changed, 260 insertions(+), 81 deletions(-) create mode 100644 command_line_assistant/config.py create mode 100644 config.toml create mode 100644 packaging/command-line-assistant.spec create mode 100644 packaging/shellai.spec diff --git a/command_line_assistant/__main__.py b/command_line_assistant/__main__.py index 2b47464..8980aa3 100644 --- a/command_line_assistant/__main__.py +++ b/command_line_assistant/__main__.py @@ -3,12 +3,13 @@ import os import sys +from command_line_assistant.config import CONFIG_DEFAULT_PATH, load_config_file from command_line_assistant.handlers import ( handle_history_write, handle_query, handle_script_session, ) -from command_line_assistant.utils import read_stdin, read_yaml_config +from command_line_assistant.utils import read_stdin logging.basicConfig( level=logging.INFO, @@ -32,7 +33,7 @@ def get_args(): ) parser.add_argument( "--config", - default=os.getenv("COMMAND_LINE_ASSISTANT_CONFIG", "config.yaml"), + default=CONFIG_DEFAULT_PATH, help="Path to the config file.", ) @@ -57,17 +58,10 @@ def get_args(): def main(): parser, args = get_args() - config = read_yaml_config(args.config) - if not config: - logging.warning( - "Config file not found. Script will continue with default values." - ) + config = load_config_file(args.config) - output_capture_conf = config.get("output_capture", {}) - enforce_script_session = output_capture_conf.get("enforce_script", False) - output_file = output_capture_conf.get( - "output_file", "/tmp/command-line-assistant_output.txt" - ) + enforce_script_session = config.output.enforce_script + output_file = config.output.enforce_script if enforce_script_session and (not args.record or not os.path.exists(output_file)): parser.error( @@ -81,7 +75,7 @@ def main(): exit(0) if args.history_clear: logging.info("Clearing history of conversation") - handle_history_write(config.get("history", {}), [], "") + handle_history_write(config, [], "") if args.query_string: handle_query(args.query_string, config) diff --git a/command_line_assistant/config.py b/command_line_assistant/config.py new file mode 100644 index 0000000..cafeed9 --- /dev/null +++ b/command_line_assistant/config.py @@ -0,0 +1,132 @@ +import json +import logging +import os +from collections import namedtuple + +try: + import tomllib +except ImportError: + # Needed for versions below RHEL 10. + import tomli as tomllib + +CONFIG_DEFAULT_PATH: str = "~/.config/shellai/config.toml" + +# tomllib does not support writting files, so we will create our own. +CONFIG_TEMPLATE = """ +[output] +# otherwise recording via script session will be enforced +enforce_script = {enforce_script} +# file with output(s) of regular commands (e.g. ls, echo, etc.) +file = "{output_file}" +# Keep non-empty if your file contains only output of commands (not prompt itself) +prompt_separator = "{prompt_separator}" + +[history] +enabled = {enabled} +file = "{history_file}" +# max number of queries in history (including responses) +max_size = {max_size} + +[backend] +endpoint = "{endpoint}" + +""" + + +class Output(namedtuple("Output", ["enforce_script", "file", "prompt_separator"])): + __slots__ = () + + def __new__( + cls, + enforce_script: bool = False, + file: str = "/tmp/shellai_output.txt", + prompt_separator: str = "$", + ): + return super(Output, cls).__new__(cls, enforce_script, file, prompt_separator) + + +class History(namedtuple("History", ["enabled", "file", "max_size"])): + __slots__ = () + + def __new__( + cls, + enabled: bool = True, + file: str = "/tmp/shellai_output.txt", + max_size: int = 100, + ): + return super(History, cls).__new__(cls, enabled, file, max_size) + + +class Backend(namedtuple("Backend", ["endpoint"])): + endpoint: str = "http://0.0.0.0:8080/v1/query/" + __slots__ = () + + def __new__( + cls, + endpoint: str = "http://0.0.0.0:8080/v1/query/", + ): + return super(Backend, cls).__new__(cls, endpoint) + + +class Config: + def __init__(self, output: dict, history: dict, backend: dict) -> None: + self.output: Output = Output(**output) + self.history: History = History(**history) + self.backend: Backend = Backend(**backend) + + +def _create_config_file(config_path: str) -> None: + """Create a new configuration file with default values.""" + config_dir = os.path.dirname(config_path) + logging.info(f"Creating new config file at {config_path}") + os.makedirs(config_dir, mode=0o755, exist_ok=True) + base_config = Config(Output()._asdict(), History()._asdict(), Backend()._asdict()) + + with open(config_path, mode="w") as handler: + mapping = { + "enforce_script": json.dumps(base_config.output.enforce_script), + "output_file": base_config.output.file, + "prompt_separator": base_config.output.prompt_separator, + "enabled": json.dumps(base_config.history.enabled), + "history_file": base_config.history.file, + "max_size": base_config.history.max_size, + "endpoint": base_config.backend.endpoint, + } + config_formatted = CONFIG_TEMPLATE.format_map(mapping) + handler.write(config_formatted) + + +def _read_config_file(config_path: str) -> Config: + """Read configuration file.""" + config_dict = {} + try: + with open(config_path, mode="rb") as handler: + config_dict = tomllib.load(handler) + except FileNotFoundError as ex: + logging.error(ex) + + # Normalize filepaths + config_dict["history"]["file"] = os.path.expanduser(config_dict["history"]["file"]) + config_dict["output"]["file"] = os.path.expanduser(config_dict["output"]["file"]) + + return Config( + output=config_dict["output"], + history=config_dict["history"], + backend=config_dict["backend"], + ) + + +def load_config_file(config_path: str) -> Config: + """Load configuration file for shellai. + + If the user specifies a path where no config file is located, we will create one with default values. + """ + config_file = os.path.expanduser(config_path) + # Handle case where the user initiates a config file in current dir. + if not os.path.dirname(config_file): + config_file = os.path.join(os.path.curdir, config_file) + + if not os.path.exists(config_file): + _create_config_file(config_file) + + return _read_config_file(config_file) diff --git a/command_line_assistant/handlers.py b/command_line_assistant/handlers.py index 29e6e8e..cf22990 100644 --- a/command_line_assistant/handlers.py +++ b/command_line_assistant/handlers.py @@ -4,51 +4,11 @@ import requests +from command_line_assistant.config import Config +from command_line_assistant.history import handle_history_read, handle_history_write from command_line_assistant.utils import get_payload -def _handle_history_read(config: dict) -> dict: - """ - Reads the history from a file and returns it as a list of dictionaries. - """ - if not config.get("enabled", False): - return [] - - filepath = config.get("filepath", "/tmp/command-line-assistant_history.json") - if not filepath or not os.path.exists(filepath): - logging.warning(f"History file {filepath} does not exist.") - logging.warning("File will be created with first response.") - return [] - - max_size = config.get("max_size", 100) - history = [] - try: - with open(filepath, "r") as f: - history = json.load(f) - except json.JSONDecodeError as e: - logging.error(f"Failed to read history file {filepath}: {e}") - return [] - - logging.info(f"Taking maximum of {max_size} entries from history.") - return history[:max_size] - - -def handle_history_write(config: dict, history: list, response: str) -> None: - """ - Writes the history to a file. - """ - if not config.get("enabled", False): - return - filepath = config.get("filepath", "/tmp/command-line-assistant_history.json") - if response: - history.append({"role": "assistant", "content": response}) - try: - with open(filepath, "w") as f: - json.dump(history, f) - except json.JSONDecodeError as e: - logging.error(f"Failed to write history file {filepath}: {e}") - - def handle_script_session(command_line_assistant_tmp_file) -> None: """ Starts a 'script' session and writes the PID to a file, but leaves control of the terminal to the user. @@ -65,17 +25,14 @@ def handle_script_session(command_line_assistant_tmp_file) -> None: os.remove(command_line_assistant_tmp_file) -def _handle_caret(query: str, config: dict) -> str: +def _handle_caret(query: str, config: Config) -> str: """ Replaces caret (^) with command output specified in config file. """ if "^" not in query: return query - output_capture_settings = config.get("output_capture_settings", {}) - captured_output_file = output_capture_settings.get( - "captured_output_file", "/tmp/command-line-assistant_output.txt" - ) + captured_output_file = config.output.file if not os.path.exists(captured_output_file): logging.error( @@ -83,36 +40,33 @@ def _handle_caret(query: str, config: dict) -> str: ) exit(1) - prompt_separator = output_capture_settings.get("prompt_separator", "$") + prompt_separator = config.output.prompt_separator with open(captured_output_file, "r") as f: # NOTE: takes only last command + output from file output = f.read().split(prompt_separator)[-1].strip() + query = query.replace("^", "") query = f"Context data: {output}\nQuestion: " + query return query -def handle_query(query: str, config: dict) -> None: +def handle_query(query: str, config: Config) -> None: query = _handle_caret(query, config) # NOTE: Add more query handling here logging.info(f"Query: {query}") - backend_service = config.get("backend_service", {}) - query_endpoint = backend_service.get( - "query_endpoint", "http://0.0.0.0:8080/v1/query/" - ) + query_endpoint = config.backend.endpoint try: - history_conf = config.get("history", {}) - history = _handle_history_read(history_conf) + history = handle_history_read(config) payload = get_payload(query) logging.info("Waiting for response from AI...") response = requests.post( query_endpoint, headers={"Content-Type": "application/json"}, data=json.dumps(payload), - timeout=30, # waiting for more than 30 seconds does not make sense + timeout=320, # waiting for more than 30 seconds does not make sense ) response.raise_for_status() completion = response.json() @@ -125,7 +79,7 @@ def handle_query(query: str, config: dict) -> None: "\n\nReferences:\n" + "\n".join(references) if references else "" ) handle_history_write( - history_conf, + config, [ *history, {"role": "user", "content": query}, diff --git a/command_line_assistant/history.py b/command_line_assistant/history.py index dc7db34..a0c0364 100644 --- a/command_line_assistant/history.py +++ b/command_line_assistant/history.py @@ -1,5 +1,6 @@ import json import logging +import os from command_line_assistant.config import Config @@ -12,7 +13,7 @@ def handle_history_read(config: Config) -> dict: return [] filepath = config.history.file - if not filepath or not filepath.exists(): + if not filepath or not os.path.exists(filepath): logging.warning(f"History file {filepath} does not exist.") logging.warning("File will be created with first response.") return [] @@ -20,8 +21,8 @@ def handle_history_read(config: Config) -> dict: max_size = config.history.max_size history = [] try: - data = filepath.read_text() - history = json.loads(data) + with open(filepath, "r") as f: + history = json.load(f) except json.JSONDecodeError as e: logging.error(f"Failed to read history file {filepath}: {e}") return [] @@ -36,15 +37,12 @@ def handle_history_write(config: Config, history: list, response: str) -> None: """ if not config.history.enabled: return - filepath = config.history.file - filepath.makedirs(mode=0o755) - + os.makedirs(os.path.dirname(filepath), mode=0o755, exist_ok=True) if response: history.append({"role": "assistant", "content": response}) - try: - data = json.dumps(history) - filepath.write_text(data) + with open(filepath, "w") as f: + json.dump(history, f) except json.JSONDecodeError as e: logging.error(f"Failed to write history file {filepath}: {e}") diff --git a/config.toml b/config.toml new file mode 100644 index 0000000..c0c32fd --- /dev/null +++ b/config.toml @@ -0,0 +1,16 @@ +[output] +# otherwise recording via script session will be enforced +enforce_script = false +# file with output(s) of regular commands (e.g. ls, echo, etc.) +file = "/tmp/shellai_output.txt" +# Keep non-empty if your file contains only output of commands (not prompt itself) +prompt_separator = "$" + +[history] +enabled = true +file = "~/.local/share/shellai/shellai_history.json" +# max number of queries in history (including responses) +max_size = 100 + +[backend] +endpoint = "http://0.0.0.0:8080/v1/query/" diff --git a/packaging/command-line-assistant.spec b/packaging/command-line-assistant.spec new file mode 100644 index 0000000..29a40bd --- /dev/null +++ b/packaging/command-line-assistant.spec @@ -0,0 +1,43 @@ +Name: command-line-assistant +Version: 0.1.0 +Release: 1%{?dist} +Summary: A simple wrapper to interact with RAG + +License: Apache-2.0 +URL: https://github.com/rhel-lightspeed/command-line-assistant +Source0: %{url}/archive/v%{version}/%{name}-%{version}.tar.gz +# noarch because there is no extension module for this package. +BuildArch: noarch + +BuildRequires: python3-devel +BuildRequires: python3-setuptools +# Not need after RHEL 10 as it is native in Python 3.11+ +%if 0%{?rhel} && 0%{?rhel} < 10 +BuildRequires: python3-tomli +%endif + +Requires: python3-requests +Requires: python3-pyyaml + +%description +A simple wrapper to interact with RAG + +%prep +%autosetup -n %{name}-%{version} + +%build +%py3_build + +%install +# TODO(r0x0d): Create config file +%py3_install + +%files +%doc README.md +%license LICENSE +%{python3_sitelib}/%{name}/ +%{python3_sitelib}/%{name}-*.egg-info/ +# Our binary is just called "c" +%{_bindir}/c + +%changelog diff --git a/packaging/shellai.spec b/packaging/shellai.spec new file mode 100644 index 0000000..2e2c69a --- /dev/null +++ b/packaging/shellai.spec @@ -0,0 +1,43 @@ +Name: shellai +Version: 0.1.0 +Release: 1%{?dist} +Summary: A simple wrapper to interact with RAG + +License: Apache-2.0 +URL: https://github.com/rhel-lightspeed/shellai +Source0: %{url}/archive/v%{version}/%{name}-%{version}.tar.gz +# noarch because there is no extension module for this package. +BuildArch: noarch + +BuildRequires: python3-devel +BuildRequires: python3-setuptools +# Not need after RHEL 10 as it is native in Python 3.11+ +%if 0%{?rhel} && 0%{?rhel} < 10 +BuildRequires: python3-tomli +%endif + +Requires: python3-requests +%if 0%{?rhel} && 0%{?rhel} < 10 +BuildRequires: python3-tomli +%endif + +%description +A simple wrapper to interact with RAG + +%prep +%autosetup -n %{name}-%{version} + +%build +%py3_build + +%install +%py3_install + +%files +%doc README.md +%license LICENSE +%{python3_sitelib}/%{name}/ +%{python3_sitelib}/%{name}-*.egg-info/ +%{_bindir}/%{name} + +%changelog diff --git a/pdm.lock b/pdm.lock index 3613dd2..f17be34 100644 --- a/pdm.lock +++ b/pdm.lock @@ -5,7 +5,7 @@ groups = ["default", "dev"] strategy = ["inherit_metadata"] lock_version = "4.5.0" -content_hash = "sha256:342770e4b536cc0e89e16ea256ab5dad928023b9f37e12c710c6f1b0b59b5d19" +content_hash = "sha256:f5966a52f460682783dd535f53b290a305bb0c8260fd6ea49ef0bd8de4314f6a" [[metadata.targets]] requires_python = ">=3.9" diff --git a/pyproject.toml b/pyproject.toml index 9bf9d0c..52697ce 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,7 +13,6 @@ dependencies = [ # tomli is only required below 3.11 as it is native after that version. 'tomli; python_version<"3.11"', "requests", - "pyyaml", ] requires-python = ">=3.9" # RHEL 9 and 10 readme = "README.md" From ae447525767b13ce202c9e993f4bece1141e23a5 Mon Sep 17 00:00:00 2001 From: Rodolfo Olivieri Date: Mon, 21 Oct 2024 14:30:12 -0300 Subject: [PATCH 2/3] Fixes after review --- command_line_assistant/__main__.py | 9 +- command_line_assistant/config.py | 124 +++++++++++++++----------- command_line_assistant/handlers.py | 2 +- command_line_assistant/history.py | 16 ++-- command_line_assistant/utils.py | 10 +++ packaging/command-line-assistant.spec | 15 ++-- packaging/shellai.spec | 43 --------- setup.py | 2 +- 8 files changed, 112 insertions(+), 109 deletions(-) delete mode 100644 packaging/shellai.spec diff --git a/command_line_assistant/__main__.py b/command_line_assistant/__main__.py index 8980aa3..1f80e39 100644 --- a/command_line_assistant/__main__.py +++ b/command_line_assistant/__main__.py @@ -3,7 +3,11 @@ import os import sys -from command_line_assistant.config import CONFIG_DEFAULT_PATH, load_config_file +from command_line_assistant import utils +from command_line_assistant.config import ( + CONFIG_DEFAULT_PATH, + load_config_file, +) from command_line_assistant.handlers import ( handle_history_write, handle_query, @@ -58,7 +62,8 @@ def get_args(): def main(): parser, args = get_args() - config = load_config_file(args.config) + config_file = utils.expand_user_path(args.config) + config = load_config_file(config_file) enforce_script_session = config.output.enforce_script output_file = config.output.enforce_script diff --git a/command_line_assistant/config.py b/command_line_assistant/config.py index cafeed9..d9754c3 100644 --- a/command_line_assistant/config.py +++ b/command_line_assistant/config.py @@ -1,18 +1,21 @@ import json import logging -import os from collections import namedtuple +from pathlib import Path +# tomllib is available in the stdlib after Python3.11. Before that, we import +# from tomli. try: import tomllib except ImportError: - # Needed for versions below RHEL 10. import tomli as tomllib -CONFIG_DEFAULT_PATH: str = "~/.config/shellai/config.toml" +from command_line_assistant import utils + +CONFIG_DEFAULT_PATH: Path = Path("~/.config/shellai/config.toml") # tomllib does not support writting files, so we will create our own. -CONFIG_TEMPLATE = """ +CONFIG_TEMPLATE = """\ [output] # otherwise recording via script session will be enforced enforce_script = {enforce_script} @@ -29,86 +32,112 @@ [backend] endpoint = "{endpoint}" - """ -class Output(namedtuple("Output", ["enforce_script", "file", "prompt_separator"])): +class OutputSchema( + namedtuple("Output", ["enforce_script", "file", "prompt_separator"]) +): + """This class represents the [output] section of our config.toml file.""" + + # Locking down against extra fields at runtime __slots__ = () + # We are overriding __new__ here because namedtuple only offers default values to fields from Python 3.7+ def __new__( cls, enforce_script: bool = False, file: str = "/tmp/shellai_output.txt", prompt_separator: str = "$", ): - return super(Output, cls).__new__(cls, enforce_script, file, prompt_separator) + file = utils.expand_user_path(file) + return super(OutputSchema, cls).__new__( + cls, enforce_script, file, prompt_separator + ) -class History(namedtuple("History", ["enabled", "file", "max_size"])): +class HistorySchema(namedtuple("History", ["enabled", "file", "max_size"])): + """This class represents the [history] section of our config.toml file.""" + + # Locking down against extra fields at runtime __slots__ = () + # We are overriding __new__ here because namedtuple only offers default values to fields from Python 3.7+ def __new__( cls, enabled: bool = True, - file: str = "/tmp/shellai_output.txt", + file: str = "~/.local/share/shellai/shellai_history.json", max_size: int = 100, ): - return super(History, cls).__new__(cls, enabled, file, max_size) + file = utils.expand_user_path(file) + return super(HistorySchema, cls).__new__(cls, enabled, file, max_size) -class Backend(namedtuple("Backend", ["endpoint"])): - endpoint: str = "http://0.0.0.0:8080/v1/query/" +class BackendSchema(namedtuple("Backend", ["endpoint"])): + """This class represents the [backend] section of our config.toml file.""" + + # Locking down against extra fields at runtime __slots__ = () + # We are overriding __new__ here because namedtuple only offers default values to fields from Python 3.7+ def __new__( cls, endpoint: str = "http://0.0.0.0:8080/v1/query/", ): - return super(Backend, cls).__new__(cls, endpoint) + return super(BackendSchema, cls).__new__(cls, endpoint) class Config: + """Class that holds our configuration file representation. + + With this class, after being initialized, one can access their fields like: + + >>> config = Config() + >>> config.output.enforce_script + + The currently available top-level fields are: + * output = Match the `py:Output` class and their fields + * history = Match the `py:History` class and their fields + * backend = Match the `py:backend` class and their fields + """ + def __init__(self, output: dict, history: dict, backend: dict) -> None: - self.output: Output = Output(**output) - self.history: History = History(**history) - self.backend: Backend = Backend(**backend) + self.output: OutputSchema = OutputSchema(**output) + self.history: HistorySchema = HistorySchema(**history) + self.backend: BackendSchema = BackendSchema(**backend) -def _create_config_file(config_path: str) -> None: +def _create_config_file(config_file: Path) -> None: """Create a new configuration file with default values.""" - config_dir = os.path.dirname(config_path) - logging.info(f"Creating new config file at {config_path}") - os.makedirs(config_dir, mode=0o755, exist_ok=True) - base_config = Config(Output()._asdict(), History()._asdict(), Backend()._asdict()) - - with open(config_path, mode="w") as handler: - mapping = { - "enforce_script": json.dumps(base_config.output.enforce_script), - "output_file": base_config.output.file, - "prompt_separator": base_config.output.prompt_separator, - "enabled": json.dumps(base_config.history.enabled), - "history_file": base_config.history.file, - "max_size": base_config.history.max_size, - "endpoint": base_config.backend.endpoint, - } - config_formatted = CONFIG_TEMPLATE.format_map(mapping) - handler.write(config_formatted) - - -def _read_config_file(config_path: str) -> Config: + + logging.info(f"Creating new config file at {config_file.parent}") + config_file.parent.mkdir(mode=0o755) + base_config = Config( + OutputSchema()._asdict(), HistorySchema()._asdict(), BackendSchema()._asdict() + ) + + mapping = { + "enforce_script": json.dumps(base_config.output.enforce_script), + "output_file": base_config.output.file, + "prompt_separator": base_config.output.prompt_separator, + "enabled": json.dumps(base_config.history.enabled), + "history_file": base_config.history.file, + "max_size": base_config.history.max_size, + "endpoint": base_config.backend.endpoint, + } + config_formatted = CONFIG_TEMPLATE.format_map(mapping) + config_file.write_text(config_formatted) + + +def _read_config_file(config_file: Path) -> Config: """Read configuration file.""" config_dict = {} try: - with open(config_path, mode="rb") as handler: - config_dict = tomllib.load(handler) + data = config_file.read_text() + config_dict = tomllib.loads(data) except FileNotFoundError as ex: logging.error(ex) - # Normalize filepaths - config_dict["history"]["file"] = os.path.expanduser(config_dict["history"]["file"]) - config_dict["output"]["file"] = os.path.expanduser(config_dict["output"]["file"]) - return Config( output=config_dict["output"], history=config_dict["history"], @@ -116,17 +145,12 @@ def _read_config_file(config_path: str) -> Config: ) -def load_config_file(config_path: str) -> Config: +def load_config_file(config_file: Path) -> Config: """Load configuration file for shellai. If the user specifies a path where no config file is located, we will create one with default values. """ - config_file = os.path.expanduser(config_path) - # Handle case where the user initiates a config file in current dir. - if not os.path.dirname(config_file): - config_file = os.path.join(os.path.curdir, config_file) - - if not os.path.exists(config_file): + if not config_file.exists(): _create_config_file(config_file) return _read_config_file(config_file) diff --git a/command_line_assistant/handlers.py b/command_line_assistant/handlers.py index cf22990..655d372 100644 --- a/command_line_assistant/handlers.py +++ b/command_line_assistant/handlers.py @@ -66,7 +66,7 @@ def handle_query(query: str, config: Config) -> None: query_endpoint, headers={"Content-Type": "application/json"}, data=json.dumps(payload), - timeout=320, # waiting for more than 30 seconds does not make sense + timeout=30, # waiting for more than 30 seconds does not make sense ) response.raise_for_status() completion = response.json() diff --git a/command_line_assistant/history.py b/command_line_assistant/history.py index a0c0364..dc7db34 100644 --- a/command_line_assistant/history.py +++ b/command_line_assistant/history.py @@ -1,6 +1,5 @@ import json import logging -import os from command_line_assistant.config import Config @@ -13,7 +12,7 @@ def handle_history_read(config: Config) -> dict: return [] filepath = config.history.file - if not filepath or not os.path.exists(filepath): + if not filepath or not filepath.exists(): logging.warning(f"History file {filepath} does not exist.") logging.warning("File will be created with first response.") return [] @@ -21,8 +20,8 @@ def handle_history_read(config: Config) -> dict: max_size = config.history.max_size history = [] try: - with open(filepath, "r") as f: - history = json.load(f) + data = filepath.read_text() + history = json.loads(data) except json.JSONDecodeError as e: logging.error(f"Failed to read history file {filepath}: {e}") return [] @@ -37,12 +36,15 @@ def handle_history_write(config: Config, history: list, response: str) -> None: """ if not config.history.enabled: return + filepath = config.history.file - os.makedirs(os.path.dirname(filepath), mode=0o755, exist_ok=True) + filepath.makedirs(mode=0o755) + if response: history.append({"role": "assistant", "content": response}) + try: - with open(filepath, "w") as f: - json.dump(history, f) + data = json.dumps(history) + filepath.write_text(data) except json.JSONDecodeError as e: logging.error(f"Failed to write history file {filepath}: {e}") diff --git a/command_line_assistant/utils.py b/command_line_assistant/utils.py index b264e74..ae1243a 100644 --- a/command_line_assistant/utils.py +++ b/command_line_assistant/utils.py @@ -2,6 +2,7 @@ import os import select import sys +from pathlib import Path import yaml @@ -37,3 +38,12 @@ def get_payload(query: str) -> dict: # {"role": "user", "content": "how do I enable selinux?"}, payload = {"query": query} return payload + + +def expand_user_path(file_path: str) -> Path: + """Helper method to expand user provided path.""" + path = Path(file_path) + if not path.exists(): + raise FileNotFoundError(f"Current file does not exist or was not found: {path}") + + return Path(path).expanduser() diff --git a/packaging/command-line-assistant.spec b/packaging/command-line-assistant.spec index 29a40bd..84684ce 100644 --- a/packaging/command-line-assistant.spec +++ b/packaging/command-line-assistant.spec @@ -17,7 +17,13 @@ BuildRequires: python3-tomli %endif Requires: python3-requests -Requires: python3-pyyaml +# Not need after RHEL 10 as it is native in Python 3.11+ +%if 0%{?rhel} && 0%{?rhel} < 10 +Requires: python3-tomli +%endif + +%global python_package_src command_line_assistant +%global binary_name c %description A simple wrapper to interact with RAG @@ -29,15 +35,14 @@ A simple wrapper to interact with RAG %py3_build %install -# TODO(r0x0d): Create config file %py3_install %files %doc README.md %license LICENSE -%{python3_sitelib}/%{name}/ -%{python3_sitelib}/%{name}-*.egg-info/ +%{python3_sitelib}/%{python_package_src}/ +%{python3_sitelib}/%{python_package_src}-*.egg-info/ # Our binary is just called "c" -%{_bindir}/c +%{_bindir}/%{binary_name} %changelog diff --git a/packaging/shellai.spec b/packaging/shellai.spec deleted file mode 100644 index 2e2c69a..0000000 --- a/packaging/shellai.spec +++ /dev/null @@ -1,43 +0,0 @@ -Name: shellai -Version: 0.1.0 -Release: 1%{?dist} -Summary: A simple wrapper to interact with RAG - -License: Apache-2.0 -URL: https://github.com/rhel-lightspeed/shellai -Source0: %{url}/archive/v%{version}/%{name}-%{version}.tar.gz -# noarch because there is no extension module for this package. -BuildArch: noarch - -BuildRequires: python3-devel -BuildRequires: python3-setuptools -# Not need after RHEL 10 as it is native in Python 3.11+ -%if 0%{?rhel} && 0%{?rhel} < 10 -BuildRequires: python3-tomli -%endif - -Requires: python3-requests -%if 0%{?rhel} && 0%{?rhel} < 10 -BuildRequires: python3-tomli -%endif - -%description -A simple wrapper to interact with RAG - -%prep -%autosetup -n %{name}-%{version} - -%build -%py3_build - -%install -%py3_install - -%files -%doc README.md -%license LICENSE -%{python3_sitelib}/%{name}/ -%{python3_sitelib}/%{name}-*.egg-info/ -%{_bindir}/%{name} - -%changelog diff --git a/setup.py b/setup.py index 711aae5..096e9e7 100644 --- a/setup.py +++ b/setup.py @@ -28,7 +28,7 @@ long_description=open(pyproject_settings["project"]["readme"]).read(), long_description_content_type="text/markdown", url=pyproject_settings["project"]["urls"]["Repository"], - packages=[pyproject_settings["project"]["name"]], + packages=pyproject_settings["tool"]["setuptools"]["packages"], install_requires=pyproject_settings["project"]["dependencies"], entry_points=entry_points, classifiers=pyproject_settings["project"]["classifiers"], From 5738ade729472abb2ab37e111d4f3b6418c41cc3 Mon Sep 17 00:00:00 2001 From: Rodolfo Olivieri Date: Tue, 29 Oct 2024 12:10:25 -0300 Subject: [PATCH 3/3] Apply suggestions from code review Co-authored-by: Preston Watson --- command_line_assistant/__main__.py | 2 +- packaging/command-line-assistant.spec | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/command_line_assistant/__main__.py b/command_line_assistant/__main__.py index 1f80e39..3e13767 100644 --- a/command_line_assistant/__main__.py +++ b/command_line_assistant/__main__.py @@ -66,7 +66,7 @@ def main(): config = load_config_file(config_file) enforce_script_session = config.output.enforce_script - output_file = config.output.enforce_script + output_file = config.output.file if enforce_script_session and (not args.record or not os.path.exists(output_file)): parser.error( diff --git a/packaging/command-line-assistant.spec b/packaging/command-line-assistant.spec index 84684ce..85af10c 100644 --- a/packaging/command-line-assistant.spec +++ b/packaging/command-line-assistant.spec @@ -11,13 +11,13 @@ BuildArch: noarch BuildRequires: python3-devel BuildRequires: python3-setuptools -# Not need after RHEL 10 as it is native in Python 3.11+ +# Not needed after RHEL 10 as it is native in Python 3.11+ %if 0%{?rhel} && 0%{?rhel} < 10 BuildRequires: python3-tomli %endif Requires: python3-requests -# Not need after RHEL 10 as it is native in Python 3.11+ +# Not needed after RHEL 10 as it is native in Python 3.11+ %if 0%{?rhel} && 0%{?rhel} < 10 Requires: python3-tomli %endif