feat(classic): add platform_blocks to Agent, enable via PLATFORM_API_KEY

- Add PlatformBlocksComponent to Agent as a default component
- Component automatically enables when PLATFORM_API_KEY env var is set
- Config now uses UserConfigurable for env var support:
  - PLATFORM_API_KEY (required to enable)
  - PLATFORM_URL (default: https://platform.agpt.co)
  - PLATFORM_BLOCKS_ENABLED (default: true)
  - PLATFORM_TIMEOUT (default: 60)
- API key stored as SecretStr for security

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Nicholas Tindle
2026-01-22 17:30:24 -06:00
parent 7dd181f4b0
commit c671af851f
4 changed files with 62 additions and 18 deletions

View File

@@ -36,24 +36,36 @@ class PlatformBlocksComponent(
def client(self) -> PlatformClient:
"""Get or create the platform client."""
if self._client is None:
api_key = ""
if self.config.api_key:
api_key = self.config.api_key.get_secret_value()
self._client = PlatformClient(
base_url=self.config.platform_url,
api_key=self.config.api_key,
api_key=api_key,
timeout=self.config.timeout,
)
return self._client
@property
def is_configured(self) -> bool:
"""Check if the component is properly configured with an API key."""
return bool(
self.config.enabled
and self.config.api_key
and self.config.api_key.get_secret_value()
)
def get_resources(self) -> Iterator[str]:
"""Describe available resources."""
if self.config.enabled:
if self.is_configured:
yield (
"Access to platform blocks via search_blocks and execute_block "
"commands. Use search_blocks first to discover available blocks."
)
def get_commands(self) -> Iterator[Command]:
"""Provide available commands."""
if not self.config.enabled:
"""Provide available commands only if configured with API key."""
if not self.is_configured:
return
yield self.search_blocks
yield self.execute_block

View File

@@ -1,15 +1,24 @@
from pydantic import BaseModel, Field
from typing import Optional
from pydantic import BaseModel, SecretStr
from forge.models.config import UserConfigurable
class PlatformBlocksConfig(BaseModel):
"""Configuration for platform blocks integration."""
"""Configuration for platform blocks integration.
enabled: bool = Field(
default=True, description="Whether platform blocks are enabled"
)
platform_url: str = Field(
Set PLATFORM_API_KEY environment variable to enable platform blocks.
"""
enabled: bool = UserConfigurable(default=True, from_env="PLATFORM_BLOCKS_ENABLED")
platform_url: str = UserConfigurable(
default="https://platform.agpt.co",
description="Platform API base URL",
from_env="PLATFORM_URL",
)
api_key: str = Field(default="", description="Platform API key for authentication")
timeout: int = Field(default=60, description="Request timeout in seconds")
api_key: Optional[SecretStr] = UserConfigurable(
default=None,
from_env="PLATFORM_API_KEY",
exclude=True,
)
timeout: int = UserConfigurable(default=60, from_env="PLATFORM_TIMEOUT")

View File

@@ -4,6 +4,7 @@ import json
from unittest.mock import AsyncMock
import pytest
from pydantic import SecretStr
from forge.components.platform_blocks import (
PlatformBlocksComponent,
@@ -63,8 +64,10 @@ def mock_blocks_response():
@pytest.fixture
def component():
"""Create a PlatformBlocksComponent for testing."""
return PlatformBlocksComponent()
"""Create a PlatformBlocksComponent for testing with API key configured."""
return PlatformBlocksComponent(
config=PlatformBlocksConfig(api_key=SecretStr("test-api-key"))
)
class TestSearchBlocks:
@@ -223,7 +226,7 @@ class TestConfiguration:
config = PlatformBlocksConfig()
assert config.enabled is True
assert config.platform_url == "https://platform.agpt.co"
assert config.api_key == ""
assert config.api_key is None
assert config.timeout == 60
def test_custom_configuration(self):
@@ -231,12 +234,13 @@ class TestConfiguration:
config = PlatformBlocksConfig(
enabled=False,
platform_url="https://dev-builder.agpt.co",
api_key="test-key",
api_key=SecretStr("test-key"),
timeout=120,
)
assert config.enabled is False
assert config.platform_url == "https://dev-builder.agpt.co"
assert config.api_key == "test-key"
assert config.api_key is not None
assert config.api_key.get_secret_value() == "test-key"
assert config.timeout == 120
def test_component_respects_disabled_config(self):
@@ -245,6 +249,22 @@ class TestConfiguration:
commands = list(component.get_commands())
assert len(commands) == 0
def test_component_disabled_without_api_key(self):
"""Component should not yield commands when api_key is not set."""
component = PlatformBlocksComponent(
config=PlatformBlocksConfig(enabled=True, api_key=None)
)
commands = list(component.get_commands())
assert len(commands) == 0
def test_component_enabled_with_api_key(self):
"""Component should yield commands when api_key is set."""
component = PlatformBlocksComponent(
config=PlatformBlocksConfig(enabled=True, api_key=SecretStr("test-key"))
)
commands = list(component.get_commands())
assert len(commands) == 2
class TestProtocols:
"""Tests for protocol implementations."""

View File

@@ -36,6 +36,7 @@ from forge.components.git_operations import GitOperationsComponent
from forge.components.http_client import HTTPClientComponent
from forge.components.image_gen import ImageGeneratorComponent
from forge.components.math_utils import MathUtilsComponent
from forge.components.platform_blocks import PlatformBlocksComponent
from forge.components.system import SystemComponent
from forge.components.text_utils import TextUtilsComponent
from forge.components.todo import TodoComponent
@@ -211,6 +212,8 @@ class Agent(BaseAgent[AnyActionProposal], Configurable[AgentSettings]):
self.watchdog = WatchdogComponent(settings.config, settings.history).run_after(
ContextComponent
)
# Platform blocks (enabled only if PLATFORM_API_KEY is set)
self.platform_blocks = PlatformBlocksComponent()
self.event_history = settings.history