Skip to content

Commit

Permalink
Merge pull request #152 from esbmc/addon-support
Browse files Browse the repository at this point in the history
Addon Support for ChatCommand
  • Loading branch information
Yiannis128 authored Nov 6, 2024
2 parents dfb7e57 + 7572b2c commit 2ecab63
Show file tree
Hide file tree
Showing 15 changed files with 236 additions and 102 deletions.
6 changes: 3 additions & 3 deletions .github/workflows/workflow.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ jobs:
- name: Setup Python
uses: actions/setup-python@v5.3.0
with:
python-version: "3.11"
python-version: "3.12.0"

- name: Check out repository code
uses: actions/checkout@v4.2.2
Expand Down Expand Up @@ -43,7 +43,7 @@ jobs:
- name: Setup Python
uses: actions/setup-python@v5.3.0
with:
python-version: "3.11"
python-version: "3.12.0"

- name: Install pipenv
run: |
Expand Down Expand Up @@ -71,7 +71,7 @@ jobs:
- name: Setup Python
uses: actions/setup-python@v5.3.0
with:
python-version: "3.11"
python-version: "3.12.0"

- name: Download Requirements
uses: actions/download-artifact@v4.1.8
Expand Down
3 changes: 2 additions & 1 deletion .pylintrc
Original file line number Diff line number Diff line change
Expand Up @@ -434,7 +434,8 @@ disable=raw-checker-failed,
unspecified-encoding,
too-many-arguments,
too-many-positional-arguments,
too-many-instance-attributes
too-many-instance-attributes,
too-few-public-methods

# Enable the message, report, category or checker with the given id(s). You can
# either give multiple identifier separated by comma (,) or put this option
Expand Down
72 changes: 29 additions & 43 deletions esbmc_ai/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
# Author: Yiannis Charalambous 2023

from pathlib import Path
import re
import sys

# Enables arrow key functionality for input(). Do not remove import.
Expand All @@ -13,6 +12,10 @@

from langchain_core.language_models import BaseChatModel

from esbmc_ai.command_runner import CommandRunner
from esbmc_ai.commands.fix_code_command import FixCodeCommandResult


import argparse

from esbmc_ai import Config
Expand All @@ -34,12 +37,16 @@
from esbmc_ai.chat_response import FinishReason, ChatResponse
from esbmc_ai.ai_models import _ai_model_names


commands: list[ChatCommand] = []
command_names: list[str]
help_command: HelpCommand = HelpCommand()
fix_code_command: FixCodeCommand = FixCodeCommand()
exit_command: ExitCommand = ExitCommand()
command_runner: CommandRunner = CommandRunner(
[
help_command,
exit_command,
fix_code_command,
]
)

chat: UserChat

Expand Down Expand Up @@ -105,21 +112,11 @@ def print_assistant_response(
)


def init_commands_list() -> None:
"""Setup Help command and commands list."""
# Built in commands
global help_command
commands.extend(
[
help_command,
exit_command,
fix_code_command,
]
)
help_command.set_commands(commands)

global command_names
command_names = [command.command_name for command in commands]
def init_addons() -> None:
command_runner.addon_commands.clear()
command_runner.addon_commands.extend(Config.get_value("addon_modules"))
if len(command_runner.addon_commands):
printv("Addons:\n\t* " + "\t * ".join(command_runner.addon_commands_names))


def update_solution(source_code: str) -> None:
Expand Down Expand Up @@ -215,23 +212,7 @@ def _run_command_mode(command: ChatCommand, args: argparse.Namespace) -> None:
sys.exit(0)


def parse_command(user_prompt_string: str) -> tuple[str, list[str]]:
"""Parses a command and returns it based on the command rules outlined in
the wiki: https://github.com/Yiannis128/esbmc-ai/wiki/User-Chat-Mode"""
regex_pattern: str = (
r'\s+(?=(?:[^\\"]*(?:\\.[^\\"]*)*)$)|(?:(?<!\\)".*?(?<!\\)")|(?:\\.)+|\S+'
)
segments: list[str] = re.findall(regex_pattern, user_prompt_string)
parsed_array: list[str] = [segment for segment in segments if segment != " "]
# Remove all empty spaces.
command: str = parsed_array[0]
command_args: list[str] = parsed_array[1:]
return command, command_args


def main() -> None:
init_commands_list()

