From ec93f368a3b7c317b2c21617ec22e2c2880f328d Mon Sep 17 00:00:00 2001 From: VAliquo Date: Sun, 17 Nov 2024 09:54:05 -0500 Subject: [PATCH 01/40] [UPDATE] Command folder name --- {commands => chips}/__init__.py | 0 {commands => chips}/navi_chip_installer.py | 0 {commands => chips}/navi_clear.py | 0 {commands => chips}/navi_exit.py | 0 {commands => chips}/navi_help.py | 0 {commands => chips}/navi_spec.py | 0 {commands => chips}/navi_system.py | 0 7 files changed, 0 insertions(+), 0 deletions(-) rename {commands => chips}/__init__.py (100%) rename {commands => chips}/navi_chip_installer.py (100%) rename {commands => chips}/navi_clear.py (100%) rename {commands => chips}/navi_exit.py (100%) rename {commands => chips}/navi_help.py (100%) rename {commands => chips}/navi_spec.py (100%) rename {commands => chips}/navi_system.py (100%) diff --git a/commands/__init__.py b/chips/__init__.py similarity index 100% rename from commands/__init__.py rename to chips/__init__.py diff --git a/commands/navi_chip_installer.py b/chips/navi_chip_installer.py similarity index 100% rename from commands/navi_chip_installer.py rename to chips/navi_chip_installer.py diff --git a/commands/navi_clear.py b/chips/navi_clear.py similarity index 100% rename from commands/navi_clear.py rename to chips/navi_clear.py diff --git a/commands/navi_exit.py b/chips/navi_exit.py similarity index 100% rename from commands/navi_exit.py rename to chips/navi_exit.py diff --git a/commands/navi_help.py b/chips/navi_help.py similarity index 100% rename from commands/navi_help.py rename to chips/navi_help.py diff --git a/commands/navi_spec.py b/chips/navi_spec.py similarity index 100% rename from commands/navi_spec.py rename to chips/navi_spec.py diff --git a/commands/navi_system.py b/chips/navi_system.py similarity index 100% rename from commands/navi_system.py rename to chips/navi_system.py From 51b8768f0452680fb8f6f3ec856a1c54aa6a3639 Mon Sep 17 00:00:00 2001 From: VAliquo Date: Sun, 17 Nov 2024 12:04:24 -0500 Subject: [PATCH 02/40] [ADD] Chip creator --- chips/SSG/chip-template.txt | 46 ++++++++ chips/SSG/navi_chip_creator.py | 141 +++++++++++++++++++++++++ chips/{ => SSG}/navi_chip_installer.py | 4 +- chips/{ => SSG}/navi_clear.py | 1 - chips/{ => SSG}/navi_exit.py | 1 - chips/{ => SSG}/navi_help.py | 4 +- chips/{ => SSG}/navi_spec.py | 0 chips/{ => SSG}/navi_system.py | 0 chips/__init__.py | 19 +++- navi_internal.py | 10 +- 10 files changed, 210 insertions(+), 16 deletions(-) create mode 100644 chips/SSG/chip-template.txt create mode 100644 chips/SSG/navi_chip_creator.py rename chips/{ => SSG}/navi_chip_installer.py (98%) rename chips/{ => SSG}/navi_clear.py (94%) rename chips/{ => SSG}/navi_exit.py (94%) rename chips/{ => SSG}/navi_help.py (95%) rename chips/{ => SSG}/navi_spec.py (100%) rename chips/{ => SSG}/navi_system.py (100%) diff --git a/chips/SSG/chip-template.txt b/chips/SSG/chip-template.txt new file mode 100644 index 0000000..3ef3365 --- /dev/null +++ b/chips/SSG/chip-template.txt @@ -0,0 +1,46 @@ +import navi_internal + +# Chip documentation: https://github.com/SaintsSec/Navi/wiki/4.-Developing-Chips-%E2%80%90-Indepth + +command: str = "{{CHIP_NAME}}" +use: str = "What does this chip do?" +aliases: list = ['{{CHIP_NAME}}'] +params: dict = { + '-help': 'Display help information', + '-h': 'Display help information', +} + +help_params: tuple = ('-help', '-h') + + +def print_params() -> None: + # Print the header + print(f"{'Parameter':<10} | {'Description'}") + print("-" * 40) + + # Print each dictionary item + for param, description in params.items(): + print(f"{param:<10} | {description}") + +# What Navi calls to run this Chip +def run(arguments=None) -> None: + # Get the instance of Navi. Required to access Navi-specific functions + navi_instance = navi_internal.navi_instance + navi_instance.print_message(f"How can I help you, {navi_instance.get_user()}?") + + # Optional: Converts argument tokens into a list + arg_array = arguments.text.split() + + # Remove the command itself + arg_array.pop(0) + + # Optional: Check for parameters + if arg_array is not None: + for arg in arg_array: + match arg: + case x if x in help_params: + print_params() + return + case _: + navi_instance.print_message(f"Invalid parameter: {arg}") + return diff --git a/chips/SSG/navi_chip_creator.py b/chips/SSG/navi_chip_creator.py new file mode 100644 index 0000000..274c396 --- /dev/null +++ b/chips/SSG/navi_chip_creator.py @@ -0,0 +1,141 @@ +#!/bin/python3 +import navi_internal +import sys +import os +from colorama import Fore + +command: str = "chip-create" +use: str = "Creates a new Navi chip" +aliases: list = ['chip-create', 'cc'] +params: dict = { + '-help': 'Display help information', + '-h': 'Display help information', +} + +help_params: tuple = ('-help', '-h') +template_file: str = "chips/SSG/chip-template.txt" +chip_install_path: str = "chips/Dev/" +chip_file_path: str = "" +chip_documentation_link: str = "https://github.com/SaintsSec/Navi/wiki/4.-Developing-Chips-%E2%80%90-Indepth" + + +def print_params() -> None: + # Print the header + print(f"{'Parameter':<10} | {'Description'}") + print("-" * 40) + + # Print each dictionary item + for param, description in params.items(): + print(f"{param:<10} | {description}") + + +def run(arguments=None) -> None: + navi_instance = navi_internal.navi_instance + arg_array = arguments.text.split() + arg_array.pop(0) + if arg_array is not None: + for arg in arg_array: + match arg: + case x if x in help_params: + print_params() + return + case _: + navi_instance.print_message(f"Invalid parameter: {arg}\n" + f"Do you want to review the available parameters? (y/n): ") + choice = input().strip().lower() + if choice == 'y': + print_params() + return + return + + navi_instance.print_message( + f"Welcome to Navi chip creator, {navi_instance.get_user()}. Please enter the name of your new Chip.") + chip_name = input("Chip Name: ") + + # Ask for the file name + navi_instance.print_message( + f"Great name, {navi_instance.get_user()}! What do you want the python file to be called?") + chip_file_name = input("Chip File Name: ") + while True: + # Recap the entered information + navi_instance.print_message(f"Perfect! Here's a recap:" + f"\nChip Name: {chip_name}" + f"\nPython File Name: {chip_file_name}.py\n") + + while True: # Loop to ensure valid input for continue/make changes + # Ask the user if they want to proceed or make changes + choice = input("Are you ready to proceed or do you want to make changes? " + f"({Fore.YELLOW}c{Fore.RESET})ontinue or ({Fore.YELLOW}m{Fore.RESET})ake changes: ").strip().lower() + + if choice == 'c': + navi_instance.print_message("Awesome! Moving forward with the given details.") + break # Exit the input validation loop + elif choice == 'm': + while True: # Loop to ensure valid input for what to change + # Ask the user what they want to change + change_choice = input(f"What do you want to change? ({Fore.YELLOW}chip{Fore.RESET}) name or " + f"({Fore.YELLOW}file{Fore.RESET}) name: ").strip().lower() + + if change_choice == 'chip': + navi_instance.print_message("Let's update the Chip Name.") + chip_name = input("New Chip Name: ") + break # Exit the loop for change choice + elif change_choice == 'file': + navi_instance.print_message("Let's update the File Name.") + chip_file_name = input("New File Name: ") + break # Exit the loop for change choice + else: + navi_instance.print_message("Invalid choice. Please enter 'chip' or 'file'.") + + break # Exit the input validation loop and recap changes + else: + navi_instance.print_message("Invalid input. Please choose 'c' to continue or 'm' to make changes.") + if choice == 'c': + break + + # Check if the template file exists + if os.path.exists(template_file): + with open(template_file, "r") as template: + template_content = template.read() + + # Replace placeholders in the template if needed (optional) + chip_file_name_final = chip_file_name.replace(".py", "") + ".py" + template_content = template_content.replace("{{CHIP_NAME}}", chip_name) + # Write the content to the new file + chip_file_path = os.path.join(os.getcwd(), chip_install_path + chip_file_name_final) + with open(chip_file_path, "w") as chip_dev: + chip_dev.write(template_content) + + navi_instance.print_message(f"{Fore.GREEN}Chip '{chip_name}' created successfully!{Fore.RESET}") + else: + navi_instance.print_message(f"{Fore.RED}ERROR:{Fore.RESET} Template file '{template_file}' is missing. Aborting.") + + navi_instance.print_message(f"Here are some options for you:\n" + f"{Fore.YELLOW}1{Fore.RESET}: Open directory of Chip location\n" + f"{Fore.YELLOW}2{Fore.RESET}: Open in your preferred code editor\n" + f"{Fore.YELLOW}3{Fore.RESET}: Reload Navi to load the new Chip\n" + f"{Fore.YELLOW}4{Fore.RESET}: Review the documentation online\n" + f"{Fore.YELLOW}5{Fore.RESET} or other value: I'm finished") + choice = input("Please enter 1, 2, 3, 4, or 5: ") + match choice: + case '1': + print("Opening directory of Chip location") + case '2': + print("Opening in your preferred code editor") + import subprocess + try: + if sys.platform == 'win32': + os.startfile(chip_file_path) + elif sys.platform == 'darwin': + subprocess.call(['open', chip_file_path]) + else: + subprocess.call(['xdg-open', chip_file_path]) + except Exception as e: + print(f"Failed to open the log file: {e}") + case '3': + navi_instance.reload() + case '4': + import webbrowser + webbrowser.open(chip_documentation_link) + case _: + return diff --git a/chips/navi_chip_installer.py b/chips/SSG/navi_chip_installer.py similarity index 98% rename from chips/navi_chip_installer.py rename to chips/SSG/navi_chip_installer.py index 98a8823..cef20d1 100644 --- a/chips/navi_chip_installer.py +++ b/chips/SSG/navi_chip_installer.py @@ -83,7 +83,7 @@ def download_and_extract(download_url): return None, None -def copy_files_to_install_path(extracted_dir, install_path="/commands"): +def copy_files_to_install_path(extracted_dir, install_path="/chips/Com"): installed_files = [] try: for item in os.listdir(extracted_dir): @@ -114,7 +114,7 @@ def install_requirements(extracted_dir): os.remove(requirements_path) -def update_script(download_url, install_path="commands"): +def update_script(download_url, install_path="chips/Com"): print("Downloading chip...") extracted_dir, download_guid = download_and_extract(download_url) if extracted_dir: diff --git a/chips/navi_clear.py b/chips/SSG/navi_clear.py similarity index 94% rename from chips/navi_clear.py rename to chips/SSG/navi_clear.py index e934f42..07ef037 100644 --- a/chips/navi_clear.py +++ b/chips/SSG/navi_clear.py @@ -1,4 +1,3 @@ -#!/bin/python3 import navi_internal command = "clear" diff --git a/chips/navi_exit.py b/chips/SSG/navi_exit.py similarity index 94% rename from chips/navi_exit.py rename to chips/SSG/navi_exit.py index cdece7f..37bfce3 100644 --- a/chips/navi_exit.py +++ b/chips/SSG/navi_exit.py @@ -1,4 +1,3 @@ -#!/bin/python3 import navi_internal command = "exit" diff --git a/chips/navi_help.py b/chips/SSG/navi_help.py similarity index 95% rename from chips/navi_help.py rename to chips/SSG/navi_help.py index 20dabea..f55a029 100644 --- a/chips/navi_help.py +++ b/chips/SSG/navi_help.py @@ -1,5 +1,5 @@ #!/bin/python3 -import commands +import chips command = "navi_help" use = "Displays the help screen" @@ -12,7 +12,7 @@ def run(arguments=None): max_alias_length = 0 command_data = [] - for command_name, module in commands.modules.items(): + for command_name, module in chips.modules.items(): command_aliases = getattr(module, 'aliases', []) command_use = getattr(module, 'use', "") diff --git a/chips/navi_spec.py b/chips/SSG/navi_spec.py similarity index 100% rename from chips/navi_spec.py rename to chips/SSG/navi_spec.py diff --git a/chips/navi_system.py b/chips/SSG/navi_system.py similarity index 100% rename from chips/navi_system.py rename to chips/SSG/navi_system.py diff --git a/chips/__init__.py b/chips/__init__.py index f5fc428..c427af4 100644 --- a/chips/__init__.py +++ b/chips/__init__.py @@ -5,6 +5,9 @@ import glob import importlib import logging +from pathlib import Path + +package_dir = Path(__file__).parent # Add the parent directory to the Python path sys.path.append(dirname(dirname(__file__))) @@ -23,7 +26,7 @@ def load_module(name): """Load module with the given name.""" try: - module = importlib.import_module(f".{name}", 'commands') + module = importlib.import_module(f".{name}", 'chips') except ModuleNotFoundError as e: print(f"Module '{name}' not found: {e}") logging.basicConfig( @@ -40,11 +43,17 @@ def load_module(name): return module -# List all .py files in the current directory, excluding __init__.py +def module_name_from_path(f): + rel_path = f.relative_to(package_dir).with_suffix('') + module_name = '.'.join(rel_path.parts) + return module_name + + +# List all .py files in the current directory and subdirectories, excluding __init__.py __all__ = [ - basename(f)[:-3] - for f in glob.glob(join(dirname(__file__), "*.py")) - if isfile(f) and not f.endswith('__init__.py') and dirname(f) == dirname(__file__) + module_name_from_path(f) + for f in package_dir.rglob('*.py') + if f.is_file() and f.name != '__init__.py' ] # Load each module found in __all__ diff --git a/navi_internal.py b/navi_internal.py index 8bf2073..ddd2934 100644 --- a/navi_internal.py +++ b/navi_internal.py @@ -3,7 +3,7 @@ import textwrap import random import time -import commands +import chips import json import config import spacy @@ -140,13 +140,13 @@ def process_message(self, user_message: str) -> None: if navi_commands and not is_question: command = navi_commands[0].text - main_command = commands.alias_to_command.get(command) + main_command = chips.alias_to_command.get(command) if main_command: - commands.modules[main_command].run(processed_message) + chips.modules[main_command].run(processed_message) else: response_message, http_status = self.llm_chat(user_message) if response_message.startswith("TERMINAL OUTPUT"): - commands.modules["navi_sys"].run(response_message) + chips.modules["navi_sys"].run(response_message) else: self.print_message(f"{response_message if http_status == 200 else 'Issue with server'}") @@ -162,7 +162,7 @@ def chat_with_navi(self) -> None: def setup_navi_vocab(self) -> None: # Register commands and aliases with the entity ruler - for command, module in commands.modules.items(): + for command, module in chips.modules.items(): patterns = [{"label": "NAVI_COMMAND", "pattern": command}] aliases = getattr(module, 'aliases', []) # Safely get the aliases attribute, default to an empty list for alias in aliases: From 19ece91bd84376e009adf04212ef24484589a3a1 Mon Sep 17 00:00:00 2001 From: VAliquo Date: Sun, 17 Nov 2024 12:09:48 -0500 Subject: [PATCH 03/40] [ADD] Dirs --- chips/Com/Community-Chips | 0 chips/Dev/Developer-Chips | 0 chips/SSG/SSG-Chips | 0 3 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 chips/Com/Community-Chips create mode 100644 chips/Dev/Developer-Chips create mode 100644 chips/SSG/SSG-Chips diff --git a/chips/Com/Community-Chips b/chips/Com/Community-Chips new file mode 100644 index 0000000..e69de29 diff --git a/chips/Dev/Developer-Chips b/chips/Dev/Developer-Chips new file mode 100644 index 0000000..e69de29 diff --git a/chips/SSG/SSG-Chips b/chips/SSG/SSG-Chips new file mode 100644 index 0000000..e69de29 From 6d827a5a2f47057e92cc2a4321b23d175705aab4 Mon Sep 17 00:00:00 2001 From: VAliquo Date: Sun, 17 Nov 2024 17:27:42 -0500 Subject: [PATCH 04/40] [UPDATE] Script improvements --- chips/SSG/navi_chip_creator.py | 225 ++++++++++++++++--------------- chips/SSG/navi_chip_installer.py | 26 +++- 2 files changed, 140 insertions(+), 111 deletions(-) diff --git a/chips/SSG/navi_chip_creator.py b/chips/SSG/navi_chip_creator.py index 274c396..ec5181e 100644 --- a/chips/SSG/navi_chip_creator.py +++ b/chips/SSG/navi_chip_creator.py @@ -1,13 +1,14 @@ -#!/bin/python3 import navi_internal import sys import os from colorama import Fore +import subprocess +import webbrowser command: str = "chip-create" use: str = "Creates a new Navi chip" aliases: list = ['chip-create', 'cc'] -params: dict = { +params: dict[str, str] = { '-help': 'Display help information', '-h': 'Display help information', } @@ -15,127 +16,137 @@ help_params: tuple = ('-help', '-h') template_file: str = "chips/SSG/chip-template.txt" chip_install_path: str = "chips/Dev/" -chip_file_path: str = "" chip_documentation_link: str = "https://github.com/SaintsSec/Navi/wiki/4.-Developing-Chips-%E2%80%90-Indepth" def print_params() -> None: - # Print the header + """Prints the available parameters and their descriptions.""" print(f"{'Parameter':<10} | {'Description'}") print("-" * 40) - - # Print each dictionary item for param, description in params.items(): print(f"{param:<10} | {description}") -def run(arguments=None) -> None: - navi_instance = navi_internal.navi_instance - arg_array = arguments.text.split() - arg_array.pop(0) - if arg_array is not None: - for arg in arg_array: - match arg: - case x if x in help_params: - print_params() - return - case _: - navi_instance.print_message(f"Invalid parameter: {arg}\n" - f"Do you want to review the available parameters? (y/n): ") - choice = input().strip().lower() - if choice == 'y': - print_params() - return - return +def get_user_input(prompt): + """Gets input from the user.""" + return input(prompt).strip() - navi_instance.print_message( - f"Welcome to Navi chip creator, {navi_instance.get_user()}. Please enter the name of your new Chip.") - chip_name = input("Chip Name: ") - # Ask for the file name - navi_instance.print_message( - f"Great name, {navi_instance.get_user()}! What do you want the python file to be called?") - chip_file_name = input("Chip File Name: ") +def confirm_details(chip_name, chip_file_name, navi_instance): + """Asks the user to confirm the chip details or make changes.""" while True: - # Recap the entered information navi_instance.print_message(f"Perfect! Here's a recap:" f"\nChip Name: {chip_name}" f"\nPython File Name: {chip_file_name}.py\n") + choice = get_user_input("Are you ready to proceed or do you want to make changes? " + f"({Fore.YELLOW}c{Fore.RESET})ontinue or ({Fore.YELLOW}m{Fore.RESET})ake changes: ").lower() + if choice == 'c': + return chip_name, chip_file_name + elif choice == 'm': + return make_changes(chip_name, chip_file_name, navi_instance) + else: + navi_instance.print_message("Invalid input. Please choose 'c' to continue or 'm' to make changes.") + - while True: # Loop to ensure valid input for continue/make changes - # Ask the user if they want to proceed or make changes - choice = input("Are you ready to proceed or do you want to make changes? " - f"({Fore.YELLOW}c{Fore.RESET})ontinue or ({Fore.YELLOW}m{Fore.RESET})ake changes: ").strip().lower() - - if choice == 'c': - navi_instance.print_message("Awesome! Moving forward with the given details.") - break # Exit the input validation loop - elif choice == 'm': - while True: # Loop to ensure valid input for what to change - # Ask the user what they want to change - change_choice = input(f"What do you want to change? ({Fore.YELLOW}chip{Fore.RESET}) name or " - f"({Fore.YELLOW}file{Fore.RESET}) name: ").strip().lower() - - if change_choice == 'chip': - navi_instance.print_message("Let's update the Chip Name.") - chip_name = input("New Chip Name: ") - break # Exit the loop for change choice - elif change_choice == 'file': - navi_instance.print_message("Let's update the File Name.") - chip_file_name = input("New File Name: ") - break # Exit the loop for change choice - else: - navi_instance.print_message("Invalid choice. Please enter 'chip' or 'file'.") - - break # Exit the input validation loop and recap changes +def make_changes(chip_name, chip_file_name, navi_instance): + """Allows the user to change the chip name or file name.""" + while True: + change_choice = get_user_input(f"What do you want to change? ({Fore.YELLOW}chip{Fore.RESET}) name or " + f"({Fore.YELLOW}file{Fore.RESET}) name: ").lower() + if change_choice == 'chip': + navi_instance.print_message("Let's update the Chip Name.") + chip_name = get_user_input("New Chip Name: ") + return confirm_details(chip_name, chip_file_name, navi_instance) + elif change_choice == 'file': + navi_instance.print_message("Let's update the File Name.") + chip_file_name = get_user_input("New File Name: ") + return confirm_details(chip_name, chip_file_name, navi_instance) + else: + navi_instance.print_message("Invalid choice. Please enter 'chip' or 'file'.") + + +def create_chip_file(chip_name, chip_file_name): + """Creates the chip file from a template.""" + if not os.path.exists(template_file): + print(f"{Fore.RED}ERROR:{Fore.RESET} Template file '{template_file}' is missing. Aborting.") + return None + + with open(template_file, "r") as template: + template_content = template.read() + + chip_file_name_final = chip_file_name.replace(".py", "") + ".py" + template_content = template_content.replace("{{CHIP_NAME}}", chip_name) + chip_file_path = os.path.join(os.getcwd(), chip_install_path, chip_file_name_final) + + with open(chip_file_path, "w") as chip_dev: + chip_dev.write(template_content) + + print(f"{Fore.GREEN}Chip '{chip_name}' created successfully!{Fore.RESET}") + return chip_file_path + + +def post_creation_options(chip_file_path, navi_instance): + """Provides options to the user after chip creation.""" + print(f"Here are some options for you:\n" + f"{Fore.YELLOW}1{Fore.RESET}: Open directory of Chip location\n" + f"{Fore.YELLOW}2{Fore.RESET}: Open in your preferred code editor\n" + f"{Fore.YELLOW}3{Fore.RESET}: Reload Navi to load the new Chip\n" + f"{Fore.YELLOW}4{Fore.RESET}: Review the documentation online\n" + f"{Fore.YELLOW}5{Fore.RESET} or other value: I'm finished") + choice = get_user_input("Please enter 1, 2, 3, 4, or 5: ") + if choice == '1': + print("Opening directory of Chip location") + try: + if sys.platform.startswith('win'): + os.startfile(os.path.dirname(chip_file_path)) + elif sys.platform == 'darwin': + subprocess.call(['open', os.path.dirname(chip_file_path)]) else: - navi_instance.print_message("Invalid input. Please choose 'c' to continue or 'm' to make changes.") - if choice == 'c': - break - - # Check if the template file exists - if os.path.exists(template_file): - with open(template_file, "r") as template: - template_content = template.read() - - # Replace placeholders in the template if needed (optional) - chip_file_name_final = chip_file_name.replace(".py", "") + ".py" - template_content = template_content.replace("{{CHIP_NAME}}", chip_name) - # Write the content to the new file - chip_file_path = os.path.join(os.getcwd(), chip_install_path + chip_file_name_final) - with open(chip_file_path, "w") as chip_dev: - chip_dev.write(template_content) - - navi_instance.print_message(f"{Fore.GREEN}Chip '{chip_name}' created successfully!{Fore.RESET}") + subprocess.call(['xdg-open', os.path.dirname(chip_file_path)]) + except Exception as e: + print(f"Failed to open the directory: {e}") + elif choice == '2': + print("Opening in your preferred code editor") + try: + if sys.platform == 'win32': + os.startfile(chip_file_path) + elif sys.platform == 'darwin': + subprocess.call(['open', chip_file_path]) + else: + subprocess.call(['xdg-open', chip_file_path]) + except Exception as e: + print(f"Failed to open the file: {e}") + elif choice == '3': + navi_instance.reload() + elif choice == '4': + webbrowser.open(chip_documentation_link) else: - navi_instance.print_message(f"{Fore.RED}ERROR:{Fore.RESET} Template file '{template_file}' is missing. Aborting.") - - navi_instance.print_message(f"Here are some options for you:\n" - f"{Fore.YELLOW}1{Fore.RESET}: Open directory of Chip location\n" - f"{Fore.YELLOW}2{Fore.RESET}: Open in your preferred code editor\n" - f"{Fore.YELLOW}3{Fore.RESET}: Reload Navi to load the new Chip\n" - f"{Fore.YELLOW}4{Fore.RESET}: Review the documentation online\n" - f"{Fore.YELLOW}5{Fore.RESET} or other value: I'm finished") - choice = input("Please enter 1, 2, 3, 4, or 5: ") - match choice: - case '1': - print("Opening directory of Chip location") - case '2': - print("Opening in your preferred code editor") - import subprocess - try: - if sys.platform == 'win32': - os.startfile(chip_file_path) - elif sys.platform == 'darwin': - subprocess.call(['open', chip_file_path]) - else: - subprocess.call(['xdg-open', chip_file_path]) - except Exception as e: - print(f"Failed to open the log file: {e}") - case '3': - navi_instance.reload() - case '4': - import webbrowser - webbrowser.open(chip_documentation_link) - case _: - return + pass + + +def run(arguments=None): + navi_instance = navi_internal.navi_instance + arg_array = arguments.text.split()[1:] # Exclude the command itself + + if arg_array: + for arg in arg_array: + if arg in help_params: + print_params() + return + else: + choice = get_user_input(f"Invalid parameter: {arg}\n" + f"Do you want to review the available parameters? (y/n): ").lower() + if choice == 'y': + print_params() + return + + navi_instance.print_message( + f"Welcome to Navi Chip creator, {navi_instance.get_user()}. Please enter the name of your new Chip.") + chip_name = get_user_input("Chip Name: ") + navi_instance.print_message(f"Great name, {navi_instance.get_user()}! What do you want the python file to be called?") + chip_file_name = get_user_input("Chip File Name: ") + + chip_name, chip_file_name = confirm_details(chip_name, chip_file_name, navi_instance) + chip_file_path = create_chip_file(chip_name, chip_file_name) + if chip_file_path: + post_creation_options(chip_file_path, navi_instance) diff --git a/chips/SSG/navi_chip_installer.py b/chips/SSG/navi_chip_installer.py index cef20d1..7bf0748 100644 --- a/chips/SSG/navi_chip_installer.py +++ b/chips/SSG/navi_chip_installer.py @@ -236,21 +236,39 @@ def get_installed_chips() -> list[dict[str, str]] | None: return modules +def get_dev_chips(): + import chips + dev_chips = [] + for command_name, module in chips.modules.items(): + if module.__name__.startswith("chips.Dev"): + command_aliases = getattr(module, 'aliases', []) + command_use = getattr(module, 'use', "") + dev_chips.append((command_name, command_use, command_aliases)) + return dev_chips + + def list_installed_chips() -> None: chips = get_installed_chips() if chips: - print("Installed Chips:") + print("Installed Community Chips:") for module in chips: print(f"- {module['name']} (Owner: {module['owner']}, Version: {module['version']})") else: - print("No chips are installed.") + print("No Community Chips are installed.") + dev_chips = get_dev_chips() + if not dev_chips: + print("No Developer Chips are installed.") + else: + print("\nDeveloper Chips:") + for chip in dev_chips: + print(f"- {chip[0]} (Use: {chip[1]}, Aliases: {chip[2]})") def about_chip(name) -> dict[str, str] | None: log_file_path = "installed_chips.txt" if not os.path.exists(log_file_path): - print("No chips are installed.") + print("No Community Chips are installed.") return None with open(log_file_path, 'r') as log_file: @@ -290,7 +308,7 @@ def about_chip(name) -> dict[str, str] | None: "latest_version": latest_version } - print(f"The chip '{name}' is not installed.") + print(f"The Community Chip '{name}' is not installed.") return None From 769bd5dc9c3189c15eacc49b940e12ffd7388644 Mon Sep 17 00:00:00 2001 From: VAliquo Date: Mon, 18 Nov 2024 01:05:14 -0500 Subject: [PATCH 05/40] [UPDATE] Subprocess --- chips/SSG/navi_chip_creator.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/chips/SSG/navi_chip_creator.py b/chips/SSG/navi_chip_creator.py index ec5181e..5bd6ff8 100644 --- a/chips/SSG/navi_chip_creator.py +++ b/chips/SSG/navi_chip_creator.py @@ -2,7 +2,7 @@ import sys import os from colorama import Fore -import subprocess +import subprocess # nosec import webbrowser command: str = "chip-create" @@ -97,23 +97,24 @@ def post_creation_options(chip_file_path, navi_instance): if choice == '1': print("Opening directory of Chip location") try: + directory = os.path.dirname(chip_file_path) if sys.platform.startswith('win'): - os.startfile(os.path.dirname(chip_file_path)) + subprocess.run(['explorer', directory], check=True) elif sys.platform == 'darwin': - subprocess.call(['open', os.path.dirname(chip_file_path)]) + subprocess.run(['open', directory], check=True) else: - subprocess.call(['xdg-open', os.path.dirname(chip_file_path)]) + subprocess.run(['xdg-open', directory], check=True) except Exception as e: print(f"Failed to open the directory: {e}") elif choice == '2': print("Opening in your preferred code editor") try: - if sys.platform == 'win32': - os.startfile(chip_file_path) + if sys.platform.startswith('win'): + subprocess.run(['explorer', chip_file_path], check=True) elif sys.platform == 'darwin': - subprocess.call(['open', chip_file_path]) + subprocess.run(['open', chip_file_path], check=True) else: - subprocess.call(['xdg-open', chip_file_path]) + subprocess.run(['xdg-open', chip_file_path], check=True) except Exception as e: print(f"Failed to open the file: {e}") elif choice == '3': From f00bf6aef66abe18a7a5ca7c5768cd19d6e76078 Mon Sep 17 00:00:00 2001 From: VAliquo Date: Mon, 18 Nov 2024 01:35:45 -0500 Subject: [PATCH 06/40] [UPDATE] Sanitize input --- chips/SSG/navi_chip_creator.py | 62 +++++++++++++++++++++++----------- 1 file changed, 42 insertions(+), 20 deletions(-) diff --git a/chips/SSG/navi_chip_creator.py b/chips/SSG/navi_chip_creator.py index 5bd6ff8..245a1d6 100644 --- a/chips/SSG/navi_chip_creator.py +++ b/chips/SSG/navi_chip_creator.py @@ -1,9 +1,11 @@ import navi_internal import sys import os -from colorama import Fore +import re import subprocess # nosec import webbrowser +from colorama import Fore +from navi_shell import restart_navi command: str = "chip-create" use: str = "Creates a new Navi chip" @@ -87,16 +89,16 @@ def create_chip_file(chip_name, chip_file_name): def post_creation_options(chip_file_path, navi_instance): """Provides options to the user after chip creation.""" - print(f"Here are some options for you:\n" + navi_instance.print_message(f"Here are some options for you:\n" f"{Fore.YELLOW}1{Fore.RESET}: Open directory of Chip location\n" f"{Fore.YELLOW}2{Fore.RESET}: Open in your preferred code editor\n" f"{Fore.YELLOW}3{Fore.RESET}: Reload Navi to load the new Chip\n" f"{Fore.YELLOW}4{Fore.RESET}: Review the documentation online\n" f"{Fore.YELLOW}5{Fore.RESET} or other value: I'm finished") choice = get_user_input("Please enter 1, 2, 3, 4, or 5: ") - if choice == '1': - print("Opening directory of Chip location") - try: + try: + if choice == '1': + print("Opening directory of Chip location") directory = os.path.dirname(chip_file_path) if sys.platform.startswith('win'): subprocess.run(['explorer', directory], check=True) @@ -104,25 +106,45 @@ def post_creation_options(chip_file_path, navi_instance): subprocess.run(['open', directory], check=True) else: subprocess.run(['xdg-open', directory], check=True) - except Exception as e: - print(f"Failed to open the directory: {e}") - elif choice == '2': - print("Opening in your preferred code editor") - try: + elif choice == '2': + print("Opening in your preferred code editor") if sys.platform.startswith('win'): subprocess.run(['explorer', chip_file_path], check=True) elif sys.platform == 'darwin': subprocess.run(['open', chip_file_path], check=True) else: subprocess.run(['xdg-open', chip_file_path], check=True) - except Exception as e: - print(f"Failed to open the file: {e}") - elif choice == '3': - navi_instance.reload() - elif choice == '4': - webbrowser.open(chip_documentation_link) - else: - pass + elif choice == '3': + restart_navi() + elif choice == '4': + webbrowser.open(chip_documentation_link) + else: + pass + except FileNotFoundError: + print("Couldn't find the file or directory.") + except subprocess.SubprocessError as e: + print(f"Failed to execute the command: {e}") + except Exception as e: + print(f"An unexpected error occurred: {e}") + + +def sanitize_input(name): + """ + Sanitizes the user-provided chip name. + Ensures it is safe and valid for use in file paths. + """ + sanitized_name = name.strip() + if len(name) > 50: + print(f"{Fore.RED}Warning:{Fore.RESET} Input is too long (50 characters max). Trimming to 49 characters.") + sanitized_name = sanitized_name[:50] + + # Replace invalid characters with underscores + old_name = sanitized_name + sanitized_name = re.sub(r'[^\w\-]', '_', name) + if old_name != sanitized_name: + print(f"{Fore.RED}Warning:{Fore.RESET} Invalid characters in '{old_name}'. Replacing with '{sanitized_name}'.") + + return sanitized_name def run(arguments=None): @@ -143,9 +165,9 @@ def run(arguments=None): navi_instance.print_message( f"Welcome to Navi Chip creator, {navi_instance.get_user()}. Please enter the name of your new Chip.") - chip_name = get_user_input("Chip Name: ") + chip_name = sanitize_input(get_user_input("Chip Name: ")) navi_instance.print_message(f"Great name, {navi_instance.get_user()}! What do you want the python file to be called?") - chip_file_name = get_user_input("Chip File Name: ") + chip_file_name = sanitize_input(get_user_input("Chip File Name: ")) chip_name, chip_file_name = confirm_details(chip_name, chip_file_name, navi_instance) chip_file_path = create_chip_file(chip_name, chip_file_name) From acca36dad970d72fe4313727c9b27768d00f5be5 Mon Sep 17 00:00:00 2001 From: VAliquo Date: Mon, 18 Nov 2024 01:40:41 -0500 Subject: [PATCH 07/40] Turn off false-positives --- chips/SSG/navi_chip_creator.py | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/chips/SSG/navi_chip_creator.py b/chips/SSG/navi_chip_creator.py index 245a1d6..1cced16 100644 --- a/chips/SSG/navi_chip_creator.py +++ b/chips/SSG/navi_chip_creator.py @@ -90,30 +90,30 @@ def create_chip_file(chip_name, chip_file_name): def post_creation_options(chip_file_path, navi_instance): """Provides options to the user after chip creation.""" navi_instance.print_message(f"Here are some options for you:\n" - f"{Fore.YELLOW}1{Fore.RESET}: Open directory of Chip location\n" - f"{Fore.YELLOW}2{Fore.RESET}: Open in your preferred code editor\n" - f"{Fore.YELLOW}3{Fore.RESET}: Reload Navi to load the new Chip\n" - f"{Fore.YELLOW}4{Fore.RESET}: Review the documentation online\n" - f"{Fore.YELLOW}5{Fore.RESET} or other value: I'm finished") + f"{Fore.YELLOW}1{Fore.RESET}: Open directory of Chip location\n" + f"{Fore.YELLOW}2{Fore.RESET}: Open in your preferred code editor\n" + f"{Fore.YELLOW}3{Fore.RESET}: Reload Navi to load the new Chip\n" + f"{Fore.YELLOW}4{Fore.RESET}: Review the documentation online\n" + f"{Fore.YELLOW}5{Fore.RESET} or other value: I'm finished") choice = get_user_input("Please enter 1, 2, 3, 4, or 5: ") try: if choice == '1': print("Opening directory of Chip location") directory = os.path.dirname(chip_file_path) if sys.platform.startswith('win'): - subprocess.run(['explorer', directory], check=True) + subprocess.run(['explorer', directory], check=True) # nosec elif sys.platform == 'darwin': - subprocess.run(['open', directory], check=True) + subprocess.run(['open', directory], check=True) # nosec else: - subprocess.run(['xdg-open', directory], check=True) + subprocess.run(['xdg-open', directory], check=True) # nosec elif choice == '2': print("Opening in your preferred code editor") if sys.platform.startswith('win'): - subprocess.run(['explorer', chip_file_path], check=True) + subprocess.run(['explorer', chip_file_path], check=True) # nosec elif sys.platform == 'darwin': - subprocess.run(['open', chip_file_path], check=True) + subprocess.run(['open', chip_file_path], check=True) # nosec else: - subprocess.run(['xdg-open', chip_file_path], check=True) + subprocess.run(['xdg-open', chip_file_path], check=True) # nosec elif choice == '3': restart_navi() elif choice == '4': @@ -166,7 +166,8 @@ def run(arguments=None): navi_instance.print_message( f"Welcome to Navi Chip creator, {navi_instance.get_user()}. Please enter the name of your new Chip.") chip_name = sanitize_input(get_user_input("Chip Name: ")) - navi_instance.print_message(f"Great name, {navi_instance.get_user()}! What do you want the python file to be called?") + navi_instance.print_message( + f"Great name, {navi_instance.get_user()}! What do you want the python file to be called?") chip_file_name = sanitize_input(get_user_input("Chip File Name: ")) chip_name, chip_file_name = confirm_details(chip_name, chip_file_name, navi_instance) From d95a82cfa4a0093921b4e64bb1bfdf88ea618a1d Mon Sep 17 00:00:00 2001 From: Alex Kollar Date: Mon, 18 Nov 2024 10:41:31 -0500 Subject: [PATCH 08/40] Added Local to navi_internal Signed-off-by: Alex Kollar --- navi_internal.py | 1 + 1 file changed, 1 insertion(+) diff --git a/navi_internal.py b/navi_internal.py index 8bf2073..3da26b0 100644 --- a/navi_internal.py +++ b/navi_internal.py @@ -23,6 +23,7 @@ class NaviApp: server: str = config.server port: int = config.port + local: str = config.local script_dir = os.path.dirname(os.path.abspath(__file__)) hist_file = os.path.join(script_dir, ".navi_history") From f52791d490cf5d1f25a3b6098cf93506b25479a4 Mon Sep 17 00:00:00 2001 From: Alex Kollar Date: Mon, 18 Nov 2024 11:05:46 -0500 Subject: [PATCH 09/40] Navi3b (llama) install / setup included in install.sh Signed-off-by: Alex Kollar --- install/install.sh | 13 +++++++++++++ install/navi3b | 1 + 2 files changed, 14 insertions(+) create mode 160000 install/navi3b diff --git a/install/install.sh b/install/install.sh index d50bb8f..31cc671 100755 --- a/install/install.sh +++ b/install/install.sh @@ -15,6 +15,18 @@ install_reqs() { sudo apt install -y python3 python3-pip python3-venv nmap } +navi3b_local() { + sudo rm -rf ./navi3b + git clone https://github.com/saintssec/navi3b.git + if command -v ollama &> /dev/null; then + echo "ollama exists. Moving on..." + ollama create navi-cli -f ./navi3b/navi3b.modelfile + else + echo "ollama does not exist. Installing ollama + Navi3b ..." + sh ./navi3b/install.sh + fi +} + set_venv(){ python3 -m venv ../navienv source ../navienv/bin/activate @@ -121,6 +133,7 @@ create_navi_group(){ install_reqs set_venv pip_install +navi3b_local create_navi_group delete_navi copy_navi diff --git a/install/navi3b b/install/navi3b new file mode 160000 index 0000000..8f7fa3e --- /dev/null +++ b/install/navi3b @@ -0,0 +1 @@ +Subproject commit 8f7fa3e3a1f315cd48091f1652816c505990dcf3 From 955f2fa12074ca42caa7ad370fe002fd42c65c31 Mon Sep 17 00:00:00 2001 From: Alex Kollar Date: Tue, 19 Nov 2024 09:59:11 -0500 Subject: [PATCH 10/40] Removed 3bfolder - Added toggle for remote Signed-off-by: Alex Kollar --- .navi_history | 12 ++++++++++++ install/navi3b | 1 - navi_internal.py | 5 +++-- navi_shell.py | 4 +++- 4 files changed, 18 insertions(+), 4 deletions(-) create mode 100644 .navi_history delete mode 160000 install/navi3b diff --git a/.navi_history b/.navi_history new file mode 100644 index 0000000..5a0a52e --- /dev/null +++ b/.navi_history @@ -0,0 +1,12 @@ + +# 2024-11-19 09:56:37.627168 ++Hello tell me about you. + +# 2024-11-19 09:56:55.924432 ++exit + +# 2024-11-19 09:57:10.522959 ++Hello there tell me about you + +# 2024-11-19 09:57:46.941877 ++exit diff --git a/install/navi3b b/install/navi3b deleted file mode 160000 index 8f7fa3e..0000000 --- a/install/navi3b +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 8f7fa3e3a1f315cd48091f1652816c505990dcf3 diff --git a/navi_internal.py b/navi_internal.py index e2d135d..5a5dd70 100644 --- a/navi_internal.py +++ b/navi_internal.py @@ -88,7 +88,7 @@ def clear_terminal(self) -> None: os.system('cls' if os.name == 'nt' else 'clear') print(self.art) - def llm_chat(self, user_message: str, called_from_app: bool = False) -> tuple[str, int]: + def llm_chat(self, user_message: str, called_from_app: bool = False, use_remote: bool = False) -> tuple[str, int]: # Define the API endpoint and payload message_amendment = user_message if not called_from_app: @@ -99,7 +99,8 @@ def llm_chat(self, user_message: str, called_from_app: bool = False) -> tuple[st "normally.") + f"The user's OS is {platform.system()}" + ". User message:") message_amendment += user_message - url = f"http://{self.server}:{self.port}/api/chat" + server_to_use = self.server if use_remote else self.local + url = f"http://{server_to_use}:{self.port}/api/chat" payload = { "model": "navi-cli", "messages": [{"role": "user", "content": message_amendment}] diff --git a/navi_shell.py b/navi_shell.py index 77e4549..4e9f4bd 100644 --- a/navi_shell.py +++ b/navi_shell.py @@ -60,6 +60,7 @@ def handle_exception(exc_type, exc_value, exc_traceback) -> None: parser.add_argument('--skip-update', action='store_true', help='Skip the update check (used internally to prevent update loop)') parser.add_argument('--install', action='store_true', help='installs Navi based on the current downloaded version.') +parser.add_argument('--remote', action='store_true', help='Use remote server instead of local server') args = parser.parse_args() @@ -73,7 +74,8 @@ def main() -> None: if args.q: response_message, http_status = navi_instance.llm_chat( f"{args.q}", - True + True, + args.remote ) navi_instance.print_message( f"{response_message if http_status == 200 else f'Trouble connecting to Navi server.'}" From 28c9923304911e493e6957f79aca46b12e1ca76d Mon Sep 17 00:00:00 2001 From: Alex Kollar Date: Tue, 19 Nov 2024 09:59:51 -0500 Subject: [PATCH 11/40] removed Navi history file + added toggle for remote server. Signed-off-by: Alex Kollar --- .navi_history | 12 ------------ 1 file changed, 12 deletions(-) delete mode 100644 .navi_history diff --git a/.navi_history b/.navi_history deleted file mode 100644 index 5a0a52e..0000000 --- a/.navi_history +++ /dev/null @@ -1,12 +0,0 @@ - -# 2024-11-19 09:56:37.627168 -+Hello tell me about you. - -# 2024-11-19 09:56:55.924432 -+exit - -# 2024-11-19 09:57:10.522959 -+Hello there tell me about you - -# 2024-11-19 09:57:46.941877 -+exit From f9d2d3d646a636079357c12a7c34f314ad00dd79 Mon Sep 17 00:00:00 2001 From: Alex Kollar Date: Tue, 19 Nov 2024 10:05:57 -0500 Subject: [PATCH 12/40] Brushed up the linguistics from 'server' to 'remote' Signed-off-by: Alex Kollar --- config.py | 2 +- navi_internal.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/config.py b/config.py index 97d03ab..d835099 100644 --- a/config.py +++ b/config.py @@ -1,3 +1,3 @@ -server = "labs.saintssec.org" +remote = "labs.saintssec.org" local = "localhost" port = 11434 diff --git a/navi_internal.py b/navi_internal.py index 5a5dd70..3bed11f 100644 --- a/navi_internal.py +++ b/navi_internal.py @@ -21,7 +21,7 @@ class NaviApp: breakline: str = mods.breakline ai_name_rep: str = "Navi> " - server: str = config.server + remote: str = config.remote port: int = config.port local: str = config.local @@ -99,7 +99,7 @@ def llm_chat(self, user_message: str, called_from_app: bool = False, use_remote: "normally.") + f"The user's OS is {platform.system()}" + ". User message:") message_amendment += user_message - server_to_use = self.server if use_remote else self.local + server_to_use = self.remote if use_remote else self.local url = f"http://{server_to_use}:{self.port}/api/chat" payload = { "model": "navi-cli", From 9e0dffa8fe7e42c973f4423769d5a350ab9ba25f Mon Sep 17 00:00:00 2001 From: Alex Kollar Date: Tue, 3 Dec 2024 14:03:59 -0500 Subject: [PATCH 13/40] Started working on the revised navi3b installer. --- mods/3binstall.py | 32 ++++++++++++++++++++++++++++++++ navi_internal.py | 29 +++++++++++++++++++++-------- 2 files changed, 53 insertions(+), 8 deletions(-) create mode 100644 mods/3binstall.py diff --git a/mods/3binstall.py b/mods/3binstall.py new file mode 100644 index 0000000..db7fed2 --- /dev/null +++ b/mods/3binstall.py @@ -0,0 +1,32 @@ +import subprocess +import os +import sys + +def intro_text(): + print("Welcome to the Navi3b model installer.\n============\nThis script will install Ollama and the Navi3b model.\n" + "It is recommended to have a stable internet connection for this process. \nIf you have any questions, please refer to the Navi documentation." + "\nIt should also be noted that this is a local AI model\nExpect it to be slower than its larger server based counterpart.") + +def install_navi3b(): + print("Installing navi3b model...") + try: + if os.path.exists("navi3b"): + print("navi3b directory already exists, skipping clone.") + subprocess.run(["bash", "-s", "navi3b/install.sh"], check=True) + else: + subprocess.run(["git", "clone", "https://github.com/saintssec/navi3b"], check=True) + subprocess.run(["bash", "-s", "navi3b/install.sh"], check=True) + except subprocess.CalledProcessError as e: + print(f"Failed to install navi3b model: {e}") + +def main(): + intro_text() + user_input = input("============\nWould you like to install the Navi3b system? \nOptions: (Y)es, (N)o\nUser Input > ").strip().lower() + if user_input in ["yes", "y"]: + install_navi3b() + else: + print("Understood exiting now") + sys.exit(0) + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/navi_internal.py b/navi_internal.py index 3bed11f..314ed02 100644 --- a/navi_internal.py +++ b/navi_internal.py @@ -14,14 +14,14 @@ from mods import mods - +# TODO - Figure out the Navi local install stuff. class NaviApp: art: str = mods.art helpAr: str = mods.helpArt breakline: str = mods.breakline ai_name_rep: str = "Navi> " - remote: str = config.remote + server: str = config.server port: int = config.port local: str = config.local @@ -88,7 +88,7 @@ def clear_terminal(self) -> None: os.system('cls' if os.name == 'nt' else 'clear') print(self.art) - def llm_chat(self, user_message: str, called_from_app: bool = False, use_remote: bool = False) -> tuple[str, int]: + def llm_chat(self, user_message: str, called_from_app: bool = False) -> tuple[str, int]: # Define the API endpoint and payload message_amendment = user_message if not called_from_app: @@ -99,8 +99,7 @@ def llm_chat(self, user_message: str, called_from_app: bool = False, use_remote: "normally.") + f"The user's OS is {platform.system()}" + ". User message:") message_amendment += user_message - server_to_use = self.remote if use_remote else self.local - url = f"http://{server_to_use}:{self.port}/api/chat" + url = f"http://{self.server}:{self.port}/api/chat" payload = { "model": "navi-cli", "messages": [{"role": "user", "content": message_amendment}] @@ -142,9 +141,14 @@ def process_message(self, user_message: str) -> None: if navi_commands and not is_question: command = navi_commands[0].text - main_command = chips.alias_to_command.get(command) - if main_command: - chips.modules[main_command].run(processed_message) + if command == "custom_command": + self.custom_method() + elif command in ["Install navi3b", "install local", "navi3b install"]: + self.install_3b() + else: + main_command = chips.alias_to_command.get(command) + if main_command: + chips.modules[main_command].run(processed_message) else: response_message, http_status = self.llm_chat(user_message) if response_message.startswith("TERMINAL OUTPUT"): @@ -170,6 +174,15 @@ def setup_navi_vocab(self) -> None: for alias in aliases: patterns.append({"label": "NAVI_COMMAND", "pattern": alias}) self.ruler.add_patterns(patterns) + # Add custom patterns + custom_patterns = [ + {"label": "NAVI_COMMAND", "pattern": "custom_command"}, + {"label": "NAVI_COMMAND", "pattern": "navi3b install"} + ] + self.ruler.add_patterns(custom_patterns) + + def install_3b(self) -> None: + self.print_message("Oh yay! you want to install me Locally! Let's get started!") navi_instance = NaviApp() From e9db4021979059e0cdfeacd097e744514e9f83c8 Mon Sep 17 00:00:00 2001 From: Alex Kollar Date: Thu, 12 Dec 2024 11:42:41 -0500 Subject: [PATCH 14/40] Revised Navi3b Installer, added 3b banner, removed forced 3b in install.sh. Signed-off-by: Alex Kollar --- .gitignore | 2 + install/install.sh | 13 ------ mods/3binstall.py | 98 ++++++++++++++++++++++++++++++++++++++++++---- mods/mods.py | 38 ++++++------------ navi_internal.py | 27 ++++--------- 5 files changed, 111 insertions(+), 67 deletions(-) diff --git a/.gitignore b/.gitignore index 022bc04..8027b99 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,5 @@ __pycache__/ navienv/ verification_test .navi_version +.navi_history + diff --git a/install/install.sh b/install/install.sh index 31cc671..d50bb8f 100755 --- a/install/install.sh +++ b/install/install.sh @@ -15,18 +15,6 @@ install_reqs() { sudo apt install -y python3 python3-pip python3-venv nmap } -navi3b_local() { - sudo rm -rf ./navi3b - git clone https://github.com/saintssec/navi3b.git - if command -v ollama &> /dev/null; then - echo "ollama exists. Moving on..." - ollama create navi-cli -f ./navi3b/navi3b.modelfile - else - echo "ollama does not exist. Installing ollama + Navi3b ..." - sh ./navi3b/install.sh - fi -} - set_venv(){ python3 -m venv ../navienv source ../navienv/bin/activate @@ -133,7 +121,6 @@ create_navi_group(){ install_reqs set_venv pip_install -navi3b_local create_navi_group delete_navi copy_navi diff --git a/mods/3binstall.py b/mods/3binstall.py index db7fed2..bb64693 100644 --- a/mods/3binstall.py +++ b/mods/3binstall.py @@ -1,28 +1,110 @@ import subprocess import os import sys +from getpass import getpass +import tempfile +import shutil + +from IPython.core.debugger import prompt + +password = getpass("Enter your sudo password: ") def intro_text(): print("Welcome to the Navi3b model installer.\n============\nThis script will install Ollama and the Navi3b model.\n" "It is recommended to have a stable internet connection for this process. \nIf you have any questions, please refer to the Navi documentation." "\nIt should also be noted that this is a local AI model\nExpect it to be slower than its larger server based counterpart.") -def install_navi3b(): - print("Installing navi3b model...") +def install_ollama(): + try: + process = subprocess.run( + ["curl", "-fsSL", "https://ollama.com/install.sh", "|", "sh"], + check=True, + capture_output=True, + text=True, + ) + print("Ollama was installed successfully!") + print("stdout:", process.stdout) + print("stderr:", process.stderr) + except subprocess.CalledProcessError as e: + print("Ollama installation failed!") + print("Return code:", e.returncode) + print("stdout:", e.stdout) + print("stderr:", e.stderr) + +def check_ollama(): try: - if os.path.exists("navi3b"): - print("navi3b directory already exists, skipping clone.") - subprocess.run(["bash", "-s", "navi3b/install.sh"], check=True) + process = subprocess.run( + ["ollama", "--version"], + check=True, + capture_output=True, + text=True, + ) + print("Ollama is already installed!") + print("stdout:", process.stdout) + print("stderr:", process.stderr) + except subprocess.CalledProcessError as e: + print("ollama is not installed!") + ollama_choice = input("Would you like to install Ollama now? \nOptions: (Y)es, (N)o\nUser Input > ").strip().lower() + if ollama_choice in ["yes", "y"]: + install_ollama() else: - subprocess.run(["git", "clone", "https://github.com/saintssec/navi3b"], check=True) - subprocess.run(["bash", "-s", "navi3b/install.sh"], check=True) + print("Understood, exiting now.") + sys.exit(0) + print("Return code:", e.returncode) + print("stdout:", e.stdout) + print("stderr:", e.stderr) + +def install_navi3b(): + with tempfile.NamedTemporaryFile("w", delete=False) as temp_script: + temp_script.write(f"#!/bin/bash\necho {password}\n") + temp_script_path = temp_script.name + + os.chmod(temp_script_path, 0o700) + + env = os.environ.copy() + env["SUDO_ASKPASS"] = temp_script_path + try: + process = subprocess.run( + ["git", "clone", "https://github.com/saintssec/navi3b"], + env=env, + check=True, + capture_output=True, + text=True, + ) + print("Command succeeded!") + print("stdout:", process.stdout) + print("stderr:", process.stderr) + except subprocess.CalledProcessError as e: + print("Command failed!") + print("Return code:", e.returncode) + print("stdout:", e.stdout) + print("stderr:", e.stderr) + + try: + process = subprocess.run( + ["sudo", "ollama", "create", "navi-cli", "-f", "./navi3b/navi3b.modelfile"], + env=env, + check=True, + capture_output=True, + text=True, + ) + print("Command succeeded!") + print("stdout:", process.stdout) + print("stderr:", process.stderr) except subprocess.CalledProcessError as e: - print(f"Failed to install navi3b model: {e}") + print("Command failed!") + print("Return code:", e.returncode) + print("stdout:", e.stdout) + print("stderr:", e.stderr) + finally: + os.remove(temp_script_path) + shutil.rmtree("./navi3b") def main(): intro_text() user_input = input("============\nWould you like to install the Navi3b system? \nOptions: (Y)es, (N)o\nUser Input > ").strip().lower() if user_input in ["yes", "y"]: + check_ollama() install_navi3b() else: print("Understood exiting now") diff --git a/mods/mods.py b/mods/mods.py index 8fe84b0..361c46a 100644 --- a/mods/mods.py +++ b/mods/mods.py @@ -11,7 +11,7 @@ | / | / /___ __ __(_) | | / |/ / __ `/ | / / / | | / /| / /_/ /| |/ / / | -| /_/ |_/\__,_/ |___/_/ v{versionNum} | +| /_/ |_/\__,_/ |___/_/ v{versionNum} |\ |===================================================| | Disclaimer: Saints Security Group LLC does not | | condone or support illegal activity and assumes | @@ -19,31 +19,17 @@ | the use of Navi. --nhelp for more information | {breakline} """ -vbusterArt = rf"""{breakline} -| _ ______ __ | -| | | / / __ )__ _______/ /____ _____ | -| | | / / __ / / / / ___/ __/ _ \/ ___/ | -| | |/ / /_/ / /_/ (__ ) /_/ __/ / | -| |___/_____/\__,_/____/\__/\___/_/ | -| Powered by ClamAV | -{breakline} - -""" -reconArt = rf"""{breakline} -| ____ | -| / __ \___ _________ ____ | -| / /_/ / _ \/ ___/ __ \/ __ \\ | -| / _, _/ __/ /__/ /_/ / / / / | -| /_/ |_|\___/\___/\____/_/ /_/ | -{breakline} -""" -gptArt = rf"""{breakline} -| _ __ _ __________ ______ | -| / | / /___ __ __(_) / ____/ __ \/_ __/ | -| / |/ / __ `/ | / / /_____/ / __/ /_/ / / / | -| / /| / /_/ /| |/ / /_____/ /_/ / ____/ / / | -| /_/ |_/\__,_/ |___/_/ \____/_/ /_/ | -| | +three_b_art = rf"""{breakline} +| _ __ _ _____ __ | +| / | / /___ __ __(_)__ // /_ | +| / |/ / __ `/ | / / / /_ " - server: str = config.server + server: str = config.remote port: int = config.port local: str = config.local @@ -86,7 +87,7 @@ def print_message(self, text: str, include_ai_name: bool = True) -> None: def clear_terminal(self) -> None: os.system('cls' if os.name == 'nt' else 'clear') - print(self.art) + print(self.three_b_art) def llm_chat(self, user_message: str, called_from_app: bool = False) -> tuple[str, int]: # Define the API endpoint and payload @@ -141,14 +142,9 @@ def process_message(self, user_message: str) -> None: if navi_commands and not is_question: command = navi_commands[0].text - if command == "custom_command": - self.custom_method() - elif command in ["Install navi3b", "install local", "navi3b install"]: - self.install_3b() - else: - main_command = chips.alias_to_command.get(command) - if main_command: - chips.modules[main_command].run(processed_message) + main_command = chips.alias_to_command.get(command) + if main_command: + chips.modules[main_command].run(processed_message) else: response_message, http_status = self.llm_chat(user_message) if response_message.startswith("TERMINAL OUTPUT"): @@ -174,15 +170,6 @@ def setup_navi_vocab(self) -> None: for alias in aliases: patterns.append({"label": "NAVI_COMMAND", "pattern": alias}) self.ruler.add_patterns(patterns) - # Add custom patterns - custom_patterns = [ - {"label": "NAVI_COMMAND", "pattern": "custom_command"}, - {"label": "NAVI_COMMAND", "pattern": "navi3b install"} - ] - self.ruler.add_patterns(custom_patterns) - - def install_3b(self) -> None: - self.print_message("Oh yay! you want to install me Locally! Let's get started!") navi_instance = NaviApp() From 6564808faf2f865b15ecd168165d15839dbb043b Mon Sep 17 00:00:00 2001 From: VAliquo Date: Sun, 15 Dec 2024 06:55:11 -0500 Subject: [PATCH 15/40] First pass --- install/__init__.py | 0 install/local_model.py | 243 +++++++++++++++++++++++++++++++++ mods/3binstall.py | 114 ---------------- mods/mods.py => navi_banner.py | 0 navi_internal.py | 28 ++-- navi_shell.py | 42 +++++- 6 files changed, 300 insertions(+), 127 deletions(-) create mode 100644 install/__init__.py create mode 100644 install/local_model.py delete mode 100644 mods/3binstall.py rename mods/mods.py => navi_banner.py (100%) diff --git a/install/__init__.py b/install/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/install/local_model.py b/install/local_model.py new file mode 100644 index 0000000..8bda60a --- /dev/null +++ b/install/local_model.py @@ -0,0 +1,243 @@ +import subprocess +import os +import sys +import tempfile +import shutil +from getpass import getpass +from typing import Tuple + + +def intro_text(): + print("Welcome to the Navi3b model installer.\n============\nThis script will install Ollama and the Navi3b model.\n" + "It is recommended to have a stable internet connection for this process. \nIf you have any questions, please refer to the Navi documentation." + "\nIt should also be noted that this is a local AI model\nExpect it to be slower than its larger server based counterpart.") + + +def install_ollama() -> bool: + import platform + try: + if platform.system() in ["Linux", "Darwin"]: # macOS/Linux + print("Installing Ollama on macOS/Linux...") + subprocess.run( + ["curl", "-fsSL", "https://ollama.com/install.sh", "|", "sh"], + shell=True, # Use shell to handle the pipe + check=True, + capture_output=True, + text=True, + ) + elif platform.system() == "Windows": + print("Installing Ollama on Windows...") + # Assuming Windows uses an executable installer + subprocess.run( + ["powershell", "-Command", + "Invoke-WebRequest -Uri https://ollama.com/install.exe -OutFile ollama_installer.exe; Start-Process -FilePath ./ollama_installer.exe -Wait"], + shell=True, + check=True, + capture_output=True, + text=True, + ) + else: + print("Unsupported platform for Ollama installation.") + return False + + print("Ollama installation succeeded!") + return True + except subprocess.CalledProcessError as e: + print("Ollama installation failed!") + print("Return code:", e.returncode) + print("stdout:", e.stdout) + print("stderr:", e.stderr) + return False + + +def ollama_installed() -> bool: + try: + subprocess.run( + ["ollama", "--version"], + check=True, + capture_output=True, + text=True, + ) + return True + except subprocess.CalledProcessError as e: + return False + + +def is_ollama_service_running() -> bool: + """ + Checks if the 'ollama' service is running by executing 'ollama --version'. + If the service is not running, it outputs a warning. + """ + try: + # Run `ollama --version` to check the service status + process = subprocess.run( + ["ollama", "--version"], + capture_output=True, + text=True, + check=True + ) + # If no warning is present, the service is running + if "Warning: could not connect to a running Ollama instance" in process.stderr: + return False + return True + except subprocess.CalledProcessError as e: + print("Error running 'ollama --version':", e.stderr) + return False + +def start_ollama_service(): + try: + subprocess.run( + ["ollama", "serve"], + check=True, + capture_output=True, + text=True, + ) + except subprocess.CalledProcessError as e: + print("Ollama service already running.") + +def check_and_start_ollama_service(): + """ + Ensures the 'ollama' service is running. Starts it if it's not running. + """ + if is_ollama_service_running(): + print("Ollama service is already running.") + else: + print("Ollama service is not running. Attempting to start it...") + start_ollama_service() + +def check_ollama(): + try: + process = subprocess.run( + ["ollama", "--version"], + check=True, + capture_output=True, + text=True, + ) + print("Ollama is already installed!") + print("stdout:", process.stdout) + print("stderr:", process.stderr) + except subprocess.CalledProcessError as e: + print("ollama is not installed!") + ollama_choice = input("Would you like to install Ollama now? \nOptions: (Y)es, (N)o\nUser Input > ").strip().lower() + if ollama_choice in ["yes", "y"]: + install_ollama() + else: + print("Understood, exiting now.") + sys.exit(0) + print("Return code:", e.returncode) + print("stdout:", e.stdout) + print("stderr:", e.stderr) + +def check_model_installed() -> Tuple[bool, bool]: + try: + # Run the command to check if the model exists + process = subprocess.run( + ["ollama", "show", "navi-cli"], + check=True, + capture_output=True, + text=True, + ) + return True, False # Model is installed, no unexpected error + except subprocess.CalledProcessError as e: + if "Error: model 'navi-cli' not found" in e.stderr: + return False, False # Model not installed, no unexpected error + else: + print("An unexpected error occurred.") + print("stderr:", e.stderr) + return False, True # Model not installed, unexpected error + + +def install_model(): + import platform + is_windows = platform.system() == "Windows" + temp_script_path = None + script_dir = os.path.dirname(os.path.abspath(__file__)) + repo_dir = os.path.join(script_dir, "navi3b") + os.mkdir(repo_dir) + + #create dir for git clone + # then clean it after + + if not is_windows: + # Get sudo password for macOS/Linux + password = getpass("Enter your sudo password: ") + + # Create a temporary sudo-askpass script + with tempfile.NamedTemporaryFile("w", delete=False) as temp_script: + temp_script.write(f"#!/bin/bash\necho {password}\n") + temp_script_path = temp_script.name + os.chmod(temp_script_path, 0o700) + + try: + # Clone the GitHub repository + print("Cloning model from github.com/saintssec/navi3b...") + subprocess.run( + ["git", "clone", "https://github.com/saintssec/navi3b", repo_dir], + check=True, + capture_output=True, + text=True, + ) + print("Git clone succeeded!") + except subprocess.CalledProcessError as e: + print("Git clone failed!") + print("Return code:", e.returncode) + print("stdout:", e.stdout) + print("stderr:", e.stderr) + if temp_script_path: + os.remove(temp_script_path) + return + + print("Verifying model...") + model_file_path = os.path.join(repo_dir, "navi3b.modelfile") + if not os.path.exists(model_file_path): + print(f"Error: {model_file_path} not found.") + if temp_script_path: + os.remove(temp_script_path) + return + # Create the model using Ollama + try: + print("Creating model... this may take a while...") + if is_windows: + # On Windows, no sudo is needed + subprocess.run( + ["ollama", "create", "navi-cli", "-f", model_file_path], + check=True, + capture_output=True, + text=True, + ) + else: + # On macOS/Linux, use sudo with the sudo-askpass script + env = os.environ.copy() + env["SUDO_ASKPASS"] = temp_script_path + subprocess.run( + ["sudo", "ollama", "create", "navi-cli", "-f", model_file_path], + env=env, + check=True, + capture_output=True, + text=True, + ) + + print("Model creation succeeded!") + except subprocess.CalledProcessError as e: + print("Model creation failed!") + print("Return code:", e.returncode) + print("stdout:", e.stdout) + print("stderr:", e.stderr) + finally: + # Cleanup: Remove the temporary script and cloned directory + if temp_script_path: + os.remove(temp_script_path) + shutil.rmtree("./navi3b", ignore_errors=True) + +def main(): + intro_text() + user_input = input("============\nWould you like to install the Navi3b system? \nOptions: (Y)es, (N)o\nUser Input > ").strip().lower() + if user_input in ["yes", "y"]: + check_ollama() + install_model() + else: + print("Understood exiting now") + sys.exit(0) + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/mods/3binstall.py b/mods/3binstall.py deleted file mode 100644 index bb64693..0000000 --- a/mods/3binstall.py +++ /dev/null @@ -1,114 +0,0 @@ -import subprocess -import os -import sys -from getpass import getpass -import tempfile -import shutil - -from IPython.core.debugger import prompt - -password = getpass("Enter your sudo password: ") - -def intro_text(): - print("Welcome to the Navi3b model installer.\n============\nThis script will install Ollama and the Navi3b model.\n" - "It is recommended to have a stable internet connection for this process. \nIf you have any questions, please refer to the Navi documentation." - "\nIt should also be noted that this is a local AI model\nExpect it to be slower than its larger server based counterpart.") - -def install_ollama(): - try: - process = subprocess.run( - ["curl", "-fsSL", "https://ollama.com/install.sh", "|", "sh"], - check=True, - capture_output=True, - text=True, - ) - print("Ollama was installed successfully!") - print("stdout:", process.stdout) - print("stderr:", process.stderr) - except subprocess.CalledProcessError as e: - print("Ollama installation failed!") - print("Return code:", e.returncode) - print("stdout:", e.stdout) - print("stderr:", e.stderr) - -def check_ollama(): - try: - process = subprocess.run( - ["ollama", "--version"], - check=True, - capture_output=True, - text=True, - ) - print("Ollama is already installed!") - print("stdout:", process.stdout) - print("stderr:", process.stderr) - except subprocess.CalledProcessError as e: - print("ollama is not installed!") - ollama_choice = input("Would you like to install Ollama now? \nOptions: (Y)es, (N)o\nUser Input > ").strip().lower() - if ollama_choice in ["yes", "y"]: - install_ollama() - else: - print("Understood, exiting now.") - sys.exit(0) - print("Return code:", e.returncode) - print("stdout:", e.stdout) - print("stderr:", e.stderr) - -def install_navi3b(): - with tempfile.NamedTemporaryFile("w", delete=False) as temp_script: - temp_script.write(f"#!/bin/bash\necho {password}\n") - temp_script_path = temp_script.name - - os.chmod(temp_script_path, 0o700) - - env = os.environ.copy() - env["SUDO_ASKPASS"] = temp_script_path - try: - process = subprocess.run( - ["git", "clone", "https://github.com/saintssec/navi3b"], - env=env, - check=True, - capture_output=True, - text=True, - ) - print("Command succeeded!") - print("stdout:", process.stdout) - print("stderr:", process.stderr) - except subprocess.CalledProcessError as e: - print("Command failed!") - print("Return code:", e.returncode) - print("stdout:", e.stdout) - print("stderr:", e.stderr) - - try: - process = subprocess.run( - ["sudo", "ollama", "create", "navi-cli", "-f", "./navi3b/navi3b.modelfile"], - env=env, - check=True, - capture_output=True, - text=True, - ) - print("Command succeeded!") - print("stdout:", process.stdout) - print("stderr:", process.stderr) - except subprocess.CalledProcessError as e: - print("Command failed!") - print("Return code:", e.returncode) - print("stdout:", e.stdout) - print("stderr:", e.stderr) - finally: - os.remove(temp_script_path) - shutil.rmtree("./navi3b") - -def main(): - intro_text() - user_input = input("============\nWould you like to install the Navi3b system? \nOptions: (Y)es, (N)o\nUser Input > ").strip().lower() - if user_input in ["yes", "y"]: - check_ollama() - install_navi3b() - else: - print("Understood exiting now") - sys.exit(0) - -if __name__ == "__main__": - main() \ No newline at end of file diff --git a/mods/mods.py b/navi_banner.py similarity index 100% rename from mods/mods.py rename to navi_banner.py diff --git a/navi_internal.py b/navi_internal.py index 3fea16d..cdd5fff 100644 --- a/navi_internal.py +++ b/navi_internal.py @@ -8,24 +8,24 @@ import config import spacy import platform +import navi_banner from prompt_toolkit import PromptSession from prompt_toolkit.history import FileHistory -from mods import mods - - class NaviApp: - art: str = mods.art - three_b_art: str = mods.three_b_art - helpAr: str = mods.helpArt - breakline: str = mods.breakline + art: str = navi_banner.art + three_b_art: str = navi_banner.three_b_art + helpAr: str = navi_banner.helpArt + breakline: str = navi_banner.breakline ai_name_rep: str = "Navi> " server: str = config.remote port: int = config.port local: str = config.local + is_local: bool = True + script_dir = os.path.dirname(os.path.abspath(__file__)) hist_file = os.path.join(script_dir, ".navi_history") @@ -51,6 +51,9 @@ def get_user(self) -> str: def set_user(self, sys_user: str) -> None: self.user = sys_user + def set_local(self, local_state) -> None: + self.is_local = local_state + def print_message(self, text: str, include_ai_name: bool = True) -> None: to_print = text if include_ai_name: @@ -87,9 +90,12 @@ def print_message(self, text: str, include_ai_name: bool = True) -> None: def clear_terminal(self) -> None: os.system('cls' if os.name == 'nt' else 'clear') - print(self.three_b_art) + if self.is_local: + print(self.three_b_art) + else: + print(self.art) - def llm_chat(self, user_message: str, called_from_app: bool = False) -> tuple[str, int]: + def llm_chat(self, user_message: str, called_from_app: bool = False, call_remote: bool = False) -> tuple[str, int]: # Define the API endpoint and payload message_amendment = user_message if not called_from_app: @@ -100,7 +106,9 @@ def llm_chat(self, user_message: str, called_from_app: bool = False) -> tuple[st "normally.") + f"The user's OS is {platform.system()}" + ". User message:") message_amendment += user_message - url = f"http://{self.server}:{self.port}/api/chat" + url = f"http://{self.local}:{self.port}/api/chat" + if call_remote or not self.is_local: + url = f"https://{self.server}:{self.port}/api/chat" payload = { "model": "navi-cli", "messages": [{"role": "user", "content": message_amendment}] diff --git a/navi_shell.py b/navi_shell.py index 4e9f4bd..524e4fc 100644 --- a/navi_shell.py +++ b/navi_shell.py @@ -5,7 +5,7 @@ import traceback import navi_internal from navi_updater import check_version, update_script - +from install import local_model def handle_exception(exc_type, exc_value, exc_traceback) -> None: from datetime import datetime @@ -64,8 +64,39 @@ def handle_exception(exc_type, exc_value, exc_traceback) -> None: args = parser.parse_args() -def restart_navi() -> None: - os.execv(sys.executable, [sys.executable] + sys.argv + ["--skip-update"]) # nosec +def restart_navi(custom_flag: bool = False, flag: str = "") -> None: + import subprocess # nosec + command = [sys.executable] + sys.argv + (["--skip-update"] if not custom_flag else [flag]) + subprocess.run(command, check=True) + + sys.exit() + + +def local_model_check(): + if local_model.ollama_installed(): + is_installed, has_unexpected_error = local_model.check_model_installed() + if is_installed: + local_model.start_ollama_service() + # continue... + else: + if has_unexpected_error: + print("Warning: We can't verify that the local navi model is installed.") + install_decision() + else: + print("The local Navi model not installed.") + install_decision() + else: + print("Warning: Ollama is required to run the local Navi model.") + install_decision() + + +def install_decision(): + user_decision = input("(C)ontinue with --remote flag or begin (i)nstallation: ") + if user_decision == "c" or user_decision == "C": + restart_navi(True, "--remote") + if user_decision == "i" or user_decision == "I": + print("Beginning installation") + local_model.install_model() def main() -> None: navi_instance = navi_internal.navi_instance @@ -87,6 +118,11 @@ def main() -> None: update_script(download_url) if args.install: os.system('cd ./install && ./install.sh') + # if --remote flag is not set, make sure that its configured correctly + if args.remote: + navi_instance.set_local(False) + if not args.remote: + local_model_check() navi_instance.setup_navi_vocab() navi_instance.clear_terminal() navi_instance.setup_history() From 47b26750cc11970fbf8088597ec8fa7354e72f68 Mon Sep 17 00:00:00 2001 From: VAliquo Date: Sun, 15 Dec 2024 09:59:26 -0500 Subject: [PATCH 16/40] [WIP] Navi settings app --- .gitignore | 3 +- chips/SSG/navi_settings.py | 86 ++++++++++++++++++++++++++++++++++++++ default_config | 5 +++ install/local_model.py | 85 +++++++------------------------------ navi_banner.py | 2 +- navi_internal.py | 37 +++++++++------- navi_shell.py | 44 ++++++++++++------- 7 files changed, 160 insertions(+), 102 deletions(-) create mode 100644 chips/SSG/navi_settings.py create mode 100644 default_config diff --git a/.gitignore b/.gitignore index 8027b99..44ce87c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,10 @@ __pycache__/ .vscode/settings.json .DS_Store +.idea/ *.log navienv/ verification_test .navi_version .navi_history - +config diff --git a/chips/SSG/navi_settings.py b/chips/SSG/navi_settings.py new file mode 100644 index 0000000..04945b3 --- /dev/null +++ b/chips/SSG/navi_settings.py @@ -0,0 +1,86 @@ +import os + +import navi_internal +from navi import get_parameters + +command = "settings" +use = "Review and modify the Navi settings" +aliases = ['--settings'] + +script_dir = os.path.dirname(os.path.abspath(__file__)) +config_path = os.path.abspath(os.path.join(script_dir, "..", "..", "config")) +default_config_path = os.path.abspath(os.path.join(script_dir, "..", "..", "default_config")) + + +def create_config(default_values): + if not os.path.exists(config_path): + with open(config_path, "w") as file: + for key, value in default_values.items(): + file.write(f"{key}={value}\n") + + +def modify_config(key, value): + if not os.path.exists(config_path): + print(f"Unable to modify file at {config_path}.") + return + + with open(config_path, "r") as file: + lines = file.readlines() + + modified = False + with open(config_path, "w") as file: + for line in lines: + if line.startswith("#") or "=" not in line.strip(): + file.write(line) + continue + + current_key, current_value = line.strip().split("=", 1) + if current_key == key: + file.write(f"{key}={value}\n") + modified = True + else: + file.write(line) + + # Add the key-value pair if it doesn't exist + if not modified: + file.write(f"{key}={value}\n") + + +def read_config(config_path): + if not os.path.exists(config_path): + print(f"Config file not found at {config_path}.") + return {} + + config = {} + with open(config_path, "r") as file: + for line in file: + if line.startswith("#") or not line.strip(): + continue + + if "=" in line: + key, value = line.strip().split("=", 1) + value = value.split("#", 1)[0].strip() + if value.lower() == "true": + value = True + elif value.lower() == "false": + value = False + config[key.strip()] = value + return config + + +def settings_init(): + if os.path.exists(config_path): + return read_config(config_path) + else: + import getpass + default_config = read_config(default_config_path) + create_config(default_config) + modify_config("username", getpass.getuser()) + return read_config(config_path) + + +def run(arguments=None): + navi_instance = navi_internal.navi_instance + argv = get_parameters(arguments.text) + argv.pop(0) + # TODO: Show and modify settings diff --git a/default_config b/default_config new file mode 100644 index 0000000..f56f869 --- /dev/null +++ b/default_config @@ -0,0 +1,5 @@ +username=default_user +navi_name=Navi +use_local_model=True +dont_check_for_updates=False +update_branch=main \ No newline at end of file diff --git a/install/local_model.py b/install/local_model.py index 8bda60a..f61957e 100644 --- a/install/local_model.py +++ b/install/local_model.py @@ -1,33 +1,25 @@ -import subprocess import os -import sys -import tempfile import shutil +import subprocess +import tempfile from getpass import getpass from typing import Tuple -def intro_text(): - print("Welcome to the Navi3b model installer.\n============\nThis script will install Ollama and the Navi3b model.\n" - "It is recommended to have a stable internet connection for this process. \nIf you have any questions, please refer to the Navi documentation." - "\nIt should also be noted that this is a local AI model\nExpect it to be slower than its larger server based counterpart.") - - def install_ollama() -> bool: import platform try: - if platform.system() in ["Linux", "Darwin"]: # macOS/Linux + if platform.system() in ["Linux", "Darwin"]: print("Installing Ollama on macOS/Linux...") subprocess.run( ["curl", "-fsSL", "https://ollama.com/install.sh", "|", "sh"], - shell=True, # Use shell to handle the pipe + shell=True, check=True, capture_output=True, text=True, ) elif platform.system() == "Windows": print("Installing Ollama on Windows...") - # Assuming Windows uses an executable installer subprocess.run( ["powershell", "-Command", "Invoke-WebRequest -Uri https://ollama.com/install.exe -OutFile ollama_installer.exe; Start-Process -FilePath ./ollama_installer.exe -Wait"], @@ -53,30 +45,27 @@ def install_ollama() -> bool: def ollama_installed() -> bool: try: subprocess.run( - ["ollama", "--version"], - check=True, - capture_output=True, - text=True, - ) + ["ollama", "--version"], + check=True, + capture_output=True, + text=True, + ) return True except subprocess.CalledProcessError as e: return False + except FileNotFoundError as e: + return False def is_ollama_service_running() -> bool: - """ - Checks if the 'ollama' service is running by executing 'ollama --version'. - If the service is not running, it outputs a warning. - """ try: - # Run `ollama --version` to check the service status process = subprocess.run( ["ollama", "--version"], capture_output=True, text=True, check=True ) - # If no warning is present, the service is running + # If no warning is present, the service is running. Ollama doesn't have a proper check... if "Warning: could not connect to a running Ollama instance" in process.stderr: return False return True @@ -84,6 +73,7 @@ def is_ollama_service_running() -> bool: print("Error running 'ollama --version':", e.stderr) return False + def start_ollama_service(): try: subprocess.run( @@ -93,40 +83,16 @@ def start_ollama_service(): text=True, ) except subprocess.CalledProcessError as e: - print("Ollama service already running.") + print("Ollama service already running or could not start.") + def check_and_start_ollama_service(): - """ - Ensures the 'ollama' service is running. Starts it if it's not running. - """ if is_ollama_service_running(): print("Ollama service is already running.") else: print("Ollama service is not running. Attempting to start it...") start_ollama_service() -def check_ollama(): - try: - process = subprocess.run( - ["ollama", "--version"], - check=True, - capture_output=True, - text=True, - ) - print("Ollama is already installed!") - print("stdout:", process.stdout) - print("stderr:", process.stderr) - except subprocess.CalledProcessError as e: - print("ollama is not installed!") - ollama_choice = input("Would you like to install Ollama now? \nOptions: (Y)es, (N)o\nUser Input > ").strip().lower() - if ollama_choice in ["yes", "y"]: - install_ollama() - else: - print("Understood, exiting now.") - sys.exit(0) - print("Return code:", e.returncode) - print("stdout:", e.stdout) - print("stderr:", e.stderr) def check_model_installed() -> Tuple[bool, bool]: try: @@ -155,9 +121,6 @@ def install_model(): repo_dir = os.path.join(script_dir, "navi3b") os.mkdir(repo_dir) - #create dir for git clone - # then clean it after - if not is_windows: # Get sudo password for macOS/Linux password = getpass("Enter your sudo password: ") @@ -166,10 +129,9 @@ def install_model(): with tempfile.NamedTemporaryFile("w", delete=False) as temp_script: temp_script.write(f"#!/bin/bash\necho {password}\n") temp_script_path = temp_script.name - os.chmod(temp_script_path, 0o700) + os.remove(temp_script_path) try: - # Clone the GitHub repository print("Cloning model from github.com/saintssec/navi3b...") subprocess.run( ["git", "clone", "https://github.com/saintssec/navi3b", repo_dir], @@ -198,7 +160,6 @@ def install_model(): try: print("Creating model... this may take a while...") if is_windows: - # On Windows, no sudo is needed subprocess.run( ["ollama", "create", "navi-cli", "-f", model_file_path], check=True, @@ -206,7 +167,6 @@ def install_model(): text=True, ) else: - # On macOS/Linux, use sudo with the sudo-askpass script env = os.environ.copy() env["SUDO_ASKPASS"] = temp_script_path subprocess.run( @@ -228,16 +188,3 @@ def install_model(): if temp_script_path: os.remove(temp_script_path) shutil.rmtree("./navi3b", ignore_errors=True) - -def main(): - intro_text() - user_input = input("============\nWould you like to install the Navi3b system? \nOptions: (Y)es, (N)o\nUser Input > ").strip().lower() - if user_input in ["yes", "y"]: - check_ollama() - install_model() - else: - print("Understood exiting now") - sys.exit(0) - -if __name__ == "__main__": - main() \ No newline at end of file diff --git a/navi_banner.py b/navi_banner.py index 361c46a..766850c 100644 --- a/navi_banner.py +++ b/navi_banner.py @@ -11,7 +11,7 @@ | / | / /___ __ __(_) | | / |/ / __ `/ | / / / | | / /| / /_/ /| |/ / / | -| /_/ |_/\__,_/ |___/_/ v{versionNum} |\ +| /_/ |_/\__,_/ |___/_/ v{versionNum} | |===================================================| | Disclaimer: Saints Security Group LLC does not | | condone or support illegal activity and assumes | diff --git a/navi_internal.py b/navi_internal.py index cdd5fff..08b99c7 100644 --- a/navi_internal.py +++ b/navi_internal.py @@ -1,24 +1,26 @@ -import requests +import json import os -import textwrap +import platform import random +import textwrap import time + +import requests +import spacy +from prompt_toolkit import PromptSession +from prompt_toolkit.history import FileHistory + import chips -import json import config -import spacy -import platform import navi_banner -from prompt_toolkit import PromptSession -from prompt_toolkit.history import FileHistory class NaviApp: art: str = navi_banner.art three_b_art: str = navi_banner.three_b_art helpAr: str = navi_banner.helpArt breakline: str = navi_banner.breakline - ai_name_rep: str = "Navi> " + ai_name_rep: str = "Navi" server: str = config.remote port: int = config.port @@ -54,10 +56,13 @@ def set_user(self, sys_user: str) -> None: def set_local(self, local_state) -> None: self.is_local = local_state + def set_navi_name(self, navi_name: str) -> None: + self.ai_name_rep = navi_name + def print_message(self, text: str, include_ai_name: bool = True) -> None: to_print = text if include_ai_name: - to_print = self.ai_name_rep + text + to_print = self.ai_name_rep + "> " + text sleep_times = { (0, 0.1): 0.0, (0.1, 0.2): 0.05, @@ -80,7 +85,7 @@ def print_message(self, text: str, include_ai_name: bool = True) -> None: wrapped_lines = textwrap.fill(line, width=wrap_width) for char in wrapped_lines: print(char, end="", flush=True) - random_num = random.uniform(0, 1) # nosec + random_num = random.uniform(0, 1) # nosec for range_tuple, sleep_time in sleep_times.items(): if range_tuple[0] <= random_num < range_tuple[1]: time.sleep(sleep_time) @@ -100,15 +105,15 @@ def llm_chat(self, user_message: str, called_from_app: bool = False, call_remote message_amendment = user_message if not called_from_app: message_amendment = ( - ("If the user message has a terminal command request, provide the following 'TERMINAL OUTPUT {" - "terminal code to execute request (no not encapsulate command in quotes)}' and NOTHING " - "ELSE. Otherwise continue to communicate" - "normally.") + - f"The user's OS is {platform.system()}" + ". User message:") + ("If the user message has a terminal command request, provide the following 'TERMINAL OUTPUT {" + "terminal code to execute request (no not encapsulate command in quotes)}' and NOTHING " + "ELSE. Otherwise continue to communicate" + "normally.") + + f"The user's OS is {platform.system()}" + ". User message:") message_amendment += user_message url = f"http://{self.local}:{self.port}/api/chat" if call_remote or not self.is_local: - url = f"https://{self.server}:{self.port}/api/chat" + url = f"http://{self.server}:{self.port}/api/chat" payload = { "model": "navi-cli", "messages": [{"role": "user", "content": message_amendment}] diff --git a/navi_shell.py b/navi_shell.py index 524e4fc..35071f1 100644 --- a/navi_shell.py +++ b/navi_shell.py @@ -1,15 +1,19 @@ +import argparse +import getpass import os import sys -import getpass -import argparse import traceback + import navi_internal -from navi_updater import check_version, update_script +from chips import SSG from install import local_model +from navi_updater import check_version, update_script +from colorama import Fore + def handle_exception(exc_type, exc_value, exc_traceback) -> None: from datetime import datetime - + if issubclass(exc_type, KeyboardInterrupt): sys.__excepthook__(exc_type, exc_value, exc_traceback) return @@ -26,7 +30,8 @@ def handle_exception(exc_type, exc_value, exc_traceback) -> None: log_file_path = os.path.abspath(log_file) - print(f"\nDang! Navi crashed. A crash log has been created at:\n{log_file_path}. \n\nYou can create a new Navi GitHub issue here: \nhttps://github.com/SaintsSec/Navi/issues. \n\nThank you for helping us make Navi better!") + print( + f"\nDang! Navi crashed. A crash log has been created at:\n{log_file_path}. \n\nYou can create a new Navi GitHub issue here: \nhttps://github.com/SaintsSec/Navi/issues. \n\nThank you for helping us make Navi better!") print("\nWould you like to:") print("1) Open the crash log") @@ -48,6 +53,7 @@ def handle_exception(exc_type, exc_value, exc_traceback) -> None: sys.exit(1) + sys.excepthook = handle_exception user = getpass.getuser() @@ -64,6 +70,7 @@ def handle_exception(exc_type, exc_value, exc_traceback) -> None: args = parser.parse_args() + def restart_navi(custom_flag: bool = False, flag: str = "") -> None: import subprocess # nosec command = [sys.executable] + sys.argv + (["--skip-update"] if not custom_flag else [flag]) @@ -77,30 +84,37 @@ def local_model_check(): is_installed, has_unexpected_error = local_model.check_model_installed() if is_installed: local_model.start_ollama_service() - # continue... else: if has_unexpected_error: - print("Warning: We can't verify that the local navi model is installed.") + print(f"{Fore.YELLOW}Warning: We can't verify that the local navi model is installed.{Fore.RESET}") install_decision() else: - print("The local Navi model not installed.") + print(f"{Fore.YELLOW}The local Navi model not installed.{Fore.RESET}") install_decision() else: - print("Warning: Ollama is required to run the local Navi model.") + print(f"{Fore.YELLOW}Warning: Ollama is required to run the local Navi model.{Fore.RESET}") install_decision() def install_decision(): user_decision = input("(C)ontinue with --remote flag or begin (i)nstallation: ") if user_decision == "c" or user_decision == "C": + print("To never see this prompt again, set 'use_local_model' to False using the 'settings'" + "command.") + default_input = input("Would you like us to set it for you? (Y)es, (N)o, please restart: ") + if default_input.lower() == "y" or default_input.lower() == "yes": + SSG.navi_settings.modify_config("use_local_model", False) restart_navi(True, "--remote") if user_decision == "i" or user_decision == "I": print("Beginning installation") local_model.install_model() + def main() -> None: navi_instance = navi_internal.navi_instance - navi_instance.set_user(user) + navi_settings = SSG.navi_settings.settings_init() + navi_instance.set_user(navi_settings["username"]) + navi_instance.set_navi_name(navi_settings["navi_name"]) try: if args.q: response_message, http_status = navi_instance.llm_chat( @@ -112,25 +126,25 @@ def main() -> None: f"{response_message if http_status == 200 else f'Trouble connecting to Navi server.'}" ) exit(0) - if not args.noupdate and not args.skip_update: + if not args.noupdate and not args.skip_update and not navi_settings["dont_check_for_updates"]: download_url = check_version(args.edge) if download_url: update_script(download_url) if args.install: os.system('cd ./install && ./install.sh') - # if --remote flag is not set, make sure that its configured correctly - if args.remote: + if args.remote or not navi_settings["use_local_model"]: navi_instance.set_local(False) - if not args.remote: + if not args.remote and navi_settings["use_local_model"]: local_model_check() navi_instance.setup_navi_vocab() navi_instance.clear_terminal() navi_instance.setup_history() navi_instance.chat_with_navi() - navi_instance.print_message(f"How can I help you {user}") + navi_instance.print_message(f"How can I help you, {user}") except KeyboardInterrupt: navi_instance.print_message(f"\nKeyboard interrupt has been registered, talk soon {user}!") exit(0) + if __name__ == "__main__": main() From 8d92b1ef071bf4190955211d7bb7805930ef4527 Mon Sep 17 00:00:00 2001 From: VAliquo Date: Sun, 15 Dec 2024 10:40:40 -0500 Subject: [PATCH 17/40] [UPDATE] macOS Ollama installer --- install/local_model.py | 55 +++++++++++++++++++++++++++++++----------- 1 file changed, 41 insertions(+), 14 deletions(-) diff --git a/install/local_model.py b/install/local_model.py index f61957e..7ab1d6c 100644 --- a/install/local_model.py +++ b/install/local_model.py @@ -9,20 +9,40 @@ def install_ollama() -> bool: import platform try: - if platform.system() in ["Linux", "Darwin"]: - print("Installing Ollama on macOS/Linux...") + if platform.system() in ["Linux"]: + print("Installing Ollama for Linux...") + curl_process = subprocess.run( + ["curl", "-fsSL", "https://ollama.com/install.sh"], + check=True, + capture_output=True, + text=True, + ) + subprocess.run( - ["curl", "-fsSL", "https://ollama.com/install.sh", "|", "sh"], - shell=True, + ["sh"], + input=curl_process.stdout, check=True, capture_output=True, text=True, ) + elif platform.system() in ["Darwin"]: + print("Installing Ollama for macOS...") + print("Checking for Homebrew...") + brew_check = subprocess.run(["brew", "--version"], capture_output=True, text=True) + if brew_check.returncode != 0: + print("Homebrew is not installed. Please install Homebrew first:") + print("Visit: https://brew.sh/") + return False + + print("Installing Ollama via Homebrew...") + subprocess.run(["brew", "install", "ollama"], check=True) + print("Ollama installed successfully on macOS!") + start_ollama_service() elif platform.system() == "Windows": - print("Installing Ollama on Windows...") + print("Installing Ollama for Windows...") subprocess.run( ["powershell", "-Command", - "Invoke-WebRequest -Uri https://ollama.com/install.exe -OutFile ollama_installer.exe; Start-Process -FilePath ./ollama_installer.exe -Wait"], + "Invoke-WebRequest -Uri https://ollama.com/download/OllamaSetup.exe -OutFile ollama_installer.exe; Start-Process -FilePath ./ollama_installer.exe -Wait"], shell=True, check=True, capture_output=True, @@ -76,14 +96,17 @@ def is_ollama_service_running() -> bool: def start_ollama_service(): try: - subprocess.run( + # Start Ollama serve in the background + process = subprocess.Popen( ["ollama", "serve"], - check=True, - capture_output=True, - text=True, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, ) - except subprocess.CalledProcessError as e: - print("Ollama service already running or could not start.") + print(f"Ollama service started with PID: {process.pid}") + return process + except Exception as e: + print(f"Failed to start Ollama service: {e}") + return None def check_and_start_ollama_service(): @@ -120,7 +143,10 @@ def install_model(): script_dir = os.path.dirname(os.path.abspath(__file__)) repo_dir = os.path.join(script_dir, "navi3b") os.mkdir(repo_dir) - + if not ollama_installed(): + if not install_ollama(): + print("Failed to install Ollama. Visit https://ollama.com and install Ollama manually.") + return if not is_windows: # Get sudo password for macOS/Linux password = getpass("Enter your sudo password: ") @@ -147,6 +173,7 @@ def install_model(): print("stderr:", e.stderr) if temp_script_path: os.remove(temp_script_path) + shutil.rmtree(repo_dir, ignore_errors=True) return print("Verifying model...") @@ -187,4 +214,4 @@ def install_model(): # Cleanup: Remove the temporary script and cloned directory if temp_script_path: os.remove(temp_script_path) - shutil.rmtree("./navi3b", ignore_errors=True) + shutil.rmtree(repo_dir, ignore_errors=True) From 7cc03dc8255874d16804f9e9aa23c6258279d132 Mon Sep 17 00:00:00 2001 From: VAliquo Date: Sun, 15 Dec 2024 12:14:51 -0500 Subject: [PATCH 18/40] [FIX] Starting ollama and file not found exception --- install/local_model.py | 10 ++++------ navi_shell.py | 2 ++ 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/install/local_model.py b/install/local_model.py index 7ab1d6c..e70e6ee 100644 --- a/install/local_model.py +++ b/install/local_model.py @@ -143,10 +143,12 @@ def install_model(): script_dir = os.path.dirname(os.path.abspath(__file__)) repo_dir = os.path.join(script_dir, "navi3b") os.mkdir(repo_dir) + if not ollama_installed(): if not install_ollama(): print("Failed to install Ollama. Visit https://ollama.com and install Ollama manually.") return + if not is_windows: # Get sudo password for macOS/Linux password = getpass("Enter your sudo password: ") @@ -155,7 +157,6 @@ def install_model(): with tempfile.NamedTemporaryFile("w", delete=False) as temp_script: temp_script.write(f"#!/bin/bash\necho {password}\n") temp_script_path = temp_script.name - os.remove(temp_script_path) try: print("Cloning model from github.com/saintssec/navi3b...") @@ -171,8 +172,6 @@ def install_model(): print("Return code:", e.returncode) print("stdout:", e.stdout) print("stderr:", e.stderr) - if temp_script_path: - os.remove(temp_script_path) shutil.rmtree(repo_dir, ignore_errors=True) return @@ -180,9 +179,8 @@ def install_model(): model_file_path = os.path.join(repo_dir, "navi3b.modelfile") if not os.path.exists(model_file_path): print(f"Error: {model_file_path} not found.") - if temp_script_path: - os.remove(temp_script_path) return + # Create the model using Ollama try: print("Creating model... this may take a while...") @@ -212,6 +210,6 @@ def install_model(): print("stderr:", e.stderr) finally: # Cleanup: Remove the temporary script and cloned directory - if temp_script_path: + if temp_script_path and os.path.exists(temp_script_path): os.remove(temp_script_path) shutil.rmtree(repo_dir, ignore_errors=True) diff --git a/navi_shell.py b/navi_shell.py index 35071f1..6d20420 100644 --- a/navi_shell.py +++ b/navi_shell.py @@ -81,6 +81,8 @@ def restart_navi(custom_flag: bool = False, flag: str = "") -> None: def local_model_check(): if local_model.ollama_installed(): + if not local_model.is_ollama_service_running(): + local_model.start_ollama_service() is_installed, has_unexpected_error = local_model.check_model_installed() if is_installed: local_model.start_ollama_service() From bee634d6ad8b826f54012289a48b1ffba454882c Mon Sep 17 00:00:00 2001 From: VAliquo Date: Sun, 15 Dec 2024 12:15:14 -0500 Subject: [PATCH 19/40] Update .gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 44ce87c..67dce19 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ verification_test .navi_version .navi_history config +qodana.yaml From 49b1f20fa6f06747091321c6f2d69be04501defa Mon Sep 17 00:00:00 2001 From: VAliquo Date: Sun, 15 Dec 2024 12:27:53 -0500 Subject: [PATCH 20/40] [UPDATE] Settings user interaction --- chips/SSG/navi_settings.py | 35 +++++++++++++++++++++++++++++++---- 1 file changed, 31 insertions(+), 4 deletions(-) diff --git a/chips/SSG/navi_settings.py b/chips/SSG/navi_settings.py index 04945b3..d8b712a 100644 --- a/chips/SSG/navi_settings.py +++ b/chips/SSG/navi_settings.py @@ -1,7 +1,7 @@ import os import navi_internal -from navi import get_parameters +from navi_shell import restart_navi command = "settings" use = "Review and modify the Navi settings" @@ -81,6 +81,33 @@ def settings_init(): def run(arguments=None): navi_instance = navi_internal.navi_instance - argv = get_parameters(arguments.text) - argv.pop(0) - # TODO: Show and modify settings + + current_settings = settings_init() + + while True: + navi_instance.clear_terminal() + print("\nCurrent Settings:") + for key, value in current_settings.items(): + print(f" {key}: {value}") + + print("\nOptions:") + print(" [1] Update a setting") + print(" [2] Exit") + + choice = input("\nEnter your choice: ").strip() + + if choice == "1": + key_to_modify = input("Enter the setting key to update: ").strip() + if key_to_modify in current_settings: + new_value = input( + f"Enter the new value for '{key_to_modify}' (current: {current_settings[key_to_modify]}): ").strip() + modify_config(key_to_modify, new_value) + print(f"'{key_to_modify}' updated successfully!") + current_settings = settings_init() + else: + print(f"Error: '{key_to_modify}' is not a valid setting.") + elif choice == "2": + restart_navi() + break + else: + print("Invalid choice. Please try again.") From 78e9003006f41cca46c6f735a175146c4b40d8f8 Mon Sep 17 00:00:00 2001 From: VAliquo Date: Sun, 15 Dec 2024 12:37:54 -0500 Subject: [PATCH 21/40] [FIX] Misaligned character Bothered the hell out of me. --- navi_banner.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/navi_banner.py b/navi_banner.py index 766850c..6e2dd4a 100644 --- a/navi_banner.py +++ b/navi_banner.py @@ -11,7 +11,7 @@ | / | / /___ __ __(_) | | / |/ / __ `/ | / / / | | / /| / /_/ /| |/ / / | -| /_/ |_/\__,_/ |___/_/ v{versionNum} | +| /_/ |_/\__,_/ |___/_/ v{versionNum} | |===================================================| | Disclaimer: Saints Security Group LLC does not | | condone or support illegal activity and assumes | @@ -24,7 +24,7 @@ | / | / /___ __ __(_)__ // /_ | | / |/ / __ `/ | / / / /_ Date: Mon, 16 Dec 2024 10:24:55 -0500 Subject: [PATCH 22/40] [UPDATE] Chip template --- chips/SSG/chip-template.txt | 4 ++++ navi_banner.py | 2 +- navi_shell.py | 15 ++++++++++++--- 3 files changed, 17 insertions(+), 4 deletions(-) diff --git a/chips/SSG/chip-template.txt b/chips/SSG/chip-template.txt index 3ef3365..3042028 100644 --- a/chips/SSG/chip-template.txt +++ b/chips/SSG/chip-template.txt @@ -1,5 +1,9 @@ import navi_internal +# To get a dictionary of current navi settings, and modify dictionary. Some modifications +# might require app restart. +#from navi_shell import get_navi_settings, modify_navi_settings + # Chip documentation: https://github.com/SaintsSec/Navi/wiki/4.-Developing-Chips-%E2%80%90-Indepth command: str = "{{CHIP_NAME}}" diff --git a/navi_banner.py b/navi_banner.py index 6e2dd4a..8eea2b3 100644 --- a/navi_banner.py +++ b/navi_banner.py @@ -11,7 +11,7 @@ | / | / /___ __ __(_) | | / |/ / __ `/ | / / / | | / /| / /_/ /| |/ / / | -| /_/ |_/\__,_/ |___/_/ v{versionNum} | +| /_/ |_/\__,_/ |___/_/ v{versionNum} | |===================================================| | Disclaimer: Saints Security Group LLC does not | | condone or support illegal activity and assumes | diff --git a/navi_shell.py b/navi_shell.py index 6d20420..57a8551 100644 --- a/navi_shell.py +++ b/navi_shell.py @@ -4,11 +4,12 @@ import sys import traceback +from colorama import Fore + import navi_internal from chips import SSG from install import local_model from navi_updater import check_version, update_script -from colorama import Fore def handle_exception(exc_type, exc_value, exc_traceback) -> None: @@ -105,16 +106,24 @@ def install_decision(): "command.") default_input = input("Would you like us to set it for you? (Y)es, (N)o, please restart: ") if default_input.lower() == "y" or default_input.lower() == "yes": - SSG.navi_settings.modify_config("use_local_model", False) + modify_navi_settings("use_local_model", False) restart_navi(True, "--remote") if user_decision == "i" or user_decision == "I": print("Beginning installation") local_model.install_model() +def get_navi_settings() -> dict: + return SSG.navi_settings.settings_init() + + +def modify_navi_settings(key, value) -> None: + SSG.navi_settings.modify_config(key, value) + + def main() -> None: navi_instance = navi_internal.navi_instance - navi_settings = SSG.navi_settings.settings_init() + navi_settings = get_navi_settings() navi_instance.set_user(navi_settings["username"]) navi_instance.set_navi_name(navi_settings["navi_name"]) try: From fe975520a81e4f61bff92141337a4787d0381a8d Mon Sep 17 00:00:00 2001 From: VAliquo Date: Mon, 16 Dec 2024 10:31:51 -0500 Subject: [PATCH 23/40] [UPDATE] Rename vars and remove unused vars --- chips/SSG/navi_settings.py | 8 ++++---- install/local_model.py | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/chips/SSG/navi_settings.py b/chips/SSG/navi_settings.py index d8b712a..ec6a162 100644 --- a/chips/SSG/navi_settings.py +++ b/chips/SSG/navi_settings.py @@ -46,13 +46,13 @@ def modify_config(key, value): file.write(f"{key}={value}\n") -def read_config(config_path): - if not os.path.exists(config_path): - print(f"Config file not found at {config_path}.") +def read_config(path_to_config): + if not os.path.exists(path_to_config): + print(f"Config file not found at {path_to_config}.") return {} config = {} - with open(config_path, "r") as file: + with open(path_to_config, "r") as file: for line in file: if line.startswith("#") or not line.strip(): continue diff --git a/install/local_model.py b/install/local_model.py index e70e6ee..e4d9348 100644 --- a/install/local_model.py +++ b/install/local_model.py @@ -71,9 +71,9 @@ def ollama_installed() -> bool: text=True, ) return True - except subprocess.CalledProcessError as e: + except subprocess.CalledProcessError: return False - except FileNotFoundError as e: + except FileNotFoundError: return False @@ -120,7 +120,7 @@ def check_and_start_ollama_service(): def check_model_installed() -> Tuple[bool, bool]: try: # Run the command to check if the model exists - process = subprocess.run( + subprocess.run( ["ollama", "show", "navi-cli"], check=True, capture_output=True, From 600fa6d0f235b88f66988a15fc95963f66eb4222 Mon Sep 17 00:00:00 2001 From: VAliquo Date: Mon, 16 Dec 2024 10:51:11 -0500 Subject: [PATCH 24/40] [ADD] Config for update branch --- navi_shell.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/navi_shell.py b/navi_shell.py index 57a8551..c0bc693 100644 --- a/navi_shell.py +++ b/navi_shell.py @@ -138,6 +138,8 @@ def main() -> None: ) exit(0) if not args.noupdate and not args.skip_update and not navi_settings["dont_check_for_updates"]: + if navi_settings["update_branch"] == "edge": + args.edge = True download_url = check_version(args.edge) if download_url: update_script(download_url) From 20c0d1a9392a11de12f1f2355ef8c2d302cb7afc Mon Sep 17 00:00:00 2001 From: VAliquo Date: Mon, 16 Dec 2024 14:21:03 -0500 Subject: [PATCH 25/40] [FIX] Windows ollama installer --- install/local_model.py | 53 ++++++++++++++++++++++++++++++++++++++---- 1 file changed, 49 insertions(+), 4 deletions(-) diff --git a/install/local_model.py b/install/local_model.py index e4d9348..f95a3b7 100644 --- a/install/local_model.py +++ b/install/local_model.py @@ -6,8 +6,11 @@ from typing import Tuple +import subprocess +import platform +import os + def install_ollama() -> bool: - import platform try: if platform.system() in ["Linux"]: print("Installing Ollama for Linux...") @@ -37,17 +40,48 @@ def install_ollama() -> bool: print("Installing Ollama via Homebrew...") subprocess.run(["brew", "install", "ollama"], check=True) print("Ollama installed successfully on macOS!") - start_ollama_service() elif platform.system() == "Windows": print("Installing Ollama for Windows...") + + # Download the installer + subprocess.run( + ["powershell", "-Command", + "Invoke-WebRequest -Uri https://ollama.com/download/OllamaSetup.exe -OutFile ollama_installer.exe"], + shell=True, + check=True, + capture_output=True, + text=True, + ) + + # Run the installer subprocess.run( ["powershell", "-Command", - "Invoke-WebRequest -Uri https://ollama.com/download/OllamaSetup.exe -OutFile ollama_installer.exe; Start-Process -FilePath ./ollama_installer.exe -Wait"], + "Start-Process -FilePath ./ollama_installer.exe"], shell=True, check=True, capture_output=True, text=True, ) + + # Prompt the user to continue. Really tried to wait for installer but it kept detaching + print("Please complete the Ollama installation. Once done, type 'c' and press Enter to continue.") + while input("Type 'c' to continue: ").strip().lower() != 'c': + print("Invalid input. Please type 'c' to continue.") + + # Verify the installation + print("Verifying Ollama installation...") + result = subprocess.run( + ["ollama", "--version"], + capture_output=True, + text=True, + ) + + if result.returncode == 0: + print(f"Ollama installed successfully: {result.stdout.strip()}") + else: + print("Ollama installation verification failed!") + print("stderr:", result.stderr.strip()) + return False else: print("Unsupported platform for Ollama installation.") return False @@ -60,6 +94,13 @@ def install_ollama() -> bool: print("stdout:", e.stdout) print("stderr:", e.stderr) return False + finally: + if platform.system() == "Windows" and os.path.exists("ollama_installer.exe"): + try: + os.remove("ollama_installer.exe") + except Exception as e: + print(f"Failed to remove installer: {e}") + def ollama_installed() -> bool: @@ -142,7 +183,11 @@ def install_model(): temp_script_path = None script_dir = os.path.dirname(os.path.abspath(__file__)) repo_dir = os.path.join(script_dir, "navi3b") - os.mkdir(repo_dir) + if not os.path.exists(repo_dir): + os.mkdir(repo_dir) + else: + shutil.rmtree(repo_dir, ignore_errors=True) + os.mkdir(repo_dir) if not ollama_installed(): if not install_ollama(): From 924b6c8867618deb4cb8534069e82293b0eac588 Mon Sep 17 00:00:00 2001 From: VAliquo Date: Mon, 16 Dec 2024 14:55:31 -0500 Subject: [PATCH 26/40] [UPDATE] Clean up Windows Ollama installation --- install/local_model.py | 19 +++---------------- 1 file changed, 3 insertions(+), 16 deletions(-) diff --git a/install/local_model.py b/install/local_model.py index f95a3b7..afe561e 100644 --- a/install/local_model.py +++ b/install/local_model.py @@ -67,25 +67,12 @@ def install_ollama() -> bool: print("Please complete the Ollama installation. Once done, type 'c' and press Enter to continue.") while input("Type 'c' to continue: ").strip().lower() != 'c': print("Invalid input. Please type 'c' to continue.") - - # Verify the installation - print("Verifying Ollama installation...") - result = subprocess.run( - ["ollama", "--version"], - capture_output=True, - text=True, - ) - - if result.returncode == 0: - print(f"Ollama installed successfully: {result.stdout.strip()}") - else: - print("Ollama installation verification failed!") - print("stderr:", result.stderr.strip()) - return False else: print("Unsupported platform for Ollama installation.") return False - + if not ollama_installed(): + print("Oh dear, something went wrong installing Ollama.") + return False print("Ollama installation succeeded!") return True except subprocess.CalledProcessError as e: From 5e6c6a57f7a1f1b79e870e3c4fb722779eca6c60 Mon Sep 17 00:00:00 2001 From: VAliquo Date: Mon, 16 Dec 2024 14:55:48 -0500 Subject: [PATCH 27/40] [UPDATE] Improve prompt --- navi_internal.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/navi_internal.py b/navi_internal.py index 08b99c7..53215c1 100644 --- a/navi_internal.py +++ b/navi_internal.py @@ -105,7 +105,7 @@ def llm_chat(self, user_message: str, called_from_app: bool = False, call_remote message_amendment = user_message if not called_from_app: message_amendment = ( - ("If the user message has a terminal command request, provide the following 'TERMINAL OUTPUT {" + ("If the user message has a request that requires the terminal to execute, provide the following 'TERMINAL OUTPUT {" "terminal code to execute request (no not encapsulate command in quotes)}' and NOTHING " "ELSE. Otherwise continue to communicate" "normally.") + @@ -125,7 +125,6 @@ def llm_chat(self, user_message: str, called_from_app: bool = False, call_remote # Check if the response is valid if response.status_code == 200: response_text = response.text - # Split the response into lines and parse each line as JSON messages = [line for line in response_text.split('\n') if line] extracted_responses = [] From f44b6e4ca25de5b7cde378363993a63f2d2b5908 Mon Sep 17 00:00:00 2001 From: Alex Kollar Date: Mon, 16 Dec 2024 15:38:13 -0500 Subject: [PATCH 28/40] Fixed install folder being deleted. --- install/install.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install/install.sh b/install/install.sh index d50bb8f..271bf00 100755 --- a/install/install.sh +++ b/install/install.sh @@ -83,7 +83,7 @@ copy_navi() { cleanup_install_directory() { cd /opt/Navi || exit 1 - declare -a files_to_remove=("install" "README.md" ".git" ".gitignore") + declare -a files_to_remove=("README.md" ".git" ".gitignore") for item in "${files_to_remove[@]}"; do if [ -e "$item" ]; then From 8ba4a8c6299118d5bcccfc24c8ec0fb3341687ea Mon Sep 17 00:00:00 2001 From: Alex Kollar <89718570+AlexKollar@users.noreply.github.com> Date: Sat, 21 Dec 2024 07:34:16 -0500 Subject: [PATCH 29/40] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 123b9a2..8338491 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@

