Skip to content

Commit

Permalink
Merge branch 'dev' into ntindle/open-2093-integration-test-build-an-a…
Browse files Browse the repository at this point in the history
…gent-with-a-few-blocks-including
  • Loading branch information
ntindle authored Dec 3, 2024
2 parents cc7ef5a + 43bd5c8 commit 34651ff
Show file tree
Hide file tree
Showing 15 changed files with 170 additions and 43 deletions.
2 changes: 2 additions & 0 deletions .github/workflows/platform-backend-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,13 @@ on:
paths:
- ".github/workflows/platform-backend-ci.yml"
- "autogpt_platform/backend/**"
- "autogpt_platform/autogpt_libs/**"
pull_request:
branches: [master, dev, release-*]
paths:
- ".github/workflows/platform-backend-ci.yml"
- "autogpt_platform/backend/**"
- "autogpt_platform/autogpt_libs/**"
merge_group:

concurrency:
Expand Down
7 changes: 6 additions & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,11 @@ repos:
files: ^autogpt_platform/autogpt_libs/
args: [--fix]

- id: ruff-format
name: Format (Ruff) - AutoGPT Platform - Libs
alias: ruff-lint-platform-libs
files: ^autogpt_platform/autogpt_libs/

- repo: local
# isort needs the context of which packages are installed to function, so we
# can't use a vendored isort pre-commit hook (which runs in its own isolated venv).
Expand Down Expand Up @@ -140,7 +145,7 @@ repos:
# everything in .gitignore, so it works fine without any config or arguments.
hooks:
- id: black
name: Lint (Black)
name: Format (Black)

