F5 AI Gateway Python SDK Quickstart

About processors

There are three types of alterations which a processor may make on a request.

  • Annotation

  • Rejection

  • Modification

A processor can only make these types of alterations when a specific parameter is provided.

If there is an attempt to make a modification without the required parameter, the SDK will drop the modification, and it will log a warning. For example:

WARNING:root:CatClassifierProcessor tried to annotate request with tags when parameters.annotate was set to false, tags will be dropped

Annotation

A processor can add tags to a request which can be used by AI Gateway to route the request. This is controlled by the parameter annotate, which defaults to true.

Rejection

A processor can reject a request which causes a rejection message to be returned to the client. This is controlled by the parameter reject, which defaults to false.

Modification

A processor can modify the prompt that is sent by a client or modify the response from an upstream model. This is controlled by the parameter modify, which defaults to false.

Before you begin

To follow the steps in this guide you will need:

  • A Python 3.11+ environment with pip or an alternative installed

  • An ASGI server

In this example we will use uvicorn as our ASGI server. Install it first if you don’t have it already or adjust the commands to use the server of your choice.

Note

You may want to use a Python virtual environment to manage your dependencies. Below is an example of setting up and activating a virtual environment.

python3 -m venv .venv
source .venv/bin/activate
pip install uvicorn

Install the SDK

The F5 AI Gateway Python SDK is designed to handle requests and responses between AI Gateway and your application, allowing you to focus on developing your processor logic.

pip install git+https://github.com/nginxinc/f5-ai-gateway-sdk-py

You can also install using one of the .whl files that are available from the releases page.

pip install f5_ai_gateway_sdk-<version>-py3-none-any.whl

Create a basic processor which annotates a request

In this tutorial we create a simple processor that inspects the prompt and response for any mentions of a ‘cat’ and tags it accordingly. Tags can be used in the AI Gateway configuration to route to a specific upstream LLM or run a particular set of processors.

Create a file called cat_classifier.py with the following contents.

from f5_ai_gateway_sdk.parameters import Parameters
from f5_ai_gateway_sdk.processor import Processor
from f5_ai_gateway_sdk.processor_routes import ProcessorRoutes
from f5_ai_gateway_sdk.request_input import Message, MessageRole
from f5_ai_gateway_sdk.result import Result, Reject, RejectCode
from f5_ai_gateway_sdk.signature import BOTH_SIGNATURE, INPUT_ONLY_SIGNATURE
from f5_ai_gateway_sdk.tags import Tags
from f5_ai_gateway_sdk.type_hints import Metadata
from starlette.applications import Starlette


class CatClassifierProcessor(Processor):
    """
    A simple processor which adds a tag when a cat is found in the prompt or response
    """

    def __init__(self):
        super().__init__(
            name="cat-classifier",
            version="v1",
            namespace="tutorial",
            signature=INPUT_ONLY_SIGNATURE,
        )

    def process_input(self, prompt, metadata, parameters, request):
        my_tags = Tags()
        cat_found = any("cat" in message.content for message in prompt.messages)

        result = Metadata({"cat_found": cat_found})

        if parameters.annotate and cat_found:
            my_tags.add_tag("animals-found", "cat")

        return Result(processor_result=result, tags=my_tags)


app = Starlette(
    routes=ProcessorRoutes([CatClassifierProcessor()]),
)

Note

This example contains all of the imports that will be needed for this tutorial so your editor may warn about unused imports.

Run the processor locally

Run the processor locally using uvicorn; this will start a server on port 9999. Ensure that you run this command from the directory where your cat_classifier.py file is located.

python -m uvicorn cat_classifier:app --host 127.0.0.1 --port 9999 --reload

The --reload flag will automatically restart the server when you make changes to your code.

We can check if the server is running by sending a curl request to the signature endpoint of the processor.

Request

curl -i http://localhost:9999/api/v1/signature/tutorial/cat-classifier

Response

{
    "fields": [
        {
            "type": "input.messages",
            "required": true
        }
    ],
    "parameters": {
        "description": "Default empty parameters class for processors that do not require parameters.\nThis class should not be inherited from.",
        "properties": {
            "annotate": {
                "default": true,
                "description": "Whether the processor can annotate the input with tags.",
                "title": "Annotate",
                "type": "boolean"
            },
            "modify": {
                "default": false,
                "description": "Whether the processor can modify the input.",
                "title": "Modify",
                "type": "boolean"
            },
            "reject": {
                "default": false,
                "description": "Whether the processor can reject requests.",
                "title": "Reject",
                "type": "boolean"
            }
        },
        "title": "Default Parameters",
        "type": "object"
    }
}