Navi | CLI - An innovation in cybersec AI

-

v0.6.5 - "CSI - Con"

+

v0.6.6 - "Specter"

## 🤝 Sponsors / Endorsements: Thank you so much! From 2f9583d4d2ee3940baf9a68a46b6551728a503ab Mon Sep 17 00:00:00 2001 From: Alex Kollar <89718570+AlexKollar@users.noreply.github.com> Date: Sat, 21 Dec 2024 07:54:54 -0500 Subject: [PATCH 30/40] Update README.md Updated README for edge branch --- README.md | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 8338491..21d3a84 100644 --- a/README.md +++ b/README.md @@ -15,14 +15,11 @@ ## ✨ **Key Features of Navi v0.6.5** -- **Upgraded Navi Shell** - The shell can now execute system commands, without breaking the flow of conversation. See more below! -- **Navi chips Upgrade** - The new alias variable within the custom scripts allow for Navi to execute scripts right from the chat. Once again not disrupting the flow. -- **Chip Creators Guide** - We are in the process of streamlining documentation on making custom chips. -- **Navi Nmap Chip** - We moved the Nmap Script over to being its own chip. +- **Navi3b** - The first iteration of our local AI system. Currently running Llama3.2-3b with ollama in the background. +- **Streamlined 3b install** - The install process is included the first time you launch navi (from edge currently) `navi --edge` +- **Navi chips Upgrade** - The chips system has been upgraded yet again with some qol, as well as a chip creator + template +- **Chip Creators Guide** - The chip documentation has been updated to reflect recent changes to the sytem. - **Wiki Re-write** - With new power comes new documentation -- **Llama3.2 Integration** - We are running Meta's Llama AI on the backend. -- **[Navi Mind](https://github.com/SaintsSec/Navi-Mind)** - Planning phase of the first Navi AI Model we are calling *Navi Mind*. -- **[Navi Public Training](https://github.com/SaintsSec/Navi-Training)** - We want to be transparent about the data going into Navi. So we are building out a whole repo just for that. ### **Work In Progress** From 73d2dcb0788da200d0b1134c188b121396d6a8a2 Mon Sep 17 00:00:00 2001 From: VAliquo Date: Wed, 1 Jan 2025 13:36:53 -0500 Subject: [PATCH 31/40] [ADD] Memory functionality --- .gitignore | 1 + chips/SSG/navi_memories.py | 73 +++++++++++++++++++++++++++++++++ default_config | 4 +- navi_internal.py | 83 ++++++++++++++++++++++++++++++++++---- navi_shell.py | 2 + 5 files changed, 155 insertions(+), 8 deletions(-) create mode 100644 chips/SSG/navi_memories.py diff --git a/.gitignore b/.gitignore index 67dce19..4f7ccc7 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,4 @@ verification_test .navi_history config qodana.yaml +memories/ diff --git a/chips/SSG/navi_memories.py b/chips/SSG/navi_memories.py new file mode 100644 index 0000000..9b98b4c --- /dev/null +++ b/chips/SSG/navi_memories.py @@ -0,0 +1,73 @@ +import navi_internal +import os + +command: str = "memory" +use: str = "Manage chat memory sessions." +aliases: list = ['session', 'memory'] +params: dict = { + 'list': 'List all available sessions and indicate the active one', + 'create ': 'Create a new session with the specified name', + 'set-default ': 'Set the default session', + 'set-active ': 'Set the active session', + '-help': 'Display help information', + '-h': 'Display help information', +} + +memory_dir = "memories" + +# Ensure the memory directory exists +if not os.path.exists(memory_dir): + os.makedirs(memory_dir) + +help_params: tuple = ('-help', '-h') + +def print_params() -> None: + """Print available parameters and descriptions.""" + print(f"{'Parameter':<20} | {'Description'}") + print("-" * 50) + for param, description in params.items(): + print(f"{param:<20} | {description}") + +def list_sessions() -> None: + sessions = os.listdir(memory_dir) + print("Available Sessions:") + for session in sessions: + session_name = os.path.splitext(session)[0] + from navi_shell import get_navi_settings + if session_name == get_navi_settings()["session"]: + print(f"* {session_name} (Active)") + else: + print(f" {session_name}") + +def does_session_exist(session_name) -> bool: + if not os.path.exists(os.path.join(memory_dir, f"{session_name}.json")): + print(f"Session '{session_name}' does not exist.") + return False + return True + +def run(arguments=None) -> None: + navi_instance = navi_internal.navi_instance + + arg_array = arguments.text.split() + arg_array.pop(0) # Remove the command itself + + if not arg_array: + navi_instance.print_message("No arguments provided. Use '-help' for more information.") + return + + match arg_array: + case ['list']: + list_sessions() + case ['create', session_name]: + navi_instance.create_new_session(session_name) + case ['set-default', session_name]: + if does_session_exist: + from navi_shell import modify_navi_settings + modify_navi_settings("session", session_name) + case ['set-active', session_name]: + if does_session_exist: + navi_instance.set_active_session(session_name) + case x if x[0] in help_params: + print_params() + case _: + navi_instance.print_message("Invalid arguments. Use '-help' for more information.") \ No newline at end of file diff --git a/default_config b/default_config index f56f869..f2af3d9 100644 --- a/default_config +++ b/default_config @@ -2,4 +2,6 @@ username=default_user navi_name=Navi use_local_model=True dont_check_for_updates=False -update_branch=main \ No newline at end of file +update_branch=main +session=DEFAULT_SESSION +overwrite_session=True \ No newline at end of file diff --git a/navi_internal.py b/navi_internal.py index 53215c1..337fa68 100644 --- a/navi_internal.py +++ b/navi_internal.py @@ -26,6 +26,20 @@ class NaviApp: port: int = config.port local: str = config.local + memory_dir: str = "memories" + default_session: str = "default.json" + token_limit: int = 2048 + active_session: str = "DEFAULT_SESSION" + + llm_chat_prompt: str = ( + ( + "YOU ARE A CHATBOT. Communicate like a normal chatbot UNLESS the user has request that explicitly requires the terminal to perform, " + "in that case,provide the following 'TERMINAL OUTPUT {" + "terminal code to execute request (no not encapsulate command in quotes)}' and NOTHING " + "ELSE." + "normally.") + + f"The user's OS is {platform.system()}" + ". User message:") + is_local: bool = True script_dir = os.path.dirname(os.path.abspath(__file__)) @@ -104,19 +118,16 @@ def llm_chat(self, user_message: str, called_from_app: bool = False, call_remote # Define the API endpoint and payload message_amendment = user_message if not called_from_app: - message_amendment = ( - ("If the user message has a request that requires the terminal to execute, provide the following 'TERMINAL OUTPUT {" - "terminal code to execute request (no not encapsulate command in quotes)}' and NOTHING " - "ELSE. Otherwise continue to communicate" - "normally.") + - f"The user's OS is {platform.system()}" + ". User message:") + message_amendment = self.llm_chat_prompt message_amendment += user_message url = f"http://{self.local}:{self.port}/api/chat" if call_remote or not self.is_local: url = f"http://{self.server}:{self.port}/api/chat" + chat_history = self.load_session(self.active_session) + chat_history = self.trim_history_to_token_limit(chat_history, self.token_limit) payload = { "model": "navi-cli", - "messages": [{"role": "user", "content": message_amendment}] + "messages": chat_history + [{"role": "user", "content": message_amendment}] } headers = {'Content-Type': 'application/json'} @@ -141,6 +152,7 @@ def llm_chat(self, user_message: str, called_from_app: bool = False, call_remote # Concatenate the extracted messages full_response = "".join(extracted_responses) + self.save_chat_to_session(self.active_session, chat_history, {"role": "user", "content": user_message}, {"role": "assistant", "content": full_response}), return full_response, 200 else: return f"{response.url},{response.json()}", 400 @@ -174,6 +186,63 @@ def chat_with_navi(self) -> None: break self.process_message(user_message) + def setup_memory(self) -> None: + if not os.path.exists(self.memory_dir): + os.makedirs(self.memory_dir) + if not os.path.exists(self.get_session_path("DEFAULT_SESSION")): + self.create_new_session("DEFAULT_SESSION") + + def get_session_path(self, session_name): + return os.path.join(self.memory_dir, f"{session_name}.json") + + def trim_history_to_token_limit(self, chat_history, token_limit): + while chat_history and self.calculate_tokens(chat_history) > token_limit: + chat_history.pop(0) + + return chat_history + + def load_session(self, session_name): + path = self.get_session_path(session_name) + if os.path.exists(path): + with open(path, 'r') as f: + return json.load(f) + return [] + + def save_session(self,session_name, chat_history): + path = self.get_session_path(session_name) + with open(path, 'w') as f: + json.dump(chat_history, f, indent=4) + + def calculate_tokens(self, chat_history): + return sum(len(entry['content'].split()) for entry in chat_history) + + def create_new_session(self, session_name): + if not session_name.upper(): + print("Session name cannot be empty.") + return + if os.path.exists(self.get_session_path(session_name.upper())): + print("Session with this name already exists.") + return + self.save_session(session_name.upper(), []) + + def set_active_session(self, session_name): + if not os.path.exists(self.get_session_path(session_name)): + print(f"Session {session_name} does not exist.") + return None + self.active_session = session_name + + def save_chat_to_session(self, session_name, history, chat_user, chat_assistant): + chat_history = history + chat_history.append(chat_user) + chat_history.append(chat_assistant) + + # Handle token overflow + from navi_shell import get_navi_settings + if get_navi_settings()["overwrite_session"] and self.calculate_tokens(chat_history) > self.token_limit: + chat_history.pop(0) + + self.save_session(session_name, chat_history) + def setup_navi_vocab(self) -> None: # Register commands and aliases with the entity ruler for command, module in chips.modules.items(): diff --git a/navi_shell.py b/navi_shell.py index c0bc693..abf202b 100644 --- a/navi_shell.py +++ b/navi_shell.py @@ -152,6 +152,8 @@ def main() -> None: navi_instance.setup_navi_vocab() navi_instance.clear_terminal() navi_instance.setup_history() + navi_instance.setup_memory() + navi_instance.set_active_session(navi_settings["session"]) navi_instance.chat_with_navi() navi_instance.print_message(f"How can I help you, {user}") except KeyboardInterrupt: From 14965d136a6b10203e07d14bf62adaac97012497 Mon Sep 17 00:00:00 2001 From: VAliquo Date: Wed, 1 Jan 2025 13:44:58 -0500 Subject: [PATCH 32/40] [FIX] Accidental overwrite of history --- navi_internal.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/navi_internal.py b/navi_internal.py index 337fa68..5146717 100644 --- a/navi_internal.py +++ b/navi_internal.py @@ -124,10 +124,10 @@ def llm_chat(self, user_message: str, called_from_app: bool = False, call_remote if call_remote or not self.is_local: url = f"http://{self.server}:{self.port}/api/chat" chat_history = self.load_session(self.active_session) - chat_history = self.trim_history_to_token_limit(chat_history, self.token_limit) + chat_history_trim = self.trim_history_to_token_limit(chat_history, self.token_limit) payload = { "model": "navi-cli", - "messages": chat_history + [{"role": "user", "content": message_amendment}] + "messages": chat_history_trim + [{"role": "user", "content": message_amendment}] } headers = {'Content-Type': 'application/json'} From ba6b347534db60958e1ad2d953b435656a58144c Mon Sep 17 00:00:00 2001 From: VAliquo Date: Wed, 1 Jan 2025 15:04:02 -0500 Subject: [PATCH 33/40] [ADD] Remove memory --- chips/SSG/navi_memories.py | 16 ++++++++++------ navi_internal.py | 24 +++++++++++++++++++++--- 2 files changed, 31 insertions(+), 9 deletions(-) diff --git a/chips/SSG/navi_memories.py b/chips/SSG/navi_memories.py index 9b98b4c..f26311e 100644 --- a/chips/SSG/navi_memories.py +++ b/chips/SSG/navi_memories.py @@ -7,6 +7,7 @@ params: dict = { 'list': 'List all available sessions and indicate the active one', 'create ': 'Create a new session with the specified name', + 'remove ': 'Remove a session with the specified name', 'set-default ': 'Set the default session', 'set-active ': 'Set the active session', '-help': 'Display help information', @@ -28,13 +29,12 @@ def print_params() -> None: for param, description in params.items(): print(f"{param:<20} | {description}") -def list_sessions() -> None: +def list_sessions(active_session) -> None: sessions = os.listdir(memory_dir) print("Available Sessions:") for session in sessions: session_name = os.path.splitext(session)[0] - from navi_shell import get_navi_settings - if session_name == get_navi_settings()["session"]: + if session_name == active_session: print(f"* {session_name} (Active)") else: print(f" {session_name}") @@ -47,6 +47,7 @@ def does_session_exist(session_name) -> bool: def run(arguments=None) -> None: navi_instance = navi_internal.navi_instance + active_session = navi_instance.get_active_session() arg_array = arguments.text.split() arg_array.pop(0) # Remove the command itself @@ -57,16 +58,19 @@ def run(arguments=None) -> None: match arg_array: case ['list']: - list_sessions() + list_sessions(active_session) case ['create', session_name]: navi_instance.create_new_session(session_name) + case ['remove', session_name]: + if does_session_exist: + navi_instance.remove_session(session_name.upper()) case ['set-default', session_name]: if does_session_exist: from navi_shell import modify_navi_settings - modify_navi_settings("session", session_name) + modify_navi_settings("session", session_name.upper()) case ['set-active', session_name]: if does_session_exist: - navi_instance.set_active_session(session_name) + navi_instance.set_active_session(session_name.upper()) case x if x[0] in help_params: print_params() case _: diff --git a/navi_internal.py b/navi_internal.py index 5146717..f8e2be3 100644 --- a/navi_internal.py +++ b/navi_internal.py @@ -27,7 +27,7 @@ class NaviApp: local: str = config.local memory_dir: str = "memories" - default_session: str = "default.json" + default_session: str = "DEFAULT_SESSION" token_limit: int = 2048 active_session: str = "DEFAULT_SESSION" @@ -189,8 +189,8 @@ def chat_with_navi(self) -> None: def setup_memory(self) -> None: if not os.path.exists(self.memory_dir): os.makedirs(self.memory_dir) - if not os.path.exists(self.get_session_path("DEFAULT_SESSION")): - self.create_new_session("DEFAULT_SESSION") + if not os.path.exists(self.get_session_path(self.default_session)): + self.create_new_session(self.default_session) def get_session_path(self, session_name): return os.path.join(self.memory_dir, f"{session_name}.json") @@ -243,6 +243,24 @@ def save_chat_to_session(self, session_name, history, chat_user, chat_assistant) self.save_session(session_name, chat_history) + def get_active_session(self): + return self.active_session + + def remove_session(self, session_name): + if os.path.exists(self.get_session_path(session_name)): + if session_name == self.default_session: + # Clear the default session + self.save_session(self.default_session, []) + else: + # Set active session to the default session + self.set_active_session(self.default_session) + # Remove the session file + os.remove(self.get_session_path(session_name)) + # If the removed session was a config default, set it to the default session + from navi_shell import get_navi_settings, modify_navi_settings + if get_navi_settings()["session"] is session_name: + modify_navi_settings("session", self.default_session) + def setup_navi_vocab(self) -> None: # Register commands and aliases with the entity ruler for command, module in chips.modules.items(): From b84f618aeb359391d690873c91a270082d5cd9c0 Mon Sep 17 00:00:00 2001 From: VAliquo Date: Wed, 1 Jan 2025 15:06:15 -0500 Subject: [PATCH 34/40] Update chip-template.txt --- chips/SSG/chip-template.txt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/chips/SSG/chip-template.txt b/chips/SSG/chip-template.txt index 3042028..0c9cc27 100644 --- a/chips/SSG/chip-template.txt +++ b/chips/SSG/chip-template.txt @@ -1,9 +1,5 @@ import navi_internal -# To get a dictionary of current navi settings, and modify dictionary. Some modifications -# might require app restart. -#from navi_shell import get_navi_settings, modify_navi_settings - # Chip documentation: https://github.com/SaintsSec/Navi/wiki/4.-Developing-Chips-%E2%80%90-Indepth command: str = "{{CHIP_NAME}}" @@ -31,6 +27,10 @@ def run(arguments=None) -> None: # Get the instance of Navi. Required to access Navi-specific functions navi_instance = navi_internal.navi_instance navi_instance.print_message(f"How can I help you, {navi_instance.get_user()}?") + + # To get a dictionary of current navi settings, and modify dictionary. Some modifications + # might require app restart. To prevent circular imports, keep this in a function or class. + #from navi_shell import get_navi_settings, modify_navi_settings # Optional: Converts argument tokens into a list arg_array = arguments.text.split() From 72a1faac73afa951752a412abc5f0a48ad17ebda Mon Sep 17 00:00:00 2001 From: VAliquo Date: Wed, 1 Jan 2025 15:10:12 -0500 Subject: [PATCH 35/40] [UPDATE] Remove session error text --- navi_internal.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/navi_internal.py b/navi_internal.py index f8e2be3..cb85d98 100644 --- a/navi_internal.py +++ b/navi_internal.py @@ -260,6 +260,8 @@ def remove_session(self, session_name): from navi_shell import get_navi_settings, modify_navi_settings if get_navi_settings()["session"] is session_name: modify_navi_settings("session", self.default_session) + else: + print(f"{session_name} does not exist.") def setup_navi_vocab(self) -> None: # Register commands and aliases with the entity ruler From 52c2507787e4587cb0e0b0e216fbed3f93ba80ed Mon Sep 17 00:00:00 2001 From: VAliquo Date: Wed, 1 Jan 2025 15:18:01 -0500 Subject: [PATCH 36/40] [UPDATE] Reference --- navi_internal.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/navi_internal.py b/navi_internal.py index cb85d98..cd80913 100644 --- a/navi_internal.py +++ b/navi_internal.py @@ -29,7 +29,7 @@ class NaviApp: memory_dir: str = "memories" default_session: str = "DEFAULT_SESSION" token_limit: int = 2048 - active_session: str = "DEFAULT_SESSION" + active_session: str = default_session llm_chat_prompt: str = ( ( From 860f7ef67343f8a3687966f4af4da8c476182506 Mon Sep 17 00:00:00 2001 From: VAliquo Date: Wed, 1 Jan 2025 17:07:50 -0500 Subject: [PATCH 37/40] [ADD] RAG --- .gitignore | 1 + install/requirements.txt | 2 + navi_internal.py | 187 ++++++++++++++++++++++++++++++++++----- navi_shell.py | 2 + 4 files changed, 171 insertions(+), 21 deletions(-) diff --git a/.gitignore b/.gitignore index 4f7ccc7..9c867a4 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,4 @@ verification_test config qodana.yaml memories/ +data/ diff --git a/install/requirements.txt b/install/requirements.txt index 57b1d34..616623f 100644 --- a/install/requirements.txt +++ b/install/requirements.txt @@ -13,3 +13,5 @@ image mido psutil prompt_toolkit==3.0.48 +PyPDF2==3.0.1 +sentence-transformers==3.3.1 diff --git a/navi_internal.py b/navi_internal.py index cd80913..46a6f6d 100644 --- a/navi_internal.py +++ b/navi_internal.py @@ -9,6 +9,8 @@ import spacy from prompt_toolkit import PromptSession from prompt_toolkit.history import FileHistory +from PyPDF2 import PdfReader +from sentence_transformers import SentenceTransformer, util import chips import config @@ -28,17 +30,34 @@ class NaviApp: memory_dir: str = "memories" default_session: str = "DEFAULT_SESSION" - token_limit: int = 2048 + token_limit_max: int = 4096 + token_limit_rag: int = 2048 + token_limit_chat: int = 2048 active_session: str = default_session + knowledge_store_path: str = "data/knowledge_store.json" + input_directory: str = "data/input_files" + archive_directory: str = "data/archive" + # Initialize SentenceTransformer for RAG + retriever_model = SentenceTransformer('all-MiniLM-L6-v2') + llm_chat_prompt: str = ( - ( - "YOU ARE A CHATBOT. Communicate like a normal chatbot UNLESS the user has request that explicitly requires the terminal to perform, " - "in that case,provide the following 'TERMINAL OUTPUT {" - "terminal code to execute request (no not encapsulate command in quotes)}' and NOTHING " - "ELSE." - "normally.") + - f"The user's OS is {platform.system()}" + ". User message:") + "You are a highly intelligent chatbot. " + "You must answer user questions conversationally unless the user explicitly requests a terminal command. " + "Rules: " + "1. Respond conversationally for all general questions. Do not include TERMINAL OUTPUT for these responses. " + "2. Only respond with terminal commands if the user explicitly requests terminal execution (e.g., 'write to a file,' 'run a command'). " + "3. When responding with a terminal command, follow this exact format: " + " TERMINAL OUTPUT {terminal code to execute (do not use quotes, backticks, or markdown)}. " + "4. Do not include additional text, explanations, or formatting (e.g., markdown, backticks, or language tags like `bash`). " + "Examples: " + "- User: 'What job did Katie apply to?' " + "- Response: 'Katie applied to the position of Office Associate II at the Maine Department of Health.' " + "- User: 'Write her job to a file called job.txt.' " + "- Response: 'TERMINAL OUTPUT {echo Office Associate II at Maine Department of Health > job.txt}' " + "Never include TERMINAL OUTPUT unless explicitly requested. " + f"The user's operating system is {platform.system()}. User message:" + ) is_local: bool = True @@ -58,6 +77,83 @@ def __new__(cls, *args, **kwargs): cls._instance = super(NaviApp, cls).__new__(cls, *args, **kwargs) return cls._instance + def __init__(self): + self.knowledge_store = self.load_knowledge_store() + self.setup_knowledge_input_dir() + + def setup_knowledge_input_dir(self): + os.makedirs(self.input_directory, exist_ok=True) + os.makedirs(self.archive_directory, exist_ok=True) + + def load_knowledge_store(self): + if os.path.exists(self.knowledge_store_path): + with open(self.knowledge_store_path, "r") as f: + return json.load(f) + return [] + + def save_knowledge_store(self): + with open(self.knowledge_store_path, "w") as f: + json.dump(self.knowledge_store, f, indent=4) + + def extract_text_from_pdf(self, file_path): + try: + text = [] + reader = PdfReader(file_path) + for page in reader.pages: + text.append(page.extract_text()) + return "\n".join(text) + except Exception as e: + print(f"Error extracting text from {file_path}: {e}") + return "" + + def extract_text_from_txt(self, file_path): + try: + with open(file_path, "r", encoding="utf-8") as f: + return f.read() + except Exception as e: + print(f"Error reading text file {file_path}: {e}") + return "" + + def process_knowledge_files(self): + for file_name in os.listdir(self.input_directory): + file_path = os.path.join(self.input_directory, file_name) + if not os.path.isfile(file_path): + continue + + content = "" + if file_name.endswith(".pdf"): + content = self.extract_text_from_pdf(file_path) + elif file_name.endswith(".txt"): + content = self.extract_text_from_txt(file_path) + + if content: + self.knowledge_store.append({"content": content, "source": file_name}) + self.save_knowledge_store() + print(f"Added knowledge from {file_name}") + # Move processed files to the archive directory + import shutil + archive_path = os.path.join(self.archive_directory, file_name) + shutil.move(file_path, archive_path) + print(f"Processed {file_name}") + + def retrieve_context(self, query): + if not self.knowledge_store: + return "No relevant knowledge available." + + query_embedding = self.retriever_model.encode(query, convert_to_tensor=True) + knowledge_embeddings = self.retriever_model.encode( + [item["content"] for item in self.knowledge_store], convert_to_tensor=True + ) + + scores = util.pytorch_cos_sim(query_embedding, knowledge_embeddings)[0] + top_indices = scores.argsort(descending=True)[:3] # Retrieve top 3 matches + + retrieved_snippets = [ + f"{self.knowledge_store[i]['content']} (Source: {self.knowledge_store[i]['source']})" + for i in top_indices + ] + return "\n".join(retrieved_snippets) + def setup_history(self) -> None: self.session = PromptSession(history=FileHistory(self.hist_file)) @@ -120,23 +216,37 @@ def llm_chat(self, user_message: str, called_from_app: bool = False, call_remote if not called_from_app: message_amendment = self.llm_chat_prompt message_amendment += user_message - url = f"http://{self.local}:{self.port}/api/chat" - if call_remote or not self.is_local: - url = f"http://{self.server}:{self.port}/api/chat" + + # Check if RAG should be used + retrieved_context = "" + if self.is_local: + # Retrieve context and trim to token limit + retrieved_context = self.retrieve_context(user_message) + retrieved_context = self.trim_rag_to_token_limit(retrieved_context, self.token_limit_rag) + + # Load chat history and trim for token limit chat_history = self.load_session(self.active_session) - chat_history_trim = self.trim_history_to_token_limit(chat_history, self.token_limit) + chat_submission = self.trim_history_to_token_limit(chat_history, self.token_limit_chat) + + # Create combined input for API call + if retrieved_context: + combined_input = f"Retrieved Context:\n{retrieved_context}\n\nUser Query:\n{message_amendment}" + else: + combined_input = message_amendment payload = { "model": "navi-cli", - "messages": chat_history_trim + [{"role": "user", "content": message_amendment}] + "messages": chat_submission + [{"role": "user", "content": combined_input}] } headers = {'Content-Type': 'application/json'} + url = f"http://{self.local}:{self.port}/api/chat" + if call_remote or not self.is_local: + url = f"http://{self.server}:{self.port}/api/chat" response = requests.post(url, headers=headers, json=payload) - # Check if the response is valid + # Process the response if response.status_code == 200: response_text = response.text - # Split the response into lines and parse each line as JSON messages = [line for line in response_text.split('\n') if line] extracted_responses = [] @@ -150,16 +260,25 @@ def llm_chat(self, user_message: str, called_from_app: bool = False, call_remote except KeyboardInterrupt: self.print_message(f"Keyboard interrupt registered, talk soon {self.user}!") - # Concatenate the extracted messages + # Concatenate assistant responses full_response = "".join(extracted_responses) - self.save_chat_to_session(self.active_session, chat_history, {"role": "user", "content": user_message}, {"role": "assistant", "content": full_response}), + + # Save only the user message and assistant response to chat history + self.save_chat_to_session( + self.active_session, + chat_history, + {"role": "user", "content": user_message}, + {"role": "assistant", "content": full_response} + ) + return full_response, 200 else: - return f"{response.url},{response.json()}", 400 + return f"Error: {response.status_code}, {response.text}", 400 def process_message(self, user_message: str) -> None: processed_message = self.nlp(user_message.strip()) navi_commands = [ent for ent in processed_message.ents if ent.label_ == "NAVI_COMMAND"] + # Check if the message is a question question_keywords = {"is", "does", "do", "what", "when", "where", "who", "why", "what", "how"} is_question = any(token.text.lower() in question_keywords for token in processed_message if token.i == 0) @@ -171,8 +290,27 @@ def process_message(self, user_message: str) -> None: chips.modules[main_command].run(processed_message) else: response_message, http_status = self.llm_chat(user_message) - if response_message.startswith("TERMINAL OUTPUT"): - chips.modules["navi_sys"].run(response_message) + + # Normalize TERMINAL OUTPUT and process terminal-related responses + if "TERMINAL OUTPUT" in response_message.upper(): # Case-insensitive check + # Normalize TERMINAL OUTPUT + response_message = response_message.replace("Terminal Output", "TERMINAL OUTPUT").replace( + "terminal output", "TERMINAL OUTPUT") + + # Remove unwanted formatting + clean_response = ( + response_message.replace("```", "") + .replace("bash", "") + .replace("TERMINAL OUTPUT", "") + .strip() + ) + if clean_response.startswith("{") and clean_response.endswith("}"): + clean_response = clean_response[1:-1].strip() # Remove surrounding braces + + if clean_response: # Ensure the command isn't empty + chips.modules["navi_sys"].run(clean_response) + else: + self.print_message("Invalid terminal command received.") else: self.print_message(f"{response_message if http_status == 200 else 'Issue with server'}") @@ -195,6 +333,13 @@ def setup_memory(self) -> None: def get_session_path(self, session_name): return os.path.join(self.memory_dir, f"{session_name}.json") + def trim_rag_to_token_limit(self, text, token_limit): + words = text.split() + if len(words) > token_limit: + trimmed_text = " ".join(words[:token_limit]) + return trimmed_text + "..." + return text + def trim_history_to_token_limit(self, chat_history, token_limit): while chat_history and self.calculate_tokens(chat_history) > token_limit: chat_history.pop(0) @@ -238,7 +383,7 @@ def save_chat_to_session(self, session_name, history, chat_user, chat_assistant) # Handle token overflow from navi_shell import get_navi_settings - if get_navi_settings()["overwrite_session"] and self.calculate_tokens(chat_history) > self.token_limit: + if get_navi_settings()["overwrite_session"] and self.calculate_tokens(chat_history) > self.token_limit_chat: chat_history.pop(0) self.save_session(session_name, chat_history) diff --git a/navi_shell.py b/navi_shell.py index abf202b..45a5f5c 100644 --- a/navi_shell.py +++ b/navi_shell.py @@ -149,6 +149,8 @@ def main() -> None: navi_instance.set_local(False) if not args.remote and navi_settings["use_local_model"]: local_model_check() + # Only process files if local model is used + navi_instance.process_knowledge_files() navi_instance.setup_navi_vocab() navi_instance.clear_terminal() navi_instance.setup_history() From 084501f92e9997f304117081e4dca7adccef420a Mon Sep 17 00:00:00 2001 From: VAliquo Date: Thu, 2 Jan 2025 09:50:34 -0500 Subject: [PATCH 38/40] [UPDATE] Code cleanup --- chips/SSG/navi_chip_creator.py | 6 +- chips/SSG/navi_chip_installer.py | 15 +- chips/SSG/navi_memories.py | 9 +- chips/SSG/navi_settings.py | 2 +- chips/SSG/navi_spec.py | 9 +- chips/SSG/navi_system.py | 6 +- chips/__init__.py | 7 +- install/local_model.py | 15 +- navi.py | 2 +- navi_banner.py | 1 - navi_internal.py | 226 +++++++++++++++++-------------- navi_updater.py | 12 +- 12 files changed, 167 insertions(+), 143 deletions(-) diff --git a/chips/SSG/navi_chip_creator.py b/chips/SSG/navi_chip_creator.py index 1cced16..426c72f 100644 --- a/chips/SSG/navi_chip_creator.py +++ b/chips/SSG/navi_chip_creator.py @@ -1,10 +1,12 @@ -import navi_internal -import sys import os import re import subprocess # nosec +import sys import webbrowser + from colorama import Fore + +import navi_internal from navi_shell import restart_navi command: str = "chip-create" diff --git a/chips/SSG/navi_chip_installer.py b/chips/SSG/navi_chip_installer.py index 7bf0748..49d7956 100644 --- a/chips/SSG/navi_chip_installer.py +++ b/chips/SSG/navi_chip_installer.py @@ -1,13 +1,14 @@ import os -import sys -import requests -import zipfile import shutil import subprocess # nosec +import sys import uuid -import navi_internal +import zipfile +import requests from colorama import Fore + +import navi_internal from navi import get_parameters from navi_shell import restart_navi @@ -328,9 +329,9 @@ def update_chip(chip_name: str) -> None: def help_text() -> None: navi.print_message("Chip Manager\n" - "chips [install | uninstall | search | update] [app/query]\n\n" - "List currently installed chips\n" - "chips list") + "chips [install | uninstall | search | update] [app/query]\n\n" + "List currently installed chips\n" + "chips list") def run(arguments=None) -> None: diff --git a/chips/SSG/navi_memories.py b/chips/SSG/navi_memories.py index f26311e..b352dd8 100644 --- a/chips/SSG/navi_memories.py +++ b/chips/SSG/navi_memories.py @@ -1,6 +1,7 @@ -import navi_internal import os +import navi_internal + command: str = "memory" use: str = "Manage chat memory sessions." aliases: list = ['session', 'memory'] @@ -22,6 +23,7 @@ help_params: tuple = ('-help', '-h') + def print_params() -> None: """Print available parameters and descriptions.""" print(f"{'Parameter':<20} | {'Description'}") @@ -29,6 +31,7 @@ def print_params() -> None: for param, description in params.items(): print(f"{param:<20} | {description}") + def list_sessions(active_session) -> None: sessions = os.listdir(memory_dir) print("Available Sessions:") @@ -39,12 +42,14 @@ def list_sessions(active_session) -> None: else: print(f" {session_name}") + def does_session_exist(session_name) -> bool: if not os.path.exists(os.path.join(memory_dir, f"{session_name}.json")): print(f"Session '{session_name}' does not exist.") return False return True + def run(arguments=None) -> None: navi_instance = navi_internal.navi_instance active_session = navi_instance.get_active_session() @@ -74,4 +79,4 @@ def run(arguments=None) -> None: case x if x[0] in help_params: print_params() case _: - navi_instance.print_message("Invalid arguments. Use '-help' for more information.") \ No newline at end of file + navi_instance.print_message("Invalid arguments. Use '-help' for more information.") diff --git a/chips/SSG/navi_settings.py b/chips/SSG/navi_settings.py index ec6a162..9701e8a 100644 --- a/chips/SSG/navi_settings.py +++ b/chips/SSG/navi_settings.py @@ -34,7 +34,7 @@ def modify_config(key, value): file.write(line) continue - current_key, current_value = line.strip().split("=", 1) + current_key, _ = line.strip().split("=", 1) if current_key == key: file.write(f"{key}={value}\n") modified = True diff --git a/chips/SSG/navi_spec.py b/chips/SSG/navi_spec.py index 9bd8482..4fc851b 100755 --- a/chips/SSG/navi_spec.py +++ b/chips/SSG/navi_spec.py @@ -1,10 +1,12 @@ #!/bin/python3 import platform +import socket +from datetime import datetime + import psutil import requests -import socket + import navi_internal -from datetime import datetime # Navi Command System Variables command = "navi_specs" @@ -78,7 +80,8 @@ def run(arguments=None): # Main functions response_message = navi_instance.llm_chat( - "Give me a really simple quip about getting my systems specs. Dont include commands or references to operating systems.", True) + "Give me a really simple quip about getting my systems specs. Dont include commands or references to operating systems.", + True) clean_text = str(response_message).replace("(", "").replace(")", "").replace(", 200", "").replace("\"", "").replace( "\\n", "") output = clean_text + "\n" diff --git a/chips/SSG/navi_system.py b/chips/SSG/navi_system.py index 57f1662..e16ea84 100644 --- a/chips/SSG/navi_system.py +++ b/chips/SSG/navi_system.py @@ -1,7 +1,8 @@ #!/bin/python3 +import shlex import subprocess # nosec + import navi_internal -import shlex from navi import get_command_path command = "navi_sys" @@ -13,7 +14,8 @@ def run(arguments=None): navi_command = str(arguments).replace("TERMINAL OUTPUT", "", 1).strip() base_command = navi_command.split()[0] if get_command_path(base_command) is not None: - navi_instance.print_message(f"\nDo I have your permission to use your **shell** to execute the following: \n\n{navi_command}\n") + navi_instance.print_message( + f"\nDo I have your permission to use your **shell** to execute the following: \n\n{navi_command}\n") user_input = input("Do you want me to continue (y/n): ").strip().lower() if user_input == 'y': result = subprocess.run( diff --git a/chips/__init__.py b/chips/__init__.py index c427af4..98d93e7 100644 --- a/chips/__init__.py +++ b/chips/__init__.py @@ -1,10 +1,9 @@ """Init commands.""" -from os.path import dirname, basename, isfile, join, exists -import os -import sys -import glob import importlib import logging +import os +import sys +from os.path import dirname, join, exists from pathlib import Path package_dir = Path(__file__).parent diff --git a/install/local_model.py b/install/local_model.py index afe561e..a92dec6 100644 --- a/install/local_model.py +++ b/install/local_model.py @@ -1,15 +1,12 @@ import os +import platform import shutil -import subprocess +import subprocess # nosec import tempfile from getpass import getpass from typing import Tuple -import subprocess -import platform -import os - def install_ollama() -> bool: try: if platform.system() in ["Linux"]: @@ -46,8 +43,8 @@ def install_ollama() -> bool: # Download the installer subprocess.run( ["powershell", "-Command", - "Invoke-WebRequest -Uri https://ollama.com/download/OllamaSetup.exe -OutFile ollama_installer.exe"], - shell=True, + "Invoke-WebRequest", "-Uri", "https://ollama.com/download/OllamaSetup.exe", + "-OutFile", "ollama_installer.exe"], check=True, capture_output=True, text=True, @@ -56,8 +53,7 @@ def install_ollama() -> bool: # Run the installer subprocess.run( ["powershell", "-Command", - "Start-Process -FilePath ./ollama_installer.exe"], - shell=True, + "Start-Process", "-FilePath", "./ollama_installer.exe", "-Wait"], check=True, capture_output=True, text=True, @@ -89,7 +85,6 @@ def install_ollama() -> bool: print(f"Failed to remove installer: {e}") - def ollama_installed() -> bool: try: subprocess.run( diff --git a/navi.py b/navi.py index 1fffe03..124c3ec 100644 --- a/navi.py +++ b/navi.py @@ -1,5 +1,5 @@ -import re import platform +import re import subprocess # nosec diff --git a/navi_banner.py b/navi_banner.py index 8eea2b3..95d0cac 100644 --- a/navi_banner.py +++ b/navi_banner.py @@ -4,7 +4,6 @@ versionNum = get_navi_version() - # The cover art: art = rf"""{breakline} | _ __ _ | diff --git a/navi_internal.py b/navi_internal.py index 46a6f6d..161a117 100644 --- a/navi_internal.py +++ b/navi_internal.py @@ -7,9 +7,9 @@ import requests import spacy +from PyPDF2 import PdfReader from prompt_toolkit import PromptSession from prompt_toolkit.history import FileHistory -from PyPDF2 import PdfReader from sentence_transformers import SentenceTransformer, util import chips @@ -81,6 +81,12 @@ def __init__(self): self.knowledge_store = self.load_knowledge_store() self.setup_knowledge_input_dir() + ''' + --------------------------------------------------------------------------- + RAG MANAGEMENT + --------------------------------------------------------------------------- + ''' + def setup_knowledge_input_dir(self): os.makedirs(self.input_directory, exist_ok=True) os.makedirs(self.archive_directory, exist_ok=True) @@ -95,25 +101,6 @@ def save_knowledge_store(self): with open(self.knowledge_store_path, "w") as f: json.dump(self.knowledge_store, f, indent=4) - def extract_text_from_pdf(self, file_path): - try: - text = [] - reader = PdfReader(file_path) - for page in reader.pages: - text.append(page.extract_text()) - return "\n".join(text) - except Exception as e: - print(f"Error extracting text from {file_path}: {e}") - return "" - - def extract_text_from_txt(self, file_path): - try: - with open(file_path, "r", encoding="utf-8") as f: - return f.read() - except Exception as e: - print(f"Error reading text file {file_path}: {e}") - return "" - def process_knowledge_files(self): for file_name in os.listdir(self.input_directory): file_path = os.path.join(self.input_directory, file_name) @@ -154,6 +141,121 @@ def retrieve_context(self, query): ] return "\n".join(retrieved_snippets) + def extract_text_from_pdf(self, file_path): + try: + text = [] + reader = PdfReader(file_path) + for page in reader.pages: + text.append(page.extract_text()) + return "\n".join(text) + except Exception as e: + print(f"Error extracting text from {file_path}: {e}") + return "" + + def extract_text_from_txt(self, file_path): + try: + with open(file_path, "r", encoding="utf-8") as f: + return f.read() + except Exception as e: + print(f"Error reading text file {file_path}: {e}") + return "" + + def trim_rag_to_token_limit(self, text, token_limit): + words = text.split() + if len(words) > token_limit: + trimmed_text = " ".join(words[:token_limit]) + return trimmed_text + "..." + return text + + ''' + --------------------------------------------------------------------------- + MEMORY MANAGEMENT + --------------------------------------------------------------------------- + ''' + + def setup_memory(self) -> None: + if not os.path.exists(self.memory_dir): + os.makedirs(self.memory_dir) + if not os.path.exists(self.get_session_path(self.default_session)): + self.create_new_session(self.default_session) + + def get_session_path(self, session_name): + return os.path.join(self.memory_dir, f"{session_name}.json") + + def trim_history_to_token_limit(self, chat_history, token_limit): + while chat_history and self.calculate_tokens(chat_history) > token_limit: + chat_history.pop(0) + + return chat_history + + def load_session(self, session_name): + path = self.get_session_path(session_name) + if os.path.exists(path): + with open(path, 'r') as f: + return json.load(f) + return [] + + def save_session(self, session_name, chat_history): + path = self.get_session_path(session_name) + with open(path, 'w') as f: + json.dump(chat_history, f, indent=4) + + def calculate_tokens(self, chat_history): + return sum(len(entry['content'].split()) for entry in chat_history) + + def create_new_session(self, session_name): + if not session_name.upper(): + print("Session name cannot be empty.") + return + if os.path.exists(self.get_session_path(session_name.upper())): + print("Session with this name already exists.") + return + self.save_session(session_name.upper(), []) + + def set_active_session(self, session_name): + if not os.path.exists(self.get_session_path(session_name)): + print(f"Session {session_name} does not exist.") + return None + self.active_session = session_name + + def save_chat_to_session(self, session_name, history, chat_user, chat_assistant): + chat_history = history + chat_history.append(chat_user) + chat_history.append(chat_assistant) + + # Handle token overflow + from navi_shell import get_navi_settings + if get_navi_settings()["overwrite_session"] and self.calculate_tokens(chat_history) > self.token_limit_chat: + chat_history.pop(0) + + self.save_session(session_name, chat_history) + + def get_active_session(self): + return self.active_session + + def remove_session(self, session_name): + if os.path.exists(self.get_session_path(session_name)): + if session_name == self.default_session: + # Clear the default session + self.save_session(self.default_session, []) + else: + # Set active session to the default session + self.set_active_session(self.default_session) + # Remove the session file + os.remove(self.get_session_path(session_name)) + # If the removed session was a config default, set it to the default session + from navi_shell import get_navi_settings, modify_navi_settings + if get_navi_settings()["session"] is session_name: + modify_navi_settings("session", self.default_session) + else: + print(f"{session_name} does not exist.") + + ''' + --------------------------------------------------------------------------- + CORE NAVI FUNCTIONS + --------------------------------------------------------------------------- + ''' + def setup_history(self) -> None: self.session = PromptSession(history=FileHistory(self.hist_file)) @@ -324,90 +426,6 @@ def chat_with_navi(self) -> None: break self.process_message(user_message) - def setup_memory(self) -> None: - if not os.path.exists(self.memory_dir): - os.makedirs(self.memory_dir) - if not os.path.exists(self.get_session_path(self.default_session)): - self.create_new_session(self.default_session) - - def get_session_path(self, session_name): - return os.path.join(self.memory_dir, f"{session_name}.json") - - def trim_rag_to_token_limit(self, text, token_limit): - words = text.split() - if len(words) > token_limit: - trimmed_text = " ".join(words[:token_limit]) - return trimmed_text + "..." - return text - - def trim_history_to_token_limit(self, chat_history, token_limit): - while chat_history and self.calculate_tokens(chat_history) > token_limit: - chat_history.pop(0) - - return chat_history - - def load_session(self, session_name): - path = self.get_session_path(session_name) - if os.path.exists(path): - with open(path, 'r') as f: - return json.load(f) - return [] - - def save_session(self,session_name, chat_history): - path = self.get_session_path(session_name) - with open(path, 'w') as f: - json.dump(chat_history, f, indent=4) - - def calculate_tokens(self, chat_history): - return sum(len(entry['content'].split()) for entry in chat_history) - - def create_new_session(self, session_name): - if not session_name.upper(): - print("Session name cannot be empty.") - return - if os.path.exists(self.get_session_path(session_name.upper())): - print("Session with this name already exists.") - return - self.save_session(session_name.upper(), []) - - def set_active_session(self, session_name): - if not os.path.exists(self.get_session_path(session_name)): - print(f"Session {session_name} does not exist.") - return None - self.active_session = session_name - - def save_chat_to_session(self, session_name, history, chat_user, chat_assistant): - chat_history = history - chat_history.append(chat_user) - chat_history.append(chat_assistant) - - # Handle token overflow - from navi_shell import get_navi_settings - if get_navi_settings()["overwrite_session"] and self.calculate_tokens(chat_history) > self.token_limit_chat: - chat_history.pop(0) - - self.save_session(session_name, chat_history) - - def get_active_session(self): - return self.active_session - - def remove_session(self, session_name): - if os.path.exists(self.get_session_path(session_name)): - if session_name == self.default_session: - # Clear the default session - self.save_session(self.default_session, []) - else: - # Set active session to the default session - self.set_active_session(self.default_session) - # Remove the session file - os.remove(self.get_session_path(session_name)) - # If the removed session was a config default, set it to the default session - from navi_shell import get_navi_settings, modify_navi_settings - if get_navi_settings()["session"] is session_name: - modify_navi_settings("session", self.default_session) - else: - print(f"{session_name} does not exist.") - def setup_navi_vocab(self) -> None: # Register commands and aliases with the entity ruler for command, module in chips.modules.items(): diff --git a/navi_updater.py b/navi_updater.py index 330aa7c..7d2b097 100644 --- a/navi_updater.py +++ b/navi_updater.py @@ -1,12 +1,11 @@ -from typing import Any - -import requests import os -import sys +import shutil import subprocess # nosec +import sys import zipfile -import shutil +from typing import Any +import requests global prime_navi_version navi_version_path = ".navi_version" @@ -67,7 +66,8 @@ def update_version_number(version: str) -> None: version_file.write(version) -def check_for_new_release(current_version: str, repo_owner: str, repo_name: str, edge: bool = False) -> tuple[str, str | None]: +def check_for_new_release(current_version: str, repo_owner: str, repo_name: str, edge: bool = False) -> tuple[ + str, str | None]: latest_release = get_latest_release(repo_owner, repo_name, edge) if current_version == "Unknown" or (latest_release and is_new_release(current_version, latest_release['tag_name'])): global prime_navi_version From 738055f759da74db5785a13d446e202b2be2dacd Mon Sep 17 00:00:00 2001 From: VAliquo Date: Thu, 2 Jan 2025 09:56:48 -0500 Subject: [PATCH 39/40] [FIX] Comment --- navi_internal.py | 18 +++--------------- 1 file changed, 3 insertions(+), 15 deletions(-) diff --git a/navi_internal.py b/navi_internal.py index 161a117..71cbc69 100644 --- a/navi_internal.py +++ b/navi_internal.py @@ -81,11 +81,7 @@ def __init__(self): self.knowledge_store = self.load_knowledge_store() self.setup_knowledge_input_dir() - ''' - --------------------------------------------------------------------------- - RAG MANAGEMENT - --------------------------------------------------------------------------- - ''' + # ------------------------------ RAG MANAGEMENT ------------------------------ def setup_knowledge_input_dir(self): os.makedirs(self.input_directory, exist_ok=True) @@ -167,11 +163,7 @@ def trim_rag_to_token_limit(self, text, token_limit): return trimmed_text + "..." return text - ''' - --------------------------------------------------------------------------- - MEMORY MANAGEMENT - --------------------------------------------------------------------------- - ''' + # ------------------------------ MEMORY MANAGEMENT --------------------------- def setup_memory(self) -> None: if not os.path.exists(self.memory_dir): @@ -250,11 +242,7 @@ def remove_session(self, session_name): else: print(f"{session_name} does not exist.") - ''' - --------------------------------------------------------------------------- - CORE NAVI FUNCTIONS - --------------------------------------------------------------------------- - ''' + # ------------------------------ CORE NAVI FUNCTIONS -------------------------- def setup_history(self) -> None: self.session = PromptSession(history=FileHistory(self.hist_file)) From 68e70f8dd573bc889a29e78709405985b3911f81 Mon Sep 17 00:00:00 2001 From: VAliquo Date: Thu, 2 Jan 2025 11:11:33 -0500 Subject: [PATCH 40/40] [UPDATE] Improved token management --- chips/SSG/navi_settings.py | 22 ++++-- chips/SSG/navi_token_management.py | 112 +++++++++++++++++++++++++++++ default_config | 4 +- navi_internal.py | 42 +++++++++-- 4 files changed, 168 insertions(+), 12 deletions(-) create mode 100644 chips/SSG/navi_token_management.py diff --git a/chips/SSG/navi_settings.py b/chips/SSG/navi_settings.py index 9701e8a..bbd39bc 100644 --- a/chips/SSG/navi_settings.py +++ b/chips/SSG/navi_settings.py @@ -69,14 +69,28 @@ def read_config(path_to_config): def settings_init(): + import getpass + + default_config = read_config(default_config_path) + + # Check if the config file exists if os.path.exists(config_path): - return read_config(config_path) + # Read the existing config + user_config = read_config(config_path) + # Add any missing keys from the default config + for key, default_value in default_config.items(): + if key not in user_config: + print(f"Adding missing key: {key} with default value: {default_value}") + user_config[key] = default_value + modify_config(key, default_value) + return user_config else: - import getpass - default_config = read_config(default_config_path) + # If the config file doesn't exist, create it using defaults + print("Config file not found. Creating a new one with default settings.") create_config(default_config) modify_config("username", getpass.getuser()) - return read_config(config_path) + + return read_config(config_path) def run(arguments=None): diff --git a/chips/SSG/navi_token_management.py b/chips/SSG/navi_token_management.py new file mode 100644 index 0000000..cd225ca --- /dev/null +++ b/chips/SSG/navi_token_management.py @@ -0,0 +1,112 @@ +import navi_internal + +token_limit_max: int = 4096 +navi_settings: dict = {} + +command: str = "token-config" +use: str = "Adjust token limits for RAG and chat memory." +aliases: list = ['set_tokens', 'token_limits'] +params: dict = { + '-rag': 'Set the token limit for RAG (e.g., -rag 1024)', + '-chat': 'Set the token limit for chat memory (e.g., -chat 3072)', + '-show': 'Display the current token limit partition.', + '-help': 'Display help information.', +} + +help_params: tuple = ('-help', '-h') + + +def print_params() -> None: + print(f"{'Parameter':<10} | {'Description'}") + print("-" * 40) + + for param, description in params.items(): + print(f"{param:<10} | {description}") + + +def set_token_limit(rag: int = None, chat: int = None) -> str: + global navi_settings + token_limit_rag = int(navi_settings["token_limit_rag"]) + token_limit_chat = int(navi_settings["token_limit_chat"]) + + # Calculate new limits if provided + new_rag = token_limit_rag if rag is None else rag + new_chat = token_limit_chat if chat is None else chat + + # Validate the total allocation + if new_rag + new_chat > token_limit_max: + return f"Error: Total allocation exceeds the maximum token limit of {token_limit_max}." + + from navi_shell import modify_navi_settings, get_navi_settings + modify_navi_settings("token_limit_rag", new_rag) + modify_navi_settings("token_limit_chat", new_chat) + + # Refresh the navi_settings dictionary + navi_settings = get_navi_settings() + + return f"Token limits updated. RAG: {new_rag}, Chat: {new_chat}" + + +def show_token_limits() -> str: + rag = int(navi_settings["token_limit_rag"]) + chat = int(navi_settings["token_limit_chat"]) + return f"Current Token Limits:\n- RAG: {rag}\n- Chat: {chat}\n- Total: {rag + chat} (Max: {token_limit_max})" + + +def run(arguments=None) -> None: + navi_instance = navi_internal.navi_instance + global token_limit_max + token_limit_max = navi_instance.get_max_token_limit() + arg_array = arguments.text.split() + + arg_array.pop(0) + + from navi_shell import get_navi_settings + global navi_settings + navi_settings = get_navi_settings() + + # Parse parameters + if arg_array: + rag_limit = None + chat_limit = None + show_only = False + + for i in range(len(arg_array)): + arg = arg_array[i] + + match arg: + case '-rag': + try: + # Fetch the next value and skip it in the loop + rag_limit = int(arg_array[i + 1]) + arg_array[i + 1] = None # Mark as processed + except (IndexError, ValueError): + navi_instance.print_message("Error: Invalid value for -rag.") + return + case '-chat': + try: + # Fetch the next value and skip it in the loop + chat_limit = int(arg_array[i + 1]) + arg_array[i + 1] = None # Mark as processed + except (IndexError, ValueError): + navi_instance.print_message("Error: Invalid value for -chat.") + return + case '-show': + show_only = True + case x if x in help_params: + print_params() + return + case _: + # Skip any previously processed value + if arg is not None: + navi_instance.print_message(f"Invalid parameter: {arg}") + return + + if show_only: + navi_instance.print_message(show_token_limits()) + return + + result = set_token_limit(rag=rag_limit, chat=chat_limit) + navi_instance.print_message(result) + else: + print_params() diff --git a/default_config b/default_config index f2af3d9..e9c7de2 100644 --- a/default_config +++ b/default_config @@ -4,4 +4,6 @@ use_local_model=True dont_check_for_updates=False update_branch=main session=DEFAULT_SESSION -overwrite_session=True \ No newline at end of file +overwrite_session=True +token_limit_rag=2048 +token_limit_chat=2048 \ No newline at end of file diff --git a/navi_internal.py b/navi_internal.py index 71cbc69..8e2f742 100644 --- a/navi_internal.py +++ b/navi_internal.py @@ -31,8 +31,6 @@ class NaviApp: memory_dir: str = "memories" default_session: str = "DEFAULT_SESSION" token_limit_max: int = 4096 - token_limit_rag: int = 2048 - token_limit_chat: int = 2048 active_session: str = default_session knowledge_store_path: str = "data/knowledge_store.json" @@ -210,14 +208,14 @@ def set_active_session(self, session_name): return None self.active_session = session_name - def save_chat_to_session(self, session_name, history, chat_user, chat_assistant): + def save_chat_to_session(self, session_name, history, chat_user, chat_assistant, token_limit): chat_history = history chat_history.append(chat_user) chat_history.append(chat_assistant) # Handle token overflow from navi_shell import get_navi_settings - if get_navi_settings()["overwrite_session"] and self.calculate_tokens(chat_history) > self.token_limit_chat: + if get_navi_settings()["overwrite_session"] and self.calculate_tokens(chat_history) > token_limit: chat_history.pop(0) self.save_session(session_name, chat_history) @@ -300,6 +298,33 @@ def clear_terminal(self) -> None: else: print(self.art) + def fetch_token_limits(self): + from navi_shell import get_navi_settings + try: + navi_settings = get_navi_settings() + + token_limit_rag = int(navi_settings["token_limit_rag"]) + token_limit_chat = int(navi_settings["token_limit_chat"]) + + # Check if the combined total exceeds the maximum allowed + return_default = False + if token_limit_rag < 0 or token_limit_chat < 0: + print("Warning: Negative token values are invalid. Using default values") + return_default = True + if token_limit_rag + token_limit_chat > self.token_limit_max: + print("Warning: Combined token limits exceed the maximum allowed. Using default values") + return_default = True + if return_default: + return 2048, 2048 + else: + return token_limit_rag, token_limit_chat + except (ValueError, TypeError, KeyError) as e: + print(f"Warning: Issue fetching token limits: {e}. Using default values.") + return 2048, 2048 + + def get_max_token_limit(self): + return self.token_limit_max + def llm_chat(self, user_message: str, called_from_app: bool = False, call_remote: bool = False) -> tuple[str, int]: # Define the API endpoint and payload message_amendment = user_message @@ -307,16 +332,18 @@ def llm_chat(self, user_message: str, called_from_app: bool = False, call_remote message_amendment = self.llm_chat_prompt message_amendment += user_message + token_limit_rag, token_limit_chat = self.fetch_token_limits() + # Check if RAG should be used retrieved_context = "" if self.is_local: # Retrieve context and trim to token limit retrieved_context = self.retrieve_context(user_message) - retrieved_context = self.trim_rag_to_token_limit(retrieved_context, self.token_limit_rag) + retrieved_context = self.trim_rag_to_token_limit(retrieved_context, token_limit_rag) # Load chat history and trim for token limit chat_history = self.load_session(self.active_session) - chat_submission = self.trim_history_to_token_limit(chat_history, self.token_limit_chat) + chat_submission = self.trim_history_to_token_limit(chat_history, token_limit_chat) # Create combined input for API call if retrieved_context: @@ -358,7 +385,8 @@ def llm_chat(self, user_message: str, called_from_app: bool = False, call_remote self.active_session, chat_history, {"role": "user", "content": user_message}, - {"role": "assistant", "content": full_response} + {"role": "assistant", "content": full_response}, + token_limit_chat ) return full_response, 200