diff --git a/.github/workflows/publish-images.yml b/.github/workflows/publish-images.yml new file mode 100644 index 0000000..04e8fb9 --- /dev/null +++ b/.github/workflows/publish-images.yml @@ -0,0 +1,54 @@ +name: Build Docker Images + +on: + workflow_run: + workflows: + - Invariant gateway testing CI + types: + - completed + branches: + - main + - v* + +jobs: + build-explorer-local-images: + runs-on: ubuntu-latest + # check that this is a tag or the 'main' branch but not a pull request + # if: ${{ github.event.workflow_run.conclusion == 'success' && (github.ref_type == 'tag' || github.ref == 'refs/heads/main') }} + + steps: + - name: Checkout Code + uses: actions/checkout@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + + - name: Login to GitHub Container Registry + run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin + + # based on whether this is a version tag or a branch, we will set the tags for the pushed images + - name: Set Docker Tags + id: set-tags + run: | + if [[ "${{ github.event.workflow_run.head_branch }}" == "main" ]]; then + echo "tags=ghcr.io/${{ github.repository }}/gateway:latest" >> $GITHUB_ENV + # starts with v* and only contains numbers and dots + elif [[ "${{ github.event.workflow_run.head_branch }}" =~ ^v[0-9.]+$ ]]; then + # get tag name from the event + version=${{ github.event.workflow_run.head_branch }} + # remove the refs/tags/ prefix + echo "version string is $version" + echo "tags=ghcr.io/${{ github.repository }}/gateway:latest,ghcr.io/${{ github.repository }}/gateway:${version}" >> $GITHUB_ENV + else + echo "Failed to determine the tags for the Docker images for branch ${{ github.event.workflow_run.head_branch }}" + exit 1 + fi + + - name: build gateway image + uses: docker/build-push-action@v5 + with: + context: ./gateway + file: ./gateway/Dockerfile.gateway + platforms: linux/amd64 + push: true + tags: ${{ env.tags }} diff --git a/README.md b/README.md index 36fd212..cf80126 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ **LLM proxy to observe and debug what your AI agents are doing.** -[Documentation](https://explorer.invariantlabs.ai/docs/gateway) | [Quickstart for Users](#quickstart-for-users) | [Quickstart for Developers](#quickstart-for-developers) +[Documentation](https://explorer.invariantlabs.ai/docs/gateway) | [Quickstart for Users](#quickstart-for-users) | [Quickstart for Developers](#quickstart-for-developers) | [Run Locally](#run-the-gateway-locally) @@ -38,6 +38,7 @@ To add Gateway to your agentic system, follow one of the integration guides belo ## **Integration Guides** ### **🔹 OpenAI Integration** + Gateway supports the OpenAI Chat Completions API (`/v1/chat/completions` endpoint). 1. Follow [these steps](https://platform.openai.com/docs/quickstart#create-and-export-an-api-key) to obtain an OpenAI API key. @@ -62,6 +63,7 @@ Gateway supports the OpenAI Chat Completions API (`/v1/chat/completions` endpoin > **Note:** Do not include the curly braces `{}`. If the dataset does not exist in Invariant Explorer, it will be created before adding traces. ### **🔹 Anthropic Integration** + Gateway supports the Anthropic Messages API (`/v1/messages` endpoint). 1. Follow [these steps](https://docs.anthropic.com/en/docs/initial-setup#set-your-api-key) to obtain an Anthropic API key. @@ -84,6 +86,7 @@ Gateway supports the Anthropic Messages API (`/v1/messages` endpoint). > **Note:** Do not include the curly braces `{}`. If the dataset does not exist in Invariant Explorer, it will be created before adding traces. ### **🔹 Gemini Integration** + Gateway supports the Gemini `generateContent` and `streamGenerateContent` methods. 1. Follow [these steps](https://ai.google.dev/gemini-api/docs/api-key) to obtain a Gemini API key. @@ -91,7 +94,7 @@ Gateway supports the Gemini `generateContent` and `streamGenerateContent` method ```python import os - + from google import genai client = genai.Client( @@ -249,14 +252,34 @@ This setup ensures that SWE-agent works seamlessly with Invariant Gateway, maint You can also operate your own instance of the Gateway, to ensure privacy and security. -First, clone this repository. To start the Invariant Gateway, run: +To run Gateway locally, you have two options: + +### 1. Run gateway from the repository + +1. Clone this repository. + +2. To start the Invariant Gateway, then run the following commands. Note that you need to have Docker installed. ```bash +cd invariant-gateway bash run.sh build && bash run.sh up ``` This will launch Gateway at [http://localhost:8005/api/v1/gateway/](http://localhost:8005/api/v1/gateway/docs/). +### 2. Run the Gateway using the published Docker image + +You can also run the Gateway using the published Docker image. This is a good option if you want to run the Gateway in a cloud environment. + +```bash +# pull the latest image +docker pull --platform linux/amd64 ghcr.io/invariantlabs-ai/invariant-gateway/gateway:latest +# run Gateway on localhost:8005 +docker run -p 8005:8005 -e PORT=8005 --platform linux/amd64 ghcr.io/invariantlabs-ai/invariant-gateway/gateway:latest +``` + +This will launch Gateway at [http://localhost:8002/api/v1/gateway/](http://localhost:8002/api/v1/gateway/docs/). + ### **Set Up an Invariant API Key** 1. Follow the instructions [here](https://explorer.invariantlabs.ai/docs/explorer/api/client-setup/) to obtain an API key. This allows the gateway to push traces to [Invariant Explorer](https://explorer.invariantlabs.ai). diff --git a/gateway/.env b/gateway/.env new file mode 100644 index 0000000..78d3391 --- /dev/null +++ b/gateway/.env @@ -0,0 +1,4 @@ +POSTGRES_USER=postgres + POSTGRES_PASSWORD=postgres + POSTGRES_DB=invariantmonitor + POSTGRES_HOST=database \ No newline at end of file diff --git a/gateway/Dockerfile.gateway b/gateway/Dockerfile.gateway index 992aaad..431bfba 100644 --- a/gateway/Dockerfile.gateway +++ b/gateway/Dockerfile.gateway @@ -8,5 +8,7 @@ COPY . /srv/gateway # Ensure run.sh is executable RUN chmod +x /srv/gateway/run.sh +WORKDIR /srv/gateway -ENTRYPOINT ./run.sh \ No newline at end of file +# Run the application +CMD ["./run.sh"] \ No newline at end of file diff --git a/gateway/common/config_manager.py b/gateway/common/config_manager.py index 09c5904..7bdb3e2 100644 --- a/gateway/common/config_manager.py +++ b/gateway/common/config_manager.py @@ -20,7 +20,7 @@ class GatewayConfig: policies_file = os.getenv("POLICIES_FILE_PATH", "") if not policies_file: - print("Warning: POLICIES_FILE_PATH is not set. Using empty policies.") + print("[warning: POLICIES_FILE_PATH is not set. Using empty policies]") return "" try: diff --git a/gateway/run.sh b/gateway/run.sh index 0663eea..ebc4b1b 100755 --- a/gateway/run.sh +++ b/gateway/run.sh @@ -9,8 +9,13 @@ if [ $? -ne 0 ]; then exit 1 fi +# check if PORT environment variable is set +UVICORN_PORT=${PORT:-8000} + +# using 'exec' belows ensures that signals like SIGTERM are passed to the child process +# and not the shell script itself (important when running in a container) if [ "$DEV_MODE" = "true" ]; then - uvicorn serve:app --host 0.0.0.0 --port 8000 --reload + exec uvicorn serve:app --host 0.0.0.0 --port $UVICORN_PORT --reload else - uvicorn serve:app --host 0.0.0.0 --port 8000 + exec uvicorn serve:app --host 0.0.0.0 --port $UVICORN_PORT fi \ No newline at end of file diff --git a/gateway/serve.py b/gateway/serve.py index 5fcdf16..e3683f9 100644 --- a/gateway/serve.py +++ b/gateway/serve.py @@ -32,5 +32,14 @@ router.include_router(gemini_gateway, prefix="/gateway", tags=["gemini_gateway"] app.include_router(router) +# on / redirect to https://explorer.invariantlabs.ai/docs/gateway +@app.get("/", include_in_schema=False) +async def redirect_to_explorer(): + """Redirect to the explorer""" + return fastapi.responses.RedirectResponse( + url="https://explorer.invariantlabs.ai/docs/gateway" + ) + + if __name__ == "__main__": uvicorn.run(app, host="0.0.0.0", port=8000) diff --git a/gateway/validate_config.py b/gateway/validate_config.py index 4d13c89..99a24bb 100644 --- a/gateway/validate_config.py +++ b/gateway/validate_config.py @@ -6,7 +6,7 @@ from common.config_manager import GatewayConfigManager try: _ = GatewayConfigManager.get_config() - print("GatewayConfig validated successfully.") + print("[gateway config validated successfully]") sys.exit(0) except Exception as e: print(f"Error loading GatewayConfig error: {e}")