In the above endpoint, tutorial is the namespace of your processor, and cat-classifier is the name of your processor.

The response should be similar to the following example and contains information that is used by AI Gateway to send correctly formatted requests to the processor:

Request

curl -i -X POST http://localhost:9999/api/v1/execute/tutorial/cat-classifier \
    -H "Content-Type: multipart/form-data" \
    --form 'input.parameters={};type=application/json' \
    --form 'metadata={};type=application/json' \
    --form 'input.messages={
    "messages": [
        {
        "content": "Curious cats gracefully roam through cozy homes, chasing shadows and basking in warm sunlit windows."
        }
    ]
    };type=application/json'

Response

HTTP/1.1 200 OK
date: Fri, 22 Nov 2024 16:33:10 GMT
server: uvicorn
content-type: multipart/form-data;charset=utf-8;boundary="3zWBuRUjyupW5lUbCEITz3riZBm2dPztxregsT4fMZJBHjmoEDYUq5fqAXGCKHr6"
Transfer-Encoding: chunked

--3zWBuRUjyupW5lUbCEITz3riZBm2dPztxregsT4fMZJBHjmoEDYUq5fqAXGCKHr6
Content-Disposition: form-data; name="metadata"
Content-Type: application/json

{"processor_id": "example:cat-classifier", "processor_version": "v1", "processor_result": {"cat_found": true}, "tags": {"animals-found": ["cat"]}}
--3zWBuRUjyupW5lUbCEITz3riZBm2dPztxregsT4fMZJBHjmoEDYUq5fqAXGCKHr6--

Use the processor with AI Gateway

To configure this processor in AI Gateway add the following to your aigw.yml.

processors:
  - name: cat-classifier
    type: external
    config:
      endpoint: "localhost:9999"
      namespace: tutorial
      version: 1

See the configure processors page for more details.

How to reject a request

Processors can be configured on a per-request basis by setting the params object in either the processors or steps sections of the AI Gateway configuration file.

Let’s use our default parameters to reject any requests which don’t contain cats. Update the end of the process function in cat_classifier.py to return a Reject object if the condition matches:

        if parameters.reject and not cat_found:
            return Reject(
                code=RejectCode.POLICY_VIOLATION, detail="No cat found in request"
            )

        return Result(processor_result=result, tags=my_tags)

We can now make a request to the processor with the reject parameter set to true:

Request

curl -i -X POST http://localhost:9999/api/v1/execute/tutorial/cat-classifier \
    -H "Content-Type: multipart/form-data" \
    --form 'input.parameters={"reject":true};type=application/json' \
    --form 'metadata={};type=application/json' \
    --form 'input.messages={
    "messages": [
        {
        "content": "Curious dogs gracefully roam through cozy homes, chasing shadows and basking in warm sunlit windows."
        }
    ]
    };type=application/json'

Response

HTTP/1.1 200 OK
date: Fri, 16 May 2025 10:41:17 GMT
server: uvicorn
content-type: multipart/form-data;charset=utf-8;boundary="zPn39CWJ76o4UJ3xpp54OkyGVh0zqpnjB9138jOjWLRZjyxlicTwIHgggTqWAZes"
Transfer-Encoding: chunked

--zPn39CWJ76o4UJ3xpp54OkyGVh0zqpnjB9138jOjWLRZjyxlicTwIHgggTqWAZes
Content-Disposition: form-data; name="reject"
Content-Type: application/json

{"code":"AIGW_POLICY_VIOLATION","detail":"No cat found in request"}
--zPn39CWJ76o4UJ3xpp54OkyGVh0zqpnjB9138jOjWLRZjyxlicTwIHgggTqWAZes
Content-Disposition: form-data; name="metadata"
Content-Type: application/json

{"processor_id": "tutorial:cat-classifier", "processor_version": "v1"}
--zPn39CWJ76o4UJ3xpp54OkyGVh0zqpnjB9138jOjWLRZjyxlicTwIHgggTqWAZes--

Modify a request

Processors also have the ability to make modifications to the prompt or response before it is routed to the next stage. Make the following changes to add a system message to the request if the processor is used in an input stage.

Additional parameters can be added to the processor by defining a class that extends f5_ai_gateway_sdk.parameters.Parameters. Add the following after the imports section at the top of the cat_classifier.py file:

class CatClassifierParameters(Parameters):
    enforce_cat_message: str = "Remember to talk about cats in your response"

We then need to update the __init__ function for the processor to specify this class.

    def __init__(self):
        super().__init__(
            name="cat-classifier",
            version="v1",
            namespace="tutorial",
            signature=INPUT_ONLY_SIGNATURE,
            parameters_class=CatClassifierParameters
        )

If we make a request to the signature endpoint, we can see the newly added parameter.

Request

curl -i http://localhost:9999/api/v1/signature/tutorial/cat-classifier

Response

{
    "fields": [
        {
            "type": "input.messages",
            "required": true
        }
    ],
    "parameters": {
        "properties": {
            "annotate": {
                "default": true,
                "description": "Whether the processor can annotate the input with tags.",
                "title": "Annotate",
                "type": "boolean"
            },
            "modify": {
                "default": false,
                "description": "Whether the processor can modify the input.",
                "title": "Modify",
                "type": "boolean"
            },
            "reject": {
                "default": false,
                "description": "Whether the processor can reject requests.",
                "title": "Reject",
                "type": "boolean"
            },
            "enforce_cat_message": {
                "default": "Remember to talk about cats in your response",
                "title": "Enforce Cat Message",
                "type": "string"
            }
        },
        "title": "CatClassifierParameters",
        "type": "object"
    }
}

Note

Due to the CatClassifierParameters used in this example being a subclass of Parameters, it automatically has access to the common parameters of annotate, reject, and modify.

Add the following to the process function before the return statement, and include modified_prompt=prompt in the Result

        if parameters.modify:
            prompt.messages.append(
                Message(
                    content=parameters.enforce_cat_message,
                    role=MessageRole.SYSTEM,
                )
            )

        return Result(processor_result=result, tags=my_tags, modified_prompt=prompt)

Verify that the system message has been added to the request.

Request

curl -i -X POST http://localhost:9999/api/v1/execute/tutorial/cat-classifier \
    -H "Content-Type: multipart/form-data" \
    --form 'input.parameters={"modify":true,"enforce_cat_message":"Remember to talk about how cute cats are in your response"};type=application/json' \
    --form 'metadata={};type=application/json' \
    --form 'input.messages={
    "messages": [
        {
        "content": "Curious dogs gracefully roam through cozy homes, chasing shadows and basking in warm sunlit windows."
        }
    ]
    };type=application/json'

Response

HTTP/1.1 200 OK
date: Thu, 12 Dec 2024 16:03:26 GMT
server: uvicorn
content-type: multipart/form-data;charset=utf-8;boundary="iBDsmhfPKiowYNQTb9bbfPT76c26q3y8Zj9o7qFHrgDIsIlLqDQi5E5bQaqjIkLy"
Transfer-Encoding: chunked

--iBDsmhfPKiowYNQTb9bbfPT76c26q3y8Zj9o7qFHrgDIsIlLqDQi5E5bQaqjIkLy
Content-Disposition: form-data; name="input.messages"
Content-Type: text/plain;charset=utf-8

{"messages":[{"content":"Curious dogs gracefully roam through cozy homes, chasing shadows and basking in warm sunlit windows.","role":"user"},{"content":"Remember to talk about how cute cats are in your response","role":"system"}]}
--iBDsmhfPKiowYNQTb9bbfPT76c26q3y8Zj9o7qFHrgDIsIlLqDQi5E5bQaqjIkLy
Content-Disposition: form-data; name="metadata"
Content-Type: application/json

{"processor_id": "tutorial:cat-classifier", "processor_version": "v1", "processor_result": {"cat_found": false}}
--iBDsmhfPKiowYNQTb9bbfPT76c26q3y8Zj9o7qFHrgDIsIlLqDQi5E5bQaqjIkLy--

Act on the response from the upstream LLM

Until now our processor used the process_input function to perform operations on requests before they reach the upstream LLM. We can also perform similar actions on the LLM response before it is returned to the client.

Let’s update our __init__ function to specify that we support processing the response by changing the signature.

    def __init__(self):
        super().__init__(
            name="cat-classifier",
            version="v1",
            namespace="tutorial",
            signature=BOTH_SIGNATURE,
            parameters_class=CatClassifierParameters,
        )

After making this change the processor starts reporting an error.

TypeError: Cannot create concrete class CatClassifierProcessor. Provided Signature supports response but 'process_response' is not implemented.

