Skip to content

Commit

Permalink
Merge branch 'dev' into master
Browse files Browse the repository at this point in the history
  • Loading branch information
Bentlybro authored Dec 2, 2024
2 parents 4612a89 + 5c49fc8 commit df13e12
Show file tree
Hide file tree
Showing 138 changed files with 5,639 additions and 1,474 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/classic-autogpt-docker-cache-clean.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ jobs:

- id: build
name: Build image
uses: docker/build-push-action@v5
uses: docker/build-push-action@v6
with:
context: classic/
file: classic/Dockerfile.autogpt
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/classic-autogpt-docker-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ jobs:
- id: build
name: Build image
uses: docker/build-push-action@v5
uses: docker/build-push-action@v6
with:
context: classic/
file: classic/Dockerfile.autogpt
Expand Down Expand Up @@ -117,7 +117,7 @@ jobs:

- id: build
name: Build image
uses: docker/build-push-action@v5
uses: docker/build-push-action@v6
with:
context: classic/
file: classic/Dockerfile.autogpt
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/classic-autogpt-docker-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ jobs:

- id: build
name: Build image
uses: docker/build-push-action@v5
uses: docker/build-push-action@v6
with:
context: classic/
file: Dockerfile.autogpt
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/codeql.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ on:
branches: [ "master", "release-*", "dev" ]
pull_request:
branches: [ "master", "release-*", "dev" ]
merge_group:
schedule:
- cron: '15 4 * * 0'

Expand Down
1 change: 1 addition & 0 deletions .github/workflows/platform-backend-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ on:
paths:
- ".github/workflows/platform-backend-ci.yml"
- "autogpt_platform/backend/**"
merge_group:

concurrency:
group: ${{ format('backend-ci-{0}', github.head_ref && format('{0}-{1}', github.event_name, github.event.pull_request.number) || github.sha) }}
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/platform-frontend-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ on:
paths:
- ".github/workflows/platform-frontend-ci.yml"
- "autogpt_platform/frontend/**"
merge_group:

defaults:
run:
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/platform-market-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ on:
paths:
- ".github/workflows/platform-market-ci.yml"
- "autogpt_platform/market/**"
merge_group:

concurrency:
group: ${{ format('backend-ci-{0}', github.head_ref && format('{0}-{1}', github.event_name, github.event.pull_request.number) || github.sha) }}
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/repo-workflow-checker.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ name: Repo - PR Status Checker
on:
pull_request:
types: [opened, synchronize, reopened]
merge_group:

jobs:
status-check:
Expand Down
7 changes: 6 additions & 1 deletion .github/workflows/scripts/check_actions_status.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,18 @@

CHECK_INTERVAL = 30


def get_environment_variables() -> Tuple[str, str, str, str, str]:
"""Retrieve and return necessary environment variables."""
try:
with open(os.environ["GITHUB_EVENT_PATH"]) as f:
event = json.load(f)

sha = event["pull_request"]["head"]["sha"]
# Handle both PR and merge group events
if "pull_request" in event:
sha = event["pull_request"]["head"]["sha"]
else:
sha = os.environ["GITHUB_SHA"]

