diff --git a/avise/cli.py b/avise/cli.py index f823bef..0465f10 100644 --- a/avise/cli.py +++ b/avise/cli.py @@ -13,7 +13,7 @@ python -m avise --SET --connectorconf --SETconf --format json/html/md --output Example: - python -m avise --SET prompt_injection --connectorconf avise/configs/connector//ollama.json --SETconf avise/configs/SET/prompt_injection_mini.json + python -m avise --SET prompt_injection --connectorconf avise/configs/connector/languagemodel/ollama.json --SETconf avise/configs/SET/prompt_injection_mini.json """ @@ -164,11 +164,11 @@ def main(arguments=[]) -> None: # Predefined configs if args.connectorconf == "ollama": - args.connectorconf = "avise/configs/connector/ollama.json" + args.connectorconf = "avise/configs/connector/languagemodel/ollama.json" elif args.connectorconf == "openai": - args.connectorconf = "avise/configs/connector/openai.json" + args.connectorconf = "avise/configs/connector/languagemodel/openai.json" elif args.connectorconf == "genericrest": - args.connectorconf = "avise/configs/connector/genericrest.json" + args.connectorconf = "avise/configs/connector/languagemodel/genericrest.json" try: # Run the SET by calling run_test function. The selected SET's run() function is called. diff --git a/avise/configs/connector/genericrest.json b/avise/configs/connector/languagemodel/genericrest.json similarity index 100% rename from avise/configs/connector/genericrest.json rename to avise/configs/connector/languagemodel/genericrest.json diff --git a/avise/configs/connector/ollama.json b/avise/configs/connector/languagemodel/ollama.json similarity index 100% rename from avise/configs/connector/ollama.json rename to avise/configs/connector/languagemodel/ollama.json diff --git a/avise/configs/connector/openai.json b/avise/configs/connector/languagemodel/openai.json similarity index 100% rename from avise/configs/connector/openai.json rename to avise/configs/connector/languagemodel/openai.json diff --git a/avise/connectors/languagemodel/base.py b/avise/connectors/languagemodel/base.py index 53e1a94..8726905 100644 --- a/avise/connectors/languagemodel/base.py +++ b/avise/connectors/languagemodel/base.py @@ -66,6 +66,9 @@ def generate( def status_check(self) -> bool: """Perform a status check for the target API via a GET request. + Returns: + True if status check was successful. + Raises: Exception: If the target API is not reachable. """ diff --git a/avise/connectors/languagemodel/ollama.py b/avise/connectors/languagemodel/ollama.py index 1fc68a2..9ae8afa 100644 --- a/avise/connectors/languagemodel/ollama.py +++ b/avise/connectors/languagemodel/ollama.py @@ -284,7 +284,7 @@ def status_check(self) -> bool: ) def _list_models(self) -> List[str]: - """Helper function, used by status_check() to verify model availability. + """Helper method, used by status_check() to verify model availability. Returns: List of model names. diff --git a/docs/source/building.connector.rst b/docs/source/building.connector.rst new file mode 100644 index 0000000..e240c91 --- /dev/null +++ b/docs/source/building.connector.rst @@ -0,0 +1,557 @@ +.. building_connector: + +Building a Connector +================================= + +Connectors include the logic of making requests to, and receiving responses from, AI models. Before +executing any SETs on your target model, a connector must be configured appropriately. In this section, +we will walkthrough building a new connector. + +1. Creating a Base Connector Class +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +To make creating new Connectors easier and ensuring compability with rest of the AVISE framework, +AVISE has a Base Connector class for each different type of a target system. Any Connectors created +should inherit from a Base Connector class and try to override as little as possible from it. + +.. note:: + +Before creating a new Base Connector class, check out ``avise/connectors/`` directory and see if there +is a Base Connector class for the type of an AI system you wish to create a connector for. + +In this example, we will be creating a Connector for Language Models running on `Ollama `_. +So the first thing we need to do, is figure out what kinds of inputs language models take and what do their outputs +generally look like. + +As language models generally take inputs in the form of a dictionary that contains ``role`` and ``content`` fields, +we can first define a data class that represents a single prompt or response in a conversation, that can be helpful later on: + +.. code:: python + + @dataclass + class Message: + """Represents a single message in a multi-turn conversation + + Attributes: + role: The role of the message sender. "system", "user", or "assistant": https://platform.openai.com/docs/guides/text + content: The text content of the message + """ + + role: str + content: str + + +Next, we create the Base Connector Class. It is an abstract class inheriting from abc.ABC. +This abstract base class provides us the structure that Connectors can inherit. We want each +language model connector to at least have methods for 1. performing a status check to make sure +the target model is reachable, and 2. generating a response from the target model. Additionally, +as we want users to be able to configure their connector via a configuration JSON file, we can +create a class attribute that holds the configuration data as a dictionary. + +The following is the finished abstract base class for language model connectors: + + +.. code:: python + + class BaseLMConnector(ABC): + """A connector handles communication with a specific API / backend, + abstracting the API usage for the framework. + This allows SET cases to be written only once and users are able to run them against different models with different configurations. + + Class Methods: + - generate(): Generate a response from target model. + - status_check(): Verify that the target API endpoint is available. + + Class Attributes: + config: Connector configuration data. + """ + + config: dict = {} + + @abstractmethod + def generate( + self, + data: dict, + ) -> dict: + """Generate a response from the target model via the target API. + + Arguments: + data: Dictionary containing data required for the generation API request. + multi_turn: Boolean flag to indicate if engaging in a multi turn conversation\ + with the target model. Default False. + + Returns: + Model response as a dictionary. The dict contains "response" field with the model response as a str. + + Raises: + RuntimeError: If the API call fails. + """ + pass + + @abstractmethod + def status_check(self) -> bool: + """Perform a status check for the target API via a GET request. + + Returns: + True if status check was successful + + Raises: + Exception: If the target API is not reachable. + """ + pass + +.. _building_connector_section_2: + +2. Creating a Connector +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Now that we have our base connector class for language models, we can move into creating +the actual connector class. In this example, we want to be able to run Security Evaluation +Tests on a target language model running via `Ollama `_, so we will +create an Ollama Connector. + +For clarity, here are the package imports that we will use later on in the code: + +* ``import logging``: logging is used to create logs that will help with debugging and informing the user of what's happening when the program is executing. +* ``from typing import List`` List is used as a type hint for method variables that are a list of some specific type. +* ``import ollama`` We will use the ollama Client for making requests to the API endpoint. +* ``from .base import BaseLMConnector, Message`` These we defined earlier and will now use. +* ``from ...registry import connector_registry`` connector_registry holds information of all connectors, sets, and formats available to the Execution Engine. We want to add our connector to the registry as well. +* ``from ...utils import ansi_colors`` ansi_colors is a dictionary of ansi codes for different colors we can use to make our logging a bit prettier and easier to follow. + +.. _building_connector_section_2_init: + +Initialization +^^^^^^^^^^^^^^^^ + +First we define the class and its ``__init__`` method, that is called whenever an instance of the +class is created. We want to make sure the connector is added to the ``registry``, so that the +Execution Engine can find and use the connector when running SETs later on - for this, we +add the ``@connector_registry.register("ollama-lm")`` class decorator for the connector class. +Additionally, we include the ``name = "ollama-lm"`` attribute for the class. + +In the ``__init__`` method, we set the instance attributes based on the read configuration JSON file. +In this step, we need to think what different possible configurations do we want users to be able to +modify and pass to the connector class. As Ollama has a REST API endpoint we can make requests to for +generating a model response, we can take a look at the Ollama documentations and figure out some most +commonly used fields in an API request made to its endpoint. These include, among others: + +* ``api_key``: used for authorized API requests +* ``model``: the name of the model +* ``url``: url of the API endpoint +* ``max_tokens``: maximum amount of tokens to generate per response + +These are a good starting point and with them we can initialize the Ollama client as an instance +variable, that we will use to generate responses from the target model. Here is the code we have written +so far, including logging, which is useful later on in debugging and informing users of what's currently +happening when they are running SETs: + + +.. code:: python + + logger = logging.getLogger(__name__) + + @connector_registry.register("ollama-lm") + class OllamaLMConnector(BaseLMConnector): + """Connector for communicating with the Ollama API. + + Used by Security Evaluation Tests for sending prompts to target Ollama models and collecting their responses. + """ + + name = "ollama-lm" + + def __init__( + self, + config: dict, + ): + """Initialize the Ollama connector. + + Args: + config: Dictionary containing data from Connector configuration JSON. + """ + self.api_key = None + + self.model = config["target_model"]["name"] + self.base_url = config["target_model"]["api_url"] + if ( + "max_tokens" in config["target_model"] + and config["target_model"]["max_tokens"] is not None + ): + self.max_tokens = config["target_model"]["max_tokens"] + else: + self.max_tokens = 512 + if ( + "api_key" in config["target_model"] + and config["target_model"]["api_key"] is not None + ): + self.api_key = config["target_model"]["api_key"] + # Configure client with optional authentication headers + self.client = ollama.Client( + host=self.base_url, + headers={"Authorization": f"Bearer {self.api_key}"}, + ) + else: + self.client = ollama.Client(host=self.base_url) + + logger.info(f" Ollama Connector Initialized") + logger.info(f" Base URL: {self.base_url}") + logger.info(f" Model: {self.model}") + if self.api_key: + logger.info( + f" API Key: {'*' * 8}...{self.api_key[-4:] if len(self.api_key) > 4 else '****'}" + ) + +Defining status_check +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Next, we can write the method for performing status checks on the API. This can be as basic +as making a GET request to the API endpoint, and checking if it returns the status code ``200``. +But since ollama has a convenient way of providing available model names via a GET request, +we can utilize this to ensure our target model is in the list of available models while +simultaneously performing the status check. For this, we can first define a helper method +``_list_models()`` that makes a GET request to the ollama API endpoint, and returns +the list of available models: + +.. code:: python + + def _list_models(self) -> List[str]: + """Helper method, used by status_check() to verify model availability. + + Returns: + List of model names. + + Raises: + Exception: If the API is not reachable. + """ + response = self.client.list() + models_list = response.get("models", []) + + model_names = [] + for model in models_list: + name = model.get("model") + if name: + model_names.append(name) + + return model_names + +After writing the helper method for making a GET request and returning the list of available +models, we can define another helper method, ``_match_model()``, that will check whether our +target model is in the list of available models: + +.. code:: python + + def _match_model(self, model_name: str, available_models: List[str]) -> bool: + """Check if a model name exists in the list of available models. + Arguents: + model_name: Name of the target model. + available_models: List of available models to scan for target model. + + Returns: + True if model_name found in available models, False if model_name not found in available models. + """ + for model in available_models: + if model_name == model: + return True + return False + + +With these helper methods, we can define our ``status_check()`` method. The ``status_check`` +calls the ``_list_models()`` method to first attempt to make the GET request and get the list +of available models as a response. If it succeeds, the ``_match_model`` method is called to +check that our target model is in the list of available models. If it succeeds the ``status_check()`` +returns ``True``. If either of the helper functions fail, the ``status_check()`` raises an error: + +.. code:: python + + def status_check(self) -> bool: + """Check if the connector can reach the Ollama API and the target model is available. + + Returns: + True if API is reachable and the target model exists. + + Raises: + ConnectionError: If the API is not reachable. + ValueError: If the model is not found. + """ + # Step 1: Check backend connectivity and get available models + try: + model_names = self._list_models() + except Exception as e: + raise ConnectionError( + f"Cannot connect to Ollama backend at {self.base_url}: {e}" + ) + + # Step 2: Check if model exists + logger.info(f"Available models found: {model_names}") + + if self._match_model(self.model, model_names): + logger.info(f"Model '{self.model}' found.") + return True + + raise ValueError( + f"Model '{self.model}' not found in Ollama backend. " + f"Available models: {model_names}" + ) + + +Defining generate() +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Next, let's write the ``generate()`` method. We want to be able to create SETs that use either +multi turn or single turn attacks to test a language model, so we will write ``_single_turn()`` +and ``_multi_turn()`` helper methods to accommodate this. They are very similar, and only differ +in how the API request is made. Both have ``data`` parameter, that is a dictionary containing all +the required data for making the API request. And both return the model response in ``{"response": str}`` +format. + +.. code:: python + :caption: ``_single_turn() helper method`` + + def _single_turn(self, data: dict) -> dict: + """Make a single-turn generation. + + Arguments: + data: Dictionary with required data for API request. + + Returns: + {"response": str} + """ + if "system_prompt" in data: + # Generate single-turn response with system prompt. + try: + response = self.client.generate( + model=self.model, + system=data["system_prompt"], + prompt=data["prompt"], + options={ + "temperature": data["temperature"], + "num_predict": data["max_tokens"], + }, + ) + except Exception as e: + logger.error( + f"{ansi_colors['red']}ERROR while generating response from model: {e}{ansi_colors['reset']}" + ) + raise RuntimeError( + "Failed to generate a response from model due to an error." + ) from e + return {"response": response.response} + try: + response = self.client.generate( + model=self.model, + prompt=data["prompt"], + options={ + "temperature": data["temperature"], + "num_predict": data["max_tokens"], + }, + ) + except Exception as e: + logger.error( + f"{ansi_colors['red']}ERROR while generating response from model: {e}{ansi_colors['reset']}" + ) + raise RuntimeError( + "Failed to generate a response from model due to an error." + ) from e + return {"response": response.response} + + +.. code:: python + :caption: ``_multi_turn() helper method`` + + def _multi_turn(self, data: dict) -> dict: + """Make a multi-turn generation. + + Arguments: + data: Dictionary with required data for API request. + + Returns: + {"response": str} + """ + # Convert Message objects to Ollama's expected format + ollama_messages = [ + {"role": msg.role, "content": msg.content} for msg in data["messages"] + ] + if "system_prompt" in data: + # If system prompt is given in the data dict, insert it into ollama_messages + ollama_messages.insert( + 0, {"role": "system", "content": data["system_prompt"]} + ) + try: + response = self.client.chat( + model=self.model, + messages=ollama_messages, + options={ + "temperature": data["temperature"], + "num_predict": data["max_tokens"], + }, + ) + return {"response": response["message"]["content"]} + except Exception as e: + logger.error( + f"{ansi_colors['red']}ERROR during chat with model: {e}{ansi_colors['reset']}" + ) + raise RuntimeError(f"Failed to chat with model.") from e + +Now with the help of ``_single_turn()`` and ``_multi_turn`` methods, we can write the ``generate()`` +method. As we use ``generate()`` method to handle both, single turn and multi turn SET instances, +we need some way to differentiate when we are making a single turn or a multi turn generation. For this, +we can use a *boolean* parameter ``multi_turn``, that indicates what kind of a generation we are making +when calling this method. In addition, we give the required data for making the API request in a dictionary +``data`` parameter. + +The ``generate()`` method checks that all expected fields have a valid value in the connector configuration +JSON file, and based on the value of the ``multi_turn`` parameter, executes the respective helper method. + +.. code:: python + :caption: ``generate()`` method + + def generate(self, data: dict, multi_turn: bool = False) -> dict: + """Generate a response from the target model via the Ollama API. + + Arguments: + data: Dictionary containing data required for the generation API request. + Valid Keys: + - prompt : str + Prompt for single turn generation. Required for single turn conversation. + - messages: list[Message] + List of Message objects representing the conversation history.\ + Message objects contain 'role' and 'content' attributes.\ + Required for multi-turn conversation. + - system_prompt : str + Optional system prompt + - temperature : float [0, 1] + Optional temperature setting for the target model. Defaults to 0.5 if not set. + - max_tokens : int + Optional setting for maximum generated tokens. Defaults to 512 if not set. + multi_turn: Boolean flag to indicate if engaging in a multi turn conversation\ + with the target model. Default False. + + Returns: + API response. + + Raises: + KeyError: If a required key is missing from data. + ValueError: If a value in data is of a wrong type. + RuntimeError: If the API call fails. + """ + if "temperature" not in data: + data["temperature"] = 0.5 + data["max_tokens"] = self.max_tokens + + if "system_prompt" in data: + if not isinstance(data["system_prompt"], str): + raise ValueError( + 'If using "system_prompt" in data, it needs to be a string.' + ) + + if multi_turn: + if "messages" not in data: + raise KeyError( + 'Multi-turn conversation requires a "messages" key in \ + data variable, which contains a List of Message objects \ + representing the conversation history.' + ) + if not isinstance(data["messages"], list): + raise ValueError( + 'Multi-turn conversation requires a "messages" key in \ + data variable, which contains a List of Message objects \ + representing the conversation history.' + ) + for message in data["messages"]: + if not isinstance(message, Message): + raise ValueError( + 'Multi-turn conversation requires a "messages" key in \ + data variable, which contains a List of Message objects \ + representing the conversation history.' + ) + return self._multi_turn(data=data) + else: + if "prompt" not in data: + raise KeyError( + 'Single-turn conversation requires a "prompt" key in \ + data variable, which contains a prompt as a string.' + ) + if not isinstance(data["prompt"], str): + raise ValueError( + 'Single-turn conversation requires a "prompt" key in \ + data variable, which contains a prompt as a string.' + ) + return self._single_turn(data=data) + + +Creating a configuration JSON file for the Connector +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +To be able to pass the required values for making an API request with the connector, +we need to create a configuration JSON file for our new connector. All of the connector +configuration files are stored in the ``avise/configs/connector/`` directory, and as our +connector is intended for language models, we will insert our's into +``avise/configs/connector/languagemodel/`` directory as ``ollama.json``. + +The configuration JSON should include all fields that are required for making an API request +with the connector (when we previously wrote the ``__init__`` method, we included each as an +instance attribute). If there are any optional fields that could be useful for some use-case, +we can set their value as ``null``: + +.. code:: text + :caption: ``avise/configs/connector/languagemodel/ollama.json`` + + { + "target_model": { + "connector": "ollama-lm", + "type": "language_model", + "name": "phi3:latest", + "api_url": "http://localhost:11434", + "api_key": null, + "max_tokens": 768 + } + } + +We need to give the name of our connector (which we defined earlier in the +:ref:`building_connector_section_2_init` section) into a ``connector`` field here, +so that the Execution Engine knows which connector we want to use when running SETs. +The ``type`` field might be used in later AVISE versions, so it is useful to include it. +It defines the type of the AI systems we are connecting to with the connector. + + +Testing the new Connector +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Now that we have created a new Ollama Connector and a configuration JSON file for it, +it is time to make sure it works as we intended. +As we have created a connector for language model SET cases, we can try to run some available +SET with the new connector to see if it works. ``prompt_injection`` SET with the +``avise/configs/SET/languagemodel/single_turn/prompt_injection_mini.json`` configuration is great +for this as it executes quickly. + +By running the ``prompt_injection`` SET in the root directory of AVISE, with the following command, +we can use the latest modification we have made to the codebase: + +.. code:: bash + + python -m avise --SET prompt_injection --connectorconf avise/configs/connector/languagemodel/ollama.json --SETconf avise/configs/SET/languagemodel/single_turn/prompt_injection_mini.json + +* ``--SET``: with this argument, we tell the CLI which SET we wish to execute. +* ``--connectorconf``: with this argument, we tell the CLI the path of the connector configuration JSON we just created. +* ``--SETconf``: with this optional argument, we can give the CLI a path to a custom SET configuration file +(there are predefined default paths if we don't use this argument) + +If our code has no errors and works as we intended, the Execution Engine starts running the SET and eventually produces +a report file and prints something like this to the console: + +.. code:: text + Security Evaluation Test completed! + Format: JSON + Total: 5 + Passed: 2 (40.0%) + Failed: 3 (60.0%) + Errors: 0 + +In the case that there were some errors in our code, we need to debug them until the SET cases execute fully. + + +Contributing the new Connector +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Now that we have a functional new connector, we can contribute it to the main repository so other users can utilize +it as well! For details on how to contribute a connector to the main repository, check out :ref:`contributing_connector`. \ No newline at end of file diff --git a/docs/source/building.evaluator.rst b/docs/source/building.evaluator.rst new file mode 100644 index 0000000..79d3bd0 --- /dev/null +++ b/docs/source/building.evaluator.rst @@ -0,0 +1,6 @@ +.. building_evaluator: + +Building an Evaluator +================================= + +TODO \ No newline at end of file diff --git a/docs/source/building.pipeline.rst b/docs/source/building.pipeline.rst new file mode 100644 index 0000000..66f65e4 --- /dev/null +++ b/docs/source/building.pipeline.rst @@ -0,0 +1,6 @@ +.. building_pipeline: + +Building a Pipelines +================================= + +TOOD: Step-by-step walkthrough example of how to build a Pipeline \ No newline at end of file diff --git a/docs/source/building.set.rst b/docs/source/building.set.rst new file mode 100644 index 0000000..f978f08 --- /dev/null +++ b/docs/source/building.set.rst @@ -0,0 +1,6 @@ +.. building_set: + +Building a Security Evaluation Test +================================= + +TOOD: Step-by-step walkthrough example of how to build a set \ No newline at end of file diff --git a/docs/source/configuring.connectors.rst b/docs/source/configuring.connectors.rst index db53a34..e37fae8 100644 --- a/docs/source/configuring.connectors.rst +++ b/docs/source/configuring.connectors.rst @@ -12,11 +12,11 @@ Ollama Connector ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ `Ollama `__ is a widely used software for running language models. -To use AVISE with Ollama models, you can modify the existing ``avise/configs/connector/ollama.json`` +To use AVISE with Ollama models, you can modify the existing ``avise/configs/connector/languagemodel/ollama.json`` configuration file: .. code-block:: json - :caption: ``avise/configs/connector/ollama.json`` + :caption: ``avise/configs/connector/languagemodel/ollama.json`` { "target_model": { @@ -42,11 +42,11 @@ model you wish to test and evaluate. ``target_model`` requires the following sub OpenAI Connector ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -To use AVISE with `OpenAI `__ models, you can modify the existing ``avise/configs/connector/openai.json`` +To use AVISE with `OpenAI `__ models, you can modify the existing ``avise/configs/connector/languagemodel/openai.json`` configuration file: .. code-block:: json - :caption: ``avise/configs/connector/openai.json`` + :caption: ``avise/configs/connector/languagemodel/openai.json`` { "target_model": { @@ -59,7 +59,7 @@ configuration file: } } -In the Connector configuraiton files, ``target_model`` defines the configurations for the +In the Connector configuration files, ``target_model`` defines the configurations for the model you wish to test and evaluate. ``target_model`` requires the following subfields: * ``"connector"``: Name of the connector to use (See available connectors by running CLI command ``avise --connector_list``) @@ -77,10 +77,10 @@ Generic REST API Connector ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ With the Generic REST API Connector, you can connect AVISE to any RESTful API Endpoint. To configure the Generic REST API Connector, -modify the existing ``avise/configs/connector/genericrest.json`` configuration file: +modify the existing ``avise/configs/connector/languagemodel/genericrest.json`` configuration file: .. code-block:: json - :caption: ``avise/configs/connector/genericrest.json`` + :caption: ``avise/configs/connector/languagemodel/genericrest.json`` { "target_model": { diff --git a/docs/source/configuring.sets.rst b/docs/source/configuring.sets.rst index af7eaec..8a7647c 100644 --- a/docs/source/configuring.sets.rst +++ b/docs/source/configuring.sets.rst @@ -63,6 +63,6 @@ Our implementation of Red Queen (https://arxiv.org/abs/2409.17458) can be config In the Red Queen configuration file, the following fields can be changed to adjust how the SET is ran: -* ``"incremental_execution"``: true or false. Determines whether the SET is ran incrementally (one prompt in the conversation at a time, and a new response from the target model is generated after each prompt) or by only generating the final response from the target model using the template conversation. -* ``"use_adversarial_languagemodel"``: true or false. Only applies if ``"incremental_execution"`` is true. Determines whether an adversarial language model is used to modify each subsequent prompt in the conversation template after a response has been generated by the target model. +* ``"incremental_execution"``: **true** or **false**. Determines whether the SET is ran incrementally (one prompt in the conversation at a time, and a new response from the target model is generated after each prompt) or by only generating the final response from the target model using the template conversation. +* ``"use_adversarial_languagemodel"``: **true** or **false**. Only applies if ``"incremental_execution"`` is true. Determines whether an adversarial language model is used to modify each subsequent prompt in the conversation template after a response has been generated by the target model. * ``"sets"``: Contains a list of SET cases. More cases can be added or existing cases can be removed to your liking. Each case contains a conversation template that is used in the execution of Red Queen. diff --git a/docs/source/connectors.rst b/docs/source/connectors.rst index d4c3161..fc2dcc2 100644 --- a/docs/source/connectors.rst +++ b/docs/source/connectors.rst @@ -3,7 +3,7 @@ Connectors Connectors include the logic of making requests to, and receiving responses from, AI models. Before executing any SETs on your target model, a connector must be configured appropriately. ``avise/configs/connector/`` directory includes template configuration -JSON files for different types of AI model hosts. Additionally, ``avise/configs/connector/genericrest.json`` configuration file can be +JSON files for different types of AI model hosts. Additionally, ``avise/configs/connector//genericrest.json`` configuration file can be adjusted to connect to models accessible via any REST API endpoint. .. toctree:: diff --git a/docs/source/contributing.connector.rst b/docs/source/contributing.connector.rst new file mode 100644 index 0000000..7f71236 --- /dev/null +++ b/docs/source/contributing.connector.rst @@ -0,0 +1,64 @@ +.. _contributing_connector: + +Contributing a Connector +================================= + +Connectors encapsulate the logic for making requests to, and receiving responses from, AI +models. Before executing any SETs on your target model, a connector must be configured +appropriately. The ``avise/configs/connector/`` directory includes template configuration +JSON files for different types of AI model hosts. Additionally, the +``avise/configs/connector//genericrest.json`` configuration file can be adjusted +to connect to models accessible via any REST API endpoint. + +Writing a New Connector +----------------------- + +Before starting a new connector module, check whether any of the existing connectors could +be adapted to fit your needs. A new connector should only be created if none of the current +modules are a suitable fit. For a practical reference, see how ``avise.connectors.languagemodel.ollama`` +was built along with its configuration file ``avise/configs/connector/languagemodel/ollama.json`` — +they serve as a good example of the structure and conventions to follow. + +New connectors must inherit from the base connector class corresponding to the target AI +system type. For example, if you are writing a connector for a Language Model, your connector +should inherit from ``avise.connectors.languagemodel.base``. When implementing your connector, +override as little as possible — only the methods that are strictly necessary for your +specific integration. This keeps connectors lean and maintainable. + +Check out :ref:`_building_connector` for a step-by-step example guide, on how to build a new connector. + +Testing Your Connector +---------------------- + +.. note:: + +Before you can use the AVISE CLI to test if your connector works, remember to import it in the +``avise/connectors//__init__.py`` file. This adds the connector to the ``registry`` +and can then be used by the Execution Engine. + +While developing your connector, you can test it by running some SET via the CLI and giving the +configuration JSON file of your connector as an argument: + +.. code:: bash + + python -m avise --SET prompt_injection --connectorconf path/to/your/connector/config/json + + +AVISE supports ``pytest`` tests located in the ``./unit-tests`` directory. You can run the full +test suite from the root of the repository with: + +.. code-block:: bash + + python -m pytest + +All tests must pass before any code can be merged to the main repository. When submitting a Pull Request, ensure +that the entire test suite passes, and consider adding tests that cover the behaviour of +your new connector to help maintain confidence in the codebase going forward. + +Contributing Your Connector +---------------------------- + +Did you build a connector that could be useful to other users of AVISE as well? We love community contributions and +would like to include it in the main repository. Once your connector is complete and all +unit tests are passing, take a look at the :ref:`_contributing` documentation for guidance on how +to submit your work to the project. \ No newline at end of file diff --git a/docs/source/contributing.pipeline.rst b/docs/source/contributing.pipeline.rst index 3245b19..449ffb1 100644 --- a/docs/source/contributing.pipeline.rst +++ b/docs/source/contributing.pipeline.rst @@ -1,4 +1,68 @@ +.. _contributing_pipeline: + Contributing a Pipeline ================================= -TOOD: How to add a new pipeline to avise. \ No newline at end of file +BaseSETPipelines contain the execution flow logic of SETs. Each BaseSETPipeline has 4 phases, for which the required +data contracts are detailed in the Pipeline Schema. The 4 phases are: Initialization, Execution, Evaluation, and Reporting. +In the Initialization phase, the SET cases are loaded from a JSON configuration file in ``avise/configs/SET/``. Execution phase +executes the loaded SETs on the target model, or system, and returns data objects for evaluation. In Evaluation phase, the data +objects containing results from executing SET are evaluated by the evaluators and optionally a evaluation language model. In the +Reporting phase, Evaluation data objects which contain the evaluation results are passed to Report Generation tools, and a final +report of the executed SETs and their evaluation results is generated. The final report includes detailed logs as a JSON file, +and a human-readable HTML summarizing the executed SETs. + +In order to develop SETs for some type of a target AI model or system (e.g. language models) not yet supported by AVISE, +first a BaseSETPipeline has to be created to accommodate a new execution flow for the SETs. Once a BaseSETPipeline has +been developed, it can be extended to create as many SETs as necessary. + + +Writing a New Pipeline +----------------------- + +Before starting a new pipeline module, check whether any of the existing pipelines could +be used to fit your needs. A new pipeline should only be created if none of the current +modules are a suitable fit for the SET you are trying to develop. For a practical reference, +see how ``avise.pipelines.languagemodel.pipeline`` +was built — +it serves as a good example of the structure and conventions to follow. + + +Check out :ref:`_building_pipeline` for a step-by-step example guide, on how to build a new pipeline. + +Testing Your Pipeline +---------------------- + +In order to test if a pipeline is functional, it needs to be extended with a SET that can be ran on +some target model/system. This is why it is adviced to only create a new pipeline when one is needed +to create a new SET. For detailed guide on how to create a SET, check out :ref:`_building_set`. + +After you have developed the first iterations of a new BaseSETPipeline, and some SET to go with it, +you can test if they work by running the SET on some target model. Remember to import the SET in +``avise/sets///__init__.py``. This adds the SET into the ``registry`` and +allows the Execution Engine to use it. After you have added the import, you can run the SET on some +target model with e.g.: + +.. code:: bash + + python -m avise --SET YOUR_SET_ID --SETconf path/to/your/set/config/json --connectorconf ollama + + +AVISE supports ``pytest`` tests located in the ``./unit-tests`` directory. You can run the full +test suite from the root of the repository with: + +.. code-block:: bash + + python -m pytest + +All tests must pass before any code can be merged to the main repository. When submitting a Pull Request, ensure +that the entire test suite passes, and consider adding tests that cover the behaviour of +your new SET to help maintain confidence in the codebase going forward. + +Contributing Your Pipeline +---------------------------- + +Did you build a pipeline that could be useful to other users of AVISE as well? We love community contributions and +would like to include it in the main repository. Once your pipeline and SET is complete and all +unit tests are passing, take a look at the :ref:`_contributing` documentation for guidance on how +to submit your work to the project. \ No newline at end of file diff --git a/docs/source/contributing.rst b/docs/source/contributing.rst index 9c4fe62..c51f739 100644 --- a/docs/source/contributing.rst +++ b/docs/source/contributing.rst @@ -1,4 +1,47 @@ -Contributing to the Repository -================================= +.. _contributing: -TODO: How to contribute code to the repository. \ No newline at end of file + +Contributing to AVISE +=========================== + +We warmly welcome contributions from the community! AVISE is an open-source project, +and its continued improvement depends on the involvement of developers like you. Whether you +are fixing a bug, adding a new feature, or improving documentation, your contributions help +make AVISE better for everyone. + +This page descibes the general process of contributing code to AVISE. If you are looking for +more detailed instructions on how to extend AVISE by contributing a new Security Evaluation Test, +Pipeline, or Connector, check out the respective contribution pages: :ref:`contributing_set`, +:ref:`_contributing_pipeline`, :ref:`_contributing_connector`. + +Getting in Touch +---------------- + +If you have questions, ideas, or need guidance before contributing, the best way to reach +the AVISE development team is through the `AVISE Discord channel `_. +The community and core developers are active there and happy to assist new contributors, +discuss proposed changes, or help troubleshoot issues. + +We look forward to your contributions and thank you for helping improve AVISE! + +Contribution Workflow +--------------------- + +To contribute code to AVISE, please follow the steps below: + +- **Fork the repository** — Navigate to the `AVISE GitHub repository `_ and click the *Fork* button to create your own copy of the project under your GitHub account. +- **Clone your fork** — Clone your forked repository to your local machine using ``git clone``. +- **Create a feature branch** — Create a new branch for your changes (e.g. ``git checkout -b feature/my-new-feature``) to keep your work isolated from the ``main`` branch. +- **Make your changes** — Implement your fix, feature, or improvement. Ensure your code follows the project's coding style and conventions, and include unit tests where applicable. +- **Commit and push** — Commit your changes with a clear and descriptive commit message, then push your branch to your forked repository. +- **Open a Pull Request** — Navigate to the original AVISE repository on GitHub and open a Pull Request (PR) from your feature branch. Provide a clear description of what your changes do and why they are needed. + +Review and Merging Process +-------------------------- + +Once a Pull Request is submitted, it will be reviewed by the AVISE developers. Automated tests +are ran on the code to ensure it follows the project's coding style and passes all unit tests. +The developers will examine the code for overall alignment +with the project's goals. Reviewers may request changes or clarifications before approval. +Once the Pull Request is approved and all checks pass, it will be merged into the main +repository by a maintainer. diff --git a/docs/source/contributing.set.rst b/docs/source/contributing.set.rst index 7f359f7..259c8a8 100644 --- a/docs/source/contributing.set.rst +++ b/docs/source/contributing.set.rst @@ -1,4 +1,54 @@ +.. _contributing_set: + Contributing a Security Evaluation Test ================================= -TODO: How to add a SET to repository. \ No newline at end of file +Security Evaluation Tests, or SETs, contain the detailed logic for identifying a specific vulnerability +or assessing the security of a target system or component within a specified scope. SETs inherit the base +logic for the execution flow of a certain type of a SET from BaseSETPipelines. For example, all language model +SETs inherit the execution flow logic from ``pipelines.languagemodel.BaseSETPipeline``. + +Writing a New SET +----------------------- + +Before starting a new SET module, check whether any of the existing SETs could +be used to fit your needs. Often it is enough to create a new SET configuration JSON +that modifies how an existing SET executes. For a practical reference, +see how ``avise.sets.languagemodel.multiturn.prompt_injection`` was built — +it serves as a good example of the structure and conventions to follow. + + +Check out :ref:`_building_set` for a step-by-step example guide, on how to build a new pipeline. + +Testing Your SET +---------------------- + +In order to test if a SET is functional, you can try to run it on some target model with the AVISE +CLI. Remember to import your SET in the ``avise/sets///__init__.py`` file. +This adds the SET into the ``registry``, allowing the Execution Engine to find and run it. + +When the SET is ready to be tested, you can run it with the CLI by e.g.: + +.. code:: bash + + python -m avise --SET YOUR_SET_ID --SETconf path/to/your/set/config/json --connectorconf ollama + + +AVISE supports ``pytest`` tests located in the ``./unit-tests`` directory. You can run the full +test suite from the root of the repository with: + +.. code-block:: bash + + python -m pytest + +All tests must pass before any code can be merged to the main repository. When submitting a Pull Request, ensure +that the entire test suite passes, and consider adding tests that cover the behaviour of +your new SET to help maintain confidence in the codebase going forward. + +Contributing Your Pipeline +---------------------------- + +Did you build a SET that could be useful to other users of AVISE as well? We love community contributions and +would like to include it in the main repository. Once your SET is complete and all +unit tests are passing, take a look at the :ref:`_contributing` documentation for guidance on how +to submit your work to the project. \ No newline at end of file diff --git a/docs/source/core_components.rst b/docs/source/core_components.rst index f4514be..4d4353a 100644 --- a/docs/source/core_components.rst +++ b/docs/source/core_components.rst @@ -22,8 +22,9 @@ or assessing the security of a target system or component within a specified sco logic for the execution flow of a certain type of a SET from BaseSETPipelines. For example, all language model SETs inherit the execution flow logic from ``pipelines.languagemodel.BaseSETPipeline``. -Developing new SETs and contributing them to the repository is straightforward. ``TODO: Add link or add details -of an example of developing a new SET.`` +Developing new SETs and contributing them to the repository is straightforward. Check out :ref:`_contributing_set` +for details on how to contribute a new SET to the repository, and :ref:`_building_set` for step-by-step guide on +how to create a new SET. BaseSETPipelines diff --git a/docs/source/index.rst b/docs/source/index.rst index f5f9ac7..803f3a8 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -49,6 +49,15 @@ and we're happy to help you. reportgen execution_engine +.. toctree:: + :caption: Extending: + :maxdepth: 1 + + building.set + building.pipeline + building.connector + building.evaluator + .. toctree:: :caption: Contributing: :maxdepth: 1 diff --git a/docs/source/sets.rst b/docs/source/sets.rst index 31cdb24..36e15bf 100644 --- a/docs/source/sets.rst +++ b/docs/source/sets.rst @@ -6,8 +6,9 @@ or assessing the security of a target system or component within a specified sco logic for the execution flow of a certain type of a SET from BaseSETPipelines. For example, all language model SETs inherit the execution flow logic from ``pipelines.languagemodel.BaseSETPipeline``. -Developing new SETs and contributing them to the repository is straightforward. ``TODO: Add link or add details -of an example of developing a new SET.`` +Developing new SETs and contributing them to the repository is straightforward. Check out :ref:`_contributing_set` +for details on how to contribute a new SET to the repository, and :ref:`_building_set` for step-by-step guide on +how to create a new SET. .. toctree:: :maxdepth: 2 diff --git a/pyproject.toml b/pyproject.toml index 5f7dfa6..1b92436 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -53,6 +53,10 @@ exclude = [ ] [tool.pytest.ini_options] +pythonpath = [ + "." +] + testpaths = ["unit-tests"] [tool.ruff] diff --git a/unit-tests/test_cli.py b/unit-tests/test_cli.py index 973050a..060372f 100644 --- a/unit-tests/test_cli.py +++ b/unit-tests/test_cli.py @@ -5,7 +5,7 @@ from avise import cli, __version__ SET_CONF_PATH = "avise/configs/SET//languagemodel/single_turn/prompt_injection_mini.json" -CONNECTOR_CONF_PATH = "avise/configs/connector/ollama.json" +CONNECTOR_CONF_PATH = "avise/configs/connector/languagemodel/ollama.json" test_incorrect_args_cases = [("--incorrectargument", "unrecognized argument"), (f"--SET prompt_injection --connectorconf {CONNECTOR_CONF_PATH} --SETcof {SET_CONF_PATH}", "unrecognized argument")]