- repo: https://github.com/PyCQA/flake8
rev: 7.0.0
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ def feature_flag(
"""

def decorator(
func: Callable[P, Union[T, Awaitable[T]]]
func: Callable[P, Union[T, Awaitable[T]]],
) -> Callable[P, Union[T, Awaitable[T]]]:
@wraps(func)
async def async_wrapper(*args: P.args, **kwargs: P.kwargs) -> T:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@


class LoggingConfig(BaseSettings):

level: str = Field(
default="INFO",
description="Logging level",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,10 @@
),
("", ""),
("hello", "hello"),
("hello\x1B[31m world", "hello world"),
("\x1B[36mHello,\x1B[32m World!", "Hello, World!"),
("hello\x1b[31m world", "hello world"),
("\x1b[36mHello,\x1b[32m World!", "Hello, World!"),
(
"\x1B[1m\x1B[31mError:\x1B[0m\x1B[31m file not found",
"\x1b[1m\x1b[31mError:\x1b[0m\x1b[31m file not found",
"Error: file not found",
),
],
Expand Down
Empty file.
31 changes: 31 additions & 0 deletions autogpt_platform/autogpt_libs/autogpt_libs/rate_limit/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
from pydantic import Field
from pydantic_settings import BaseSettings, SettingsConfigDict


class RateLimitSettings(BaseSettings):
redis_host: str = Field(
default="redis://localhost:6379",
description="Redis host",
validation_alias="REDIS_HOST",
)

redis_port: str = Field(
default="6379", description="Redis port", validation_alias="REDIS_PORT"
)

redis_password: str = Field(
default="password",
description="Redis password",
validation_alias="REDIS_PASSWORD",
)

requests_per_minute: int = Field(
default=60,
description="Maximum number of requests allowed per minute per API key",
validation_alias="RATE_LIMIT_REQUESTS_PER_MINUTE",
)

model_config = SettingsConfigDict(case_sensitive=True, extra="ignore")


RATE_LIMIT_SETTINGS = RateLimitSettings()
51 changes: 51 additions & 0 deletions autogpt_platform/autogpt_libs/autogpt_libs/rate_limit/limiter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import time
from typing import Tuple

from redis import Redis

from .config import RATE_LIMIT_SETTINGS


class RateLimiter:
def __init__(
self,
redis_host: str = RATE_LIMIT_SETTINGS.redis_host,
redis_port: str = RATE_LIMIT_SETTINGS.redis_port,
redis_password: str = RATE_LIMIT_SETTINGS.redis_password,
requests_per_minute: int = RATE_LIMIT_SETTINGS.requests_per_minute,
):
self.redis = Redis(
host=redis_host,
port=int(redis_port),
password=redis_password,
decode_responses=True,
)
self.window = 60
self.max_requests = requests_per_minute

async def check_rate_limit(self, api_key_id: str) -> Tuple[bool, int, int]:
"""
Check if request is within rate limits.
Args:
api_key_id: The API key identifier to check
Returns:
Tuple of (is_allowed, remaining_requests, reset_time)
"""
now = time.time()
window_start = now - self.window
key = f"ratelimit:{api_key_id}:1min"

pipe = self.redis.pipeline()
pipe.zremrangebyscore(key, 0, window_start)
pipe.zadd(key, {str(now): now})
pipe.zcount(key, window_start, now)
pipe.expire(key, self.window)

_, _, request_count, _ = pipe.execute()

remaining = max(0, self.max_requests - request_count)
reset_time = int(now + self.window)

return request_count <= self.max_requests, remaining, reset_time
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
from fastapi import HTTPException, Request
from starlette.middleware.base import RequestResponseEndpoint

from .limiter import RateLimiter


async def rate_limit_middleware(request: Request, call_next: RequestResponseEndpoint):
"""FastAPI middleware for rate limiting API requests."""
limiter = RateLimiter()

if not request.url.path.startswith("/api"):
return await call_next(request)

api_key = request.headers.get("Authorization")
if not api_key:
return await call_next(request)

api_key = api_key.replace("Bearer ", "")

is_allowed, remaining, reset_time = await limiter.check_rate_limit(api_key)

if not is_allowed:
raise HTTPException(
status_code=429, detail="Rate limit exceeded. Please try again later."
)

response = await call_next(request)
response.headers["X-RateLimit-Limit"] = str(limiter.max_requests)
response.headers["X-RateLimit-Remaining"] = str(remaining)
response.headers["X-RateLimit-Reset"] = str(reset_time)

return response
40 changes: 20 additions & 20 deletions autogpt_platform/autogpt_libs/poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion autogpt_platform/autogpt_libs/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ supabase = "^2.10.0"

[tool.poetry.group.dev.dependencies]
redis = "^5.2.0"
ruff = "^0.8.0"
ruff = "^0.8.1"

[build-system]
requires = ["poetry-core"]
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import logging
import time
from enum import Enum
from typing import Any, Dict
from typing import Any

import httpx

Expand Down Expand Up @@ -64,16 +64,16 @@ def __init__(self):
},
)

def _get_headers(self, api_key: str) -> Dict[str, str]:
def _get_headers(self, api_key: str) -> dict[str, str]:
"""Get headers for FAL API requests."""
return {
"Authorization": f"Key {api_key}",
"Content-Type": "application/json",
}

def _submit_request(
self, url: str, headers: Dict[str, str], data: Dict[str, Any]
) -> Dict[str, Any]:
self, url: str, headers: dict[str, str], data: dict[str, Any]
) -> dict[str, Any]:
"""Submit a request to the FAL API."""
try:
response = httpx.post(url, headers=headers, json=data)
Expand All @@ -83,7 +83,7 @@ def _submit_request(
logger.error(f"FAL API request failed: {str(e)}")
raise RuntimeError(f"Failed to submit request: {str(e)}")

def _poll_status(self, status_url: str, headers: Dict[str, str]) -> Dict[str, Any]:
def _poll_status(self, status_url: str, headers: dict[str, str]) -> dict[str, Any]:
"""Poll the status endpoint until completion or failure."""
try:
response = httpx.get(status_url, headers=headers)
Expand Down
4 changes: 3 additions & 1 deletion autogpt_platform/backend/backend/blocks/github/triggers.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,9 @@ class Output(GitHubTriggerBase.Output):
def __init__(self):
from backend.integrations.webhooks.github import GithubWebhookType

example_payload = json.loads(self.EXAMPLE_PAYLOAD_FILE.read_text())
example_payload = json.loads(
self.EXAMPLE_PAYLOAD_FILE.read_text(encoding="utf-8")
)

super().__init__(
id="6c60ec01-8128-419e-988f-96a063ee2fea",
Expand Down
2 changes: 1 addition & 1 deletion autogpt_platform/backend/backend/data/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ def SchemaField(
title: Optional[str] = None,
description: Optional[str] = None,
placeholder: Optional[str] = None,
advanced: Optional[bool] = None,
advanced: Optional[bool] = False,
secret: bool = False,
exclude: bool = False,
hidden: Optional[bool] = None,
Expand Down
23 changes: 14 additions & 9 deletions autogpt_platform/backend/linter.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@
import subprocess

directory = os.path.dirname(os.path.realpath(__file__))
target_dirs = ["../backend", "../autogpt_libs"]

BACKEND_DIR = "."
LIBS_DIR = "../autogpt_libs"
TARGET_DIRS = [BACKEND_DIR, LIBS_DIR]


def run(*command: str) -> None:
Expand All @@ -12,17 +15,19 @@ def run(*command: str) -> None:

def lint():
try:
run("ruff", "check", *target_dirs, "--exit-zero")
run("isort", "--diff", "--check", "--profile", "black", ".")
run("black", "--diff", "--check", ".")
run("pyright", *target_dirs)
run("ruff", "check", *TARGET_DIRS, "--exit-zero")
run("ruff", "format", "--diff", "--check", LIBS_DIR)
run("isort", "--diff", "--check", "--profile", "black", BACKEND_DIR)
run("black", "--diff", "--check", BACKEND_DIR)
run("pyright", *TARGET_DIRS)
except subprocess.CalledProcessError as e:
print("Lint failed, try running `poetry run format` to fix the issues: ", e)
raise e


def format():
run("ruff", "check", "--fix", *target_dirs)
run("isort", "--profile", "black", ".")
run("black", ".")
run("pyright", *target_dirs)
run("ruff", "check", "--fix", *TARGET_DIRS)
run("ruff", "format", LIBS_DIR)
run("isort", "--profile", "black", BACKEND_DIR)
run("black", BACKEND_DIR)
run("pyright", *TARGET_DIRS)

0 comments on commit 34651ff

Please sign in to comment.