return (
os.environ["GITHUB_API_URL"],
Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -171,3 +171,5 @@ ig*
.github_access_token
LICENSE.rtf
autogpt_platform/backend/settings.py
/.auth
/autogpt_platform/frontend/.auth
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ The AutoGPT frontend is where users interact with our powerful AI automation pla

**Monitoring and Analytics:** Keep track of your agents' performance and gain insights to continually improve your automation processes.

[Read this guide](https://docs.agpt.co/server/new_blocks/) to learn how to build your own custom blocks.
[Read this guide](https://docs.agpt.co/platform/new_blocks/) to learn how to build your own custom blocks.

### 💽 AutoGPT Server

Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
from typing import NamedTuple
import secrets
import hashlib
import secrets
from typing import NamedTuple


class APIKeyContainer(NamedTuple):
"""Container for API key parts."""

raw: str
prefix: str
postfix: str
hash: str


class APIKeyManager:
PREFIX: str = "agpt_"
PREFIX_LENGTH: int = 8
Expand All @@ -19,9 +22,9 @@ def generate_api_key(self) -> APIKeyContainer:
raw_key = f"{self.PREFIX}{secrets.token_urlsafe(32)}"
return APIKeyContainer(
raw=raw_key,
prefix=raw_key[:self.PREFIX_LENGTH],
postfix=raw_key[-self.POSTFIX_LENGTH:],
hash=hashlib.sha256(raw_key.encode()).hexdigest()
prefix=raw_key[: self.PREFIX_LENGTH],
postfix=raw_key[-self.POSTFIX_LENGTH :],
hash=hashlib.sha256(raw_key.encode()).hexdigest(),
)

def verify_api_key(self, provided_key: str, stored_hash: str) -> bool:
Expand Down
4 changes: 2 additions & 2 deletions autogpt_platform/autogpt_libs/autogpt_libs/auth/depends.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import fastapi

from .middleware import auth_middleware
from .models import User, DEFAULT_USER_ID, DEFAULT_EMAIL
from .config import Settings
from .middleware import auth_middleware
from .models import DEFAULT_USER_ID, User


def requires_user(payload: dict = fastapi.Depends(auth_middleware)) -> User:
Expand Down
167 changes: 167 additions & 0 deletions autogpt_platform/autogpt_libs/autogpt_libs/feature_flag/client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
import asyncio
import contextlib
import logging
from functools import wraps
from typing import Any, Awaitable, Callable, Dict, Optional, TypeVar, Union, cast

import ldclient
from fastapi import HTTPException
from ldclient import Context, LDClient
from ldclient.config import Config
from typing_extensions import ParamSpec

from .config import SETTINGS

logger = logging.getLogger(__name__)
logging.basicConfig(level=logging.DEBUG)

P = ParamSpec("P")
T = TypeVar("T")


def get_client() -> LDClient:
"""Get the LaunchDarkly client singleton."""
return ldclient.get()


def initialize_launchdarkly() -> None:
sdk_key = SETTINGS.launch_darkly_sdk_key
logger.debug(
f"Initializing LaunchDarkly with SDK key: {'present' if sdk_key else 'missing'}"
)

if not sdk_key:
logger.warning("LaunchDarkly SDK key not configured")
return

config = Config(sdk_key)
ldclient.set_config(config)

if ldclient.get().is_initialized():
logger.info("LaunchDarkly client initialized successfully")
else:
logger.error("LaunchDarkly client failed to initialize")


def shutdown_launchdarkly() -> None:
"""Shutdown the LaunchDarkly client."""
if ldclient.get().is_initialized():
ldclient.get().close()
logger.info("LaunchDarkly client closed successfully")


def create_context(
user_id: str, additional_attributes: Optional[Dict[str, Any]] = None
) -> Context:
"""Create LaunchDarkly context with optional additional attributes."""
builder = Context.builder(str(user_id)).kind("user")
if additional_attributes:
for key, value in additional_attributes.items():
builder.set(key, value)
return builder.build()


def feature_flag(
flag_key: str,
default: bool = False,
) -> Callable[
[Callable[P, Union[T, Awaitable[T]]]], Callable[P, Union[T, Awaitable[T]]]
]:
"""
Decorator for feature flag protected endpoints.
"""

def decorator(
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:
try:
user_id = kwargs.get("user_id")
if not user_id:
raise ValueError("user_id is required")

if not get_client().is_initialized():
logger.warning(
f"LaunchDarkly not initialized, using default={default}"
)
is_enabled = default
else:
context = create_context(str(user_id))
is_enabled = get_client().variation(flag_key, context, default)

if not is_enabled:
raise HTTPException(status_code=404, detail="Feature not available")

result = func(*args, **kwargs)
if asyncio.iscoroutine(result):
return await result
return cast(T, result)
except Exception as e:
logger.error(f"Error evaluating feature flag {flag_key}: {e}")
raise

@wraps(func)
def sync_wrapper(*args: P.args, **kwargs: P.kwargs) -> T:
try:
user_id = kwargs.get("user_id")
if not user_id:
raise ValueError("user_id is required")

if not get_client().is_initialized():
logger.warning(
f"LaunchDarkly not initialized, using default={default}"
)
is_enabled = default
else:
context = create_context(str(user_id))
is_enabled = get_client().variation(flag_key, context, default)

if not is_enabled:
raise HTTPException(status_code=404, detail="Feature not available")

return cast(T, func(*args, **kwargs))
except Exception as e:
logger.error(f"Error evaluating feature flag {flag_key}: {e}")
raise

return cast(
Callable[P, Union[T, Awaitable[T]]],
async_wrapper if asyncio.iscoroutinefunction(func) else sync_wrapper,
)

return decorator


def percentage_rollout(
flag_key: str,
default: bool = False,
) -> Callable[
[Callable[P, Union[T, Awaitable[T]]]], Callable[P, Union[T, Awaitable[T]]]
]:
"""Decorator for percentage-based rollouts."""
return feature_flag(flag_key, default)


def beta_feature(
flag_key: Optional[str] = None,
unauthorized_response: Any = {"message": "Not available in beta"},
) -> Callable[
[Callable[P, Union[T, Awaitable[T]]]], Callable[P, Union[T, Awaitable[T]]]
]:
"""Decorator for beta features."""
actual_key = f"beta-{flag_key}" if flag_key else "beta"
return feature_flag(actual_key, False)


@contextlib.contextmanager
def mock_flag_variation(flag_key: str, return_value: Any):
"""Context manager for testing feature flags."""
original_variation = get_client().variation
get_client().variation = lambda key, context, default: (
return_value if key == flag_key else original_variation(key, context, default)
)
try:
yield
finally:
get_client().variation = original_variation
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import pytest
from ldclient import LDClient

from autogpt_libs.feature_flag.client import feature_flag, mock_flag_variation


@pytest.fixture
def ld_client(mocker):
client = mocker.Mock(spec=LDClient)
mocker.patch("ldclient.get", return_value=client)
client.is_initialized.return_value = True
return client


@pytest.mark.asyncio
async def test_feature_flag_enabled(ld_client):
ld_client.variation.return_value = True

@feature_flag("test-flag")
async def test_function(user_id: str):
return "success"

result = test_function(user_id="test-user")
assert result == "success"
ld_client.variation.assert_called_once()


@pytest.mark.asyncio
async def test_feature_flag_unauthorized_response(ld_client):
ld_client.variation.return_value = False

@feature_flag("test-flag")
async def test_function(user_id: str):
return "success"

result = test_function(user_id="test-user")
assert result == {"error": "disabled"}


def test_mock_flag_variation(ld_client):
with mock_flag_variation("test-flag", True):
assert ld_client.variation("test-flag", None, False)

with mock_flag_variation("test-flag", False):
assert ld_client.variation("test-flag", None, False)
15 changes: 15 additions & 0 deletions autogpt_platform/autogpt_libs/autogpt_libs/feature_flag/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
from pydantic import Field
from pydantic_settings import BaseSettings, SettingsConfigDict


class Settings(BaseSettings):
launch_darkly_sdk_key: str = Field(
default="",
description="The Launch Darkly SDK key",
validation_alias="LAUNCH_DARKLY_SDK_KEY",
)

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


SETTINGS = Settings()
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

from pydantic import Field, field_validator
from pydantic_settings import BaseSettings, SettingsConfigDict

from .filters import BelowLevelFilter
from .formatters import AGPTFormatter, StructuredLoggingFormatter

Expand Down
Loading

0 comments on commit df13e12

Please sign in to comment.