Let’s implement a basic process_response function to satisfy this requirement. LLMs do not always respond to instructions in the way we expect, so we can reject any responses which ignore the instruction we added in the Modify a request section to mention how cute cats are and does nothing if reject: true is not set.

    def process_response(self, prompt, response, metadata, parameters, request):
        cute_found = any(
            "cute" in choice.message.content for choice in response.choices
        )

        if parameters.reject and not cute_found:
            return Reject(
                code=RejectCode.POLICY_VIOLATION,
                detail="Upstream did not follow instructions",
            )

        return Result()

In this request instead of using input.parameters and input.messages we provide response.parameters and response.choices. The format of input.parameters and response.parameters are the same. However while input.messages contains a list of message objects, response.choices contain a list of choice objects which each contain a message.

Request

curl -i -X POST http://localhost:9999/api/v1/execute/tutorial/cat-classifier \
    -H "Content-Type: multipart/form-data" \
    --form 'response.parameters={"reject":true};type=application/json' \
    --form 'metadata={};type=application/json' \
    --form 'response.choices={
    "choices": [
      {
        "message": {
          "content": "Curious dogs gracefully roam through cozy homes, chasing shadows and basking in warm sunlit windows."
        }
      }
    ]
    };type=application/json'

Response

HTTP/1.1 200 OK
date: Fri, 16 May 2025 14:29:38 GMT
server: uvicorn
content-type: multipart/form-data;charset=utf-8;boundary="Z5pDkuwlewP1uy1QocdQual50l156Y2ceOlM3en5tMlG3NNvisCVlF2loDmosSXw"
Transfer-Encoding: chunked

--Z5pDkuwlewP1uy1QocdQual50l156Y2ceOlM3en5tMlG3NNvisCVlF2loDmosSXw
Content-Disposition: form-data; name="reject"
Content-Type: application/json

{"code":"AIGW_POLICY_VIOLATION","detail":"Upstream did not follow instructions"}
--Z5pDkuwlewP1uy1QocdQual50l156Y2ceOlM3en5tMlG3NNvisCVlF2loDmosSXw
Content-Disposition: form-data; name="metadata"
Content-Type: application/json

{"processor_id": "tutorial:cat-classifier", "processor_version": "v1"}
--Z5pDkuwlewP1uy1QocdQual50l156Y2ceOlM3en5tMlG3NNvisCVlF2loDmosSXw--

Completed tutorial processor

from f5_ai_gateway_sdk.parameters import Parameters
from f5_ai_gateway_sdk.processor import Processor
from f5_ai_gateway_sdk.processor_routes import ProcessorRoutes
from f5_ai_gateway_sdk.request_input import Message, MessageRole
from f5_ai_gateway_sdk.result import Result, Reject, RejectCode
from f5_ai_gateway_sdk.signature import BOTH_SIGNATURE, INPUT_ONLY_SIGNATURE
from f5_ai_gateway_sdk.tags import Tags
from f5_ai_gateway_sdk.type_hints import Metadata
from starlette.applications import Starlette


class CatClassifierParameters(Parameters):
    enforce_cat_message: str = "Remember to talk about cats in your response"


class CatClassifierProcessor(Processor):
    """
    A simple processor which adds a tag when a cat is found in the prompt or response
    """

    def __init__(self):
        super().__init__(
            name="cat-classifier",
            version="v1",
            namespace="tutorial",
            signature=BOTH_SIGNATURE,
            parameters_class=CatClassifierParameters,
        )

    def process_input(self, prompt, metadata, parameters, request):
        my_tags = Tags()
        cat_found = any("cat" in message.content for message in prompt.messages)

        result = Metadata({"cat_found": cat_found})

        if parameters.annotate and cat_found:
            my_tags.add_tag("animals-found", "cat")

        if parameters.reject and not cat_found:
            return Reject(
                code=RejectCode.POLICY_VIOLATION, detail="No cat found in request"
            )

        if parameters.modify:
            prompt.messages.append(
                Message(
                    content=parameters.enforce_cat_message,
                    role=MessageRole.SYSTEM,
                )
            )

        return Result(processor_result=result, tags=my_tags, modified_prompt=prompt)

    def process_response(self, prompt, response, metadata, parameters, request):
        cute_found = any(
            "cute" in choice.message.content for choice in response.choices
        )

        if parameters.reject and not cute_found:
            return Reject(
                code=RejectCode.POLICY_VIOLATION,
                detail="Upstream did not follow instructions",
            )

        return Result()


app = Starlette(
    routes=ProcessorRoutes([CatClassifierProcessor()]),
)