diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 59f765e..79385c0 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -19,7 +19,7 @@ jobs: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - - name: Build and push Docker images + - name: Build and push the base Docker image uses: docker/build-push-action@v4.1.0 with: context: . @@ -28,3 +28,12 @@ jobs: cache-from: type=gha cache-to: type=gha,mode=max tags: ghcr.io/khaosresearch/eidos:latest,ghcr.io/khaosresearch/eidos:${{ github.event.release.tag_name }} + - name: Build and push the lambda Docker image + uses: docker/build-push-action@v4.1.0 + with: + context: . + file: ./Dockerfile.lambda + push: true + cache-from: type=gha + cache-to: type=gha,mode=max + tags: ghcr.io/khaosresearch/eidos-lambda:latest,ghcr.io/khaosresearch/eidos-lambda:${{ github.event.release.tag_name }} diff --git a/Dockerfile b/Dockerfile index 88d4624..df97f71 100644 --- a/Dockerfile +++ b/Dockerfile @@ -14,7 +14,7 @@ ENV EIDOS_ENV production ENV EIDOS_FUNCTIONS_FOLDER /functions -RUN pip install "." +RUN pip install --no-cache-dir "." EXPOSE 80 diff --git a/Dockerfile.lambda b/Dockerfile.lambda new file mode 100644 index 0000000..c97b424 --- /dev/null +++ b/Dockerfile.lambda @@ -0,0 +1,22 @@ +FROM amazon/aws-lambda-python:3.10 + +# When sending request from the UI it crashes as described in this issue, this is a patch for it +# https://github.com/aws/aws-lambda-runtime-interface-emulator/issues/97#issuecomment-1707171018 +RUN curl -Lo /usr/local/bin/aws-lambda-rie https://github.com/aws/aws-lambda-runtime-interface-emulator/releases/download/v1.10/aws-lambda-rie \ + && chmod +x /usr/local/bin/aws-lambda-rie + + +COPY README.md /code/README.md +COPY src /code/src +COPY pyproject.toml /code/pyproject.toml +COPY ./functions /functions + +ENV EIDOS_ENV production + +ENV EIDOS_FUNCTIONS_FOLDER=/functions + +RUN pip install --no-cache-dir "/code" + +EXPOSE 8080 + +CMD [ "eidos.lambda.lambda_handler" ] \ No newline at end of file diff --git a/README.md b/README.md index b82615e..48a5b2d 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,10 @@ # eidos: Validation and execution of AI functions +[![Linter: Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff) +![Python Version from PEP 621 TOML](https://img.shields.io/python/required-version-toml?tomlFilePath=https%3A%2F%2Fraw.githubusercontent.com%2FKhaosResearch%2Feidos%2Fmaster%2Fpyproject.toml) +![GitHub Release](https://img.shields.io/github/v/release/KhaosResearch/eidos) +![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/KhaosResearch/eidos/test.yaml?label=CI) + _eidos_ is an API for validating and executing AI functions. It aims to be a generic API to serve as a common interface to allow execution of functions by LLMs. ![Usage diagram of eidos](assets/eidos.png) @@ -20,7 +25,9 @@ Or directly from GitHub: python -m pip install "eidos @ git+ssh://git@github.com/KhaosResearch/eidos.git" ``` -## Run +## Deployment + +* Development Run the API with the following command: @@ -30,11 +37,13 @@ uvicorn eidos.api:app --host 0.0.0.0 --port 8090 --reload You can override the default configuration by setting [environment variables](src/eidos/settings.py). +* Docker + Alternatively, you can use the provided [Dockerfile](Dockerfile) to build a Docker image and run the API in a container: ```bash docker build -t eidos-server:latest . -docker run -v $(pwd)/functions:/functions -p 8090:80 eidos-server:latest +docker run --rm -v $(pwd)/functions:/functions -p 8090:80 eidos-server:latest ``` Example: @@ -43,7 +52,28 @@ Example: curl -X POST -H "Content-Type: application/json" -d '{"who": "me"}' http://localhost:8090/api/v1/execution/salute ``` -To deploy the container in Kubernetes, a reference deployment is available and documented at [deployments](deployments/). +* Kubernetes + +To deploy the container in Kubernetes, a reference deployment is available and documented at [manifests](manifests/). + +* Serverless in AWS +Another docker image to deploy serverless in AWS Lambda is provided in [Dockerfile.lambda](Dockerfile.lambda). The image is based on the official AWS Lambda Python 3.11 image. For extending this image the process is the same as the main image. + +```bash +docker build -t eidos-lambda -f Dockerfile.lambda . +``` + +Run the container locally with the following command or deploy in AWS Lambda as a docker container image: + +```bash +docker run --rm -p 8091:8080 eidos-lambda +``` + +Invoke the function for local testing with sample query + +```bash +curl -XPOST "http://localhost:8091/2015-03-31/functions/function/invocations" -d '{"command": "EXECUTE", "parameters": {"function": "salute", "args": {"who": "me, I am executing serverless"}}}' +``` ## Testing diff --git a/deployments/README.md b/manifests/README.md similarity index 100% rename from deployments/README.md rename to manifests/README.md diff --git a/deployments/eidos-deployment.yaml b/manifests/eidos-deployment.yaml similarity index 100% rename from deployments/eidos-deployment.yaml rename to manifests/eidos-deployment.yaml diff --git a/src/eidos/execute.py b/src/eidos/execute.py index 7e6f35f..ffe51d9 100644 --- a/src/eidos/execute.py +++ b/src/eidos/execute.py @@ -94,6 +94,7 @@ def execute(function_name: str, arguments: dict | None) -> dict[str, Any]: try: function_definition = json_load(file_path) except FileNotFoundError: + log.error("Error: function module not found.", function=function_name, file_path=file_path) raise FileNotFoundError("Error: function module not found.") # Validate input arguments against the function's schema. diff --git a/src/eidos/lambda.py b/src/eidos/lambda.py new file mode 100644 index 0000000..c7bdb8a --- /dev/null +++ b/src/eidos/lambda.py @@ -0,0 +1,93 @@ +from enum import Enum +from typing import Any + +import structlog + +from eidos.execute import ( + execute, + get_function_schema, + get_openai_function_definition, + list_functions_names, + list_functions_openai, +) + +log = structlog.get_logger("eidos.lambda") + + +class ValidationCommands(Enum): + """Enum to hold the different validation commands from eidos.""" + + LIST = "LIST" + LIST_NAMES = "LIST_NAMES" + GET_DEFINITION = "GET_DEFINITION" + GET_SCHEMA = "GET_SCHEMA" + EXECUTE = "EXECUTE" + + def __str__(self): + return self.value + + def __repr__(self): + return self.value + + +def lambda_handler(event: dict[str, Any], context: dict[str, Any]): + try: + log.info("Processing event", command=event["command"]) + command = event["command"] + except KeyError: + raise ValueError( + f"There is a required field 'command' with the function to execute. " + f"Possible values: {ValidationCommands.__members__}" + ) + + try: + validation_function = ValidationCommands[command] + except KeyError: + raise ValueError( + f"Unknown function: {event['command']}. " + f"Possible values: {ValidationCommands.__members__}" + ) + + match validation_function: + case ValidationCommands.LIST: + return list_functions_openai() + case ValidationCommands.LIST_NAMES: + return list_functions_names() + case ValidationCommands.GET_DEFINITION: + if "function" in event.get("parameters", {}): + function = event["parameters"]["function"] + return get_openai_function_definition(function) + else: + return { + "statusCode": 400, + "body": "Missing function. Provide as parameters.function", + } + case ValidationCommands.GET_SCHEMA: + if "function" in event.get("parameters", {}): + function = event["parameters"]["function"] + return get_function_schema(function) + else: + return { + "statusCode": 400, + "body": "Missing function. Provide as parameters.function", + } + case ValidationCommands.EXECUTE: + if "function" in event.get("parameters", {}): + function = event["parameters"]["function"] + else: + return { + "statusCode": 400, + "body": "Missing function. Provide as parameters.function", + } + + args = event["parameters"].get("args", {}) # Default to empty parameters + log.info("Executing function ", function=function, arguments=args) + + result = execute(function, args) + + return result + case _: + raise ValueError( + f"Unknown function: {event['command']}. " + f"Possible values: {ValidationCommands.__members__}" + )