parser = argparse.ArgumentParser(
prog="ESBMC-ChatGPT",
description=HELP_MESSAGE,
Expand Down Expand Up @@ -295,11 +276,9 @@ def main() -> None:
parser.add_argument(
"-c",
"--command",
choices=command_names,
metavar="",
help="Runs the program in command mode, it will exit after the command ends with an exit code. Options: {"
+ ", ".join(command_names)
+ "}",
+ ", ".join(command_runner.builtin_commands_names)
+ "}. To see addon commands avaiilable: Run with '-c help'.",
)

parser.add_argument(
Expand All @@ -326,8 +305,8 @@ def main() -> None:

Config.init(args)
ESBMCUtil.init(Config.get_value("esbmc.path"))

check_health()
init_addons()

printv(f"Source code format: {Config.get_value('source_code_format')}")
printv(f"ESBMC output type: {Config.get_value('esbmc.output_type')}")
Expand All @@ -349,12 +328,19 @@ def main() -> None:
# If not, then continue to user mode.
if args.command != None:
command = args.command
command_names: list[str] = command_runner.command_names
if command in command_names:
print("Running Command:", command)
for idx, command_name in enumerate(command_names):
if command == command_name:
_run_command_mode(command=commands[idx], args=args)
_run_command_mode(command=command_runner.commands[idx], args=args)
sys.exit(0)
else:
print(
f"Error: Unknown command: {command}. Choose from: "
+ ", ".join(command_names)
)
sys.exit(1)

# ===========================================
# User Mode (Supports only 1 file)
Expand Down Expand Up @@ -444,7 +430,7 @@ def main() -> None:

# Check if it is a command, if not, then pass it to the chat interface.
if user_message.startswith("/"):
command, command_args = parse_command(user_message)
command, command_args = CommandRunner.parse_command(user_message)
command = command[1:] # Remove the /
if command == fix_code_command.command_name:
# Fix Code command
Expand All @@ -462,7 +448,7 @@ def main() -> None:
else:
# Commands without parameters or returns are handled automatically.
found: bool = False
for cmd in commands:
for cmd in command_runner.commands:
if cmd.command_name == command:
found = True
cmd.execute()
Expand Down
7 changes: 4 additions & 3 deletions esbmc_ai/ai_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,10 @@ def create_llm(
def convert_messages_to_tuples(
cls, messages: Iterable[BaseMessage]
) -> list[tuple[str, str]]:
"""Converts messages into a format understood by the ChatPromptTemplate - since it won't format
BaseMessage derived classes for some reason, but will for tuples, because they get converted into
Templates in function `_convert_to_message`."""
"""Converts messages into a format understood by the ChatPromptTemplate,
since it won't format BaseMessage derived classes for some reason, but
will for tuples, because they get converted into Templates in function
`_convert_to_message`."""
return [(message.type, str(message.content)) for message in messages]

@classmethod
Expand Down
1 change: 0 additions & 1 deletion esbmc_ai/chats/base_chat_interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@

from abc import abstractmethod
from typing import Optional
import traceback

from langchain.schema import (
BaseMessage,
Expand Down
2 changes: 2 additions & 0 deletions esbmc_ai/chats/reverse_order_solution_generator.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# Author: Yiannis Charalambous

"""This module contains the ReverseOrderSolutionGenerator"""

from langchain.schema import BaseMessage
from typing_extensions import override, Optional
from esbmc_ai.chats.solution_generator import SolutionGenerator
Expand Down
60 changes: 60 additions & 0 deletions esbmc_ai/command_runner.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
"""Contains code for managing and running built-in and addon commands."""

import re
from esbmc_ai.commands.chat_command import ChatCommand
from esbmc_ai.commands.help_command import HelpCommand


class CommandRunner:
"""Command runner manages running and storing commands."""

def __init__(self, builtin_commands: list[ChatCommand]) -> None:
self._builtin_commands: list[ChatCommand] = builtin_commands.copy()
self._addon_commands: list[ChatCommand] = []

# Set the help command commands
for cmd in self._builtin_commands:
if cmd.command_name == "help":
assert isinstance(cmd, HelpCommand)
cmd.commands = self.commands

@property
def commands(self) -> list[ChatCommand]:
"""Returns all commands. The list is copied."""
return self._builtin_commands + self._addon_commands

@property
def command_names(self) -> list[str]:
"""Returns a list of built-in commands. This is a reference to the
internal list."""
return [cmd.command_name for cmd in self.commands]

@property
def builtin_commands_names(self) -> list[str]:
"""Returns a list of built-in command names."""
return [cmd.command_name for cmd in self._builtin_commands]

@property
def addon_commands_names(self) -> list[str]:
"""Returns a list of the addon command names."""
return [cmd.command_name for cmd in self._addon_commands]

@property
def addon_commands(self) -> list[ChatCommand]:
"""Returns a list of the addon commands. This is a reference to the
internal list."""
return self._addon_commands

@staticmethod
def parse_command(user_prompt_string: str) -> tuple[str, list[str]]:
"""Parses a command and returns it based on the command rules outlined in
the wiki: https://github.com/Yiannis128/esbmc-ai/wiki/User-Chat-Mode"""
regex_pattern: str = (
r'\s+(?=(?:[^\\"]*(?:\\.[^\\"]*)*)$)|(?:(?<!\\)".*?(?<!\\)")|(?:\\.)+|\S+'
)
segments: list[str] = re.findall(regex_pattern, user_prompt_string)
parsed_array: list[str] = [segment for segment in segments if segment != " "]
# Remove all empty spaces.
command: str = parsed_array[0]
command_args: list[str] = parsed_array[1:]
return command, command_args
4 changes: 2 additions & 2 deletions esbmc_ai/commands/__init__.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
"""This module contains built-in commands that can be executed by ESBMC-AI."""

from .chat_command import ChatCommand
from .exit_command import ExitCommand
from .fix_code_command import FixCodeCommand, FixCodeCommandResult
from .help_command import HelpCommand
from .command_result import CommandResult

"""This module contains built-in commands that can be executed by ESBMC-AI."""

__all__ = [
"ChatCommand",
"ExitCommand",
Expand Down
6 changes: 6 additions & 0 deletions esbmc_ai/commands/chat_command.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
# Author: Yiannis Charalambous

"""Contains things related to chat commands."""

from abc import ABC, abstractmethod
from typing import Any, Optional

from esbmc_ai.commands.command_result import CommandResult


class ChatCommand(ABC):
"""Abstract Base Class for implementing chat commands."""

def __init__(
self,
command_name: str = "",
Expand All @@ -20,4 +24,6 @@ def __init__(

@abstractmethod
def execute(self, **kwargs: Optional[Any]) -> Optional[CommandResult]:
"""The main entrypoint of the command. This is abstract and will need to
be implemented."""
raise NotImplementedError(f"Command {self.command_name} is not implemented.")
13 changes: 10 additions & 3 deletions esbmc_ai/commands/command_result.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,22 @@
# Author: Yiannis Charalambous

from abc import abstractmethod
"""Contains the base class for chat command results."""

from abc import ABC, abstractmethod


class CommandResult(ABC):
"""Base class for the return result of chat commands."""

class CommandResult:
@property
@abstractmethod
def successful(self) -> bool:
"""Returns true if the execution of the command was successful. False if
otherwise. If false, then it is up to the command to provide a method to
access the reason."""
raise NotImplementedError()

def __str__(self) -> str:
return f"Command returned " + (
return "Command returned " + (
"successful" if self.successful else "unsuccessful"
)
4 changes: 4 additions & 0 deletions esbmc_ai/commands/exit_command.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# Author: Yiannis Charalambous

"""Contains the exit chat command."""

import sys
from typing import Any, Optional
from typing_extensions import override
Expand All @@ -8,6 +10,8 @@


class ExitCommand(ChatCommand):
"""Used to exit user chat mode gracefully."""

def __init__(self) -> None:
super().__init__(
command_name="exit",
Expand Down
8 changes: 5 additions & 3 deletions esbmc_ai/commands/help_command.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
# Author: Yiannis Charalambous

"""Contains the help command."""

from typing import Any, Optional
from typing_extensions import override
from .chat_command import ChatCommand


class HelpCommand(ChatCommand):
"""Command that prints helpful information about other commands. Including
addon commands."""

commands: list[ChatCommand] = []

def __init__(self) -> None:
Expand All @@ -14,9 +19,6 @@ def __init__(self) -> None:
help_message="Print this help message.",
)

def set_commands(self, commands: list[ChatCommand]) -> None:
self.commands = commands

@override
def execute(self, **_: Optional[Any]) -> Optional[Any]:
print()
Expand Down
Loading

0 comments on commit 2ecab63

Please sign in to comment.