diff --git a/README.md b/README.md index b56b0d4..d27b993 100644 --- a/README.md +++ b/README.md @@ -26,36 +26,64 @@ pip install adcp ## Quick Start: Test Helpers -The fastest way to get started is using the pre-configured test agents: +The fastest way to get started is using pre-configured test agents with the **`.simple` API**: ```python from adcp.testing import test_agent -from adcp.types.generated import GetProductsRequest -# Zero configuration - just import and use! -result = await test_agent.get_products( - GetProductsRequest( - brief="Coffee subscription service", - promoted_offering="Premium coffee deliveries" - ) +# Zero configuration - just import and call with kwargs! +products = await test_agent.simple.get_products( + brief='Coffee subscription service for busy professionals' ) -if result.success: - print(f"Found {len(result.data.products)} products") +print(f"Found {len(products.products)} products") +``` + +### Simple vs. Standard API + +**Every ADCPClient** includes both API styles via the `.simple` accessor: + +**Simple API** (`client.simple.*`) - Recommended for examples/prototyping: +```python +from adcp.testing import test_agent + +# Kwargs and direct return - raises on error +products = await test_agent.simple.get_products(brief='Coffee brands') +print(products.products[0].name) +``` + +**Standard API** (`client.*`) - Recommended for production: +```python +from adcp.testing import test_agent +from adcp.types.generated import GetProductsRequest + +# Explicit request objects and TaskResult wrapper +request = GetProductsRequest(brief='Coffee brands') +result = await test_agent.get_products(request) + +if result.success and result.data: + print(result.data.products[0].name) +else: + print(f"Error: {result.error}") ``` -Test helpers include: -- **`test_agent`**: Pre-configured MCP test agent with authentication -- **`test_agent_a2a`**: Pre-configured A2A test agent with authentication -- **`test_agent_no_auth`**: Pre-configured MCP test agent WITHOUT authentication -- **`test_agent_a2a_no_auth`**: Pre-configured A2A test agent WITHOUT authentication +**When to use which:** +- **Simple API** (`.simple`): Quick testing, documentation, examples, notebooks +- **Standard API**: Production code, complex error handling, webhook workflows + +### Available Test Helpers + +Pre-configured agents (all include `.simple` accessor): +- **`test_agent`**: MCP test agent with authentication +- **`test_agent_a2a`**: A2A test agent with authentication +- **`test_agent_no_auth`**: MCP test agent without authentication +- **`test_agent_a2a_no_auth`**: A2A test agent without authentication - **`creative_agent`**: Reference creative agent for preview functionality - **`test_agent_client`**: Multi-agent client with both protocols -- **`create_test_agent()`**: Factory for custom test configurations > **Note**: Test agents are rate-limited and for testing/examples only. DO NOT use in production. -See [examples/test_helpers_demo.py](examples/test_helpers_demo.py) for more examples. +See [examples/simple_api_demo.py](examples/simple_api_demo.py) for a complete comparison. ## Quick Start: Distributed Operations diff --git a/examples/simple_api_demo.py b/examples/simple_api_demo.py new file mode 100644 index 0000000..5d07ab7 --- /dev/null +++ b/examples/simple_api_demo.py @@ -0,0 +1,173 @@ +"""Demo of the simplified API accessor. + +This example demonstrates the .simple accessor available on all ADCPClient instances: +- Accepts kwargs directly (no request objects needed) +- Returns unwrapped data (no TaskResult.data unwrapping) +- Raises exceptions on errors + +Compare this to the standard API which requires explicit request objects +and TaskResult unwrapping. +""" + +import asyncio + +# Import test agents +from adcp.testing import creative_agent, test_agent +from adcp.types.generated import GetProductsRequest + + +async def demo_simple_api(): + """Demo the .simple accessor API.""" + print("=== Simple API Demo (client.simple.*) ===\n") + + # Simple kwargs-based call, direct data return + products = await test_agent.simple.get_products( + brief="Coffee subscription service for busy professionals", + ) + + print(f"Found {len(products.products)} products") + if products.products: + product = products.products[0] + print(f" - {product.name}") + print(f" {product.description}\n") + + # List formats with simple API + formats = await test_agent.simple.list_creative_formats() + print(f"Found {len(formats.formats)} creative formats") + if formats.formats: + fmt = formats.formats[0] + print(f" - {fmt.name}") + print(f" {fmt.description}\n") + + # Creative agent also has .simple accessor + print("Creative agent preview:") + try: + preview = await creative_agent.simple.preview_creative( + manifest={ + "format_id": { + "id": "banner_300x250", + "agent_url": "https://creative.adcontextprotocol.org", + }, + "assets": {}, + } + ) + if preview.previews: + print(f" Generated {len(preview.previews)} preview(s)\n") + except Exception as e: + print(f" Preview failed (expected for demo): {e}\n") + + +async def demo_standard_api_comparison(): + """Compare with standard API for reference.""" + print("=== Standard API (for comparison) ===\n") + + # Standard API: More verbose but full control over error handling + request = GetProductsRequest( + brief="Coffee subscription service for busy professionals", + ) + + result = await test_agent.get_products(request) + + if result.success and result.data: + print(f"Found {len(result.data.products)} products") + if result.data.products: + product = result.data.products[0] + print(f" - {product.name}") + print(f" {product.description}\n") + else: + print(f"Error: {result.error}\n") + + +async def demo_production_client(): + """Show that .simple works on any ADCPClient.""" + print("=== Simple API on Production Clients ===\n") + + # Create a production client + from adcp import ADCPClient, AgentConfig, Protocol + from adcp.testing import TEST_AGENT_TOKEN + + client = ADCPClient( + AgentConfig( + id="my-agent", + agent_uri="https://test-agent.adcontextprotocol.org/mcp/", + protocol=Protocol.MCP, + auth_token=TEST_AGENT_TOKEN, # Public test token (rate-limited) + ) + ) + + # Both APIs available + print("Standard API:") + result = await client.get_products(GetProductsRequest(brief="Test")) + print(f" Result type: {type(result).__name__}") + print(f" Has .success: {hasattr(result, 'success')}") + print(f" Has .data: {hasattr(result, 'data')}\n") + + print("Simple API:") + try: + products = await client.simple.get_products(brief="Test") + print(f" Result type: {type(products).__name__}") + print(f" Direct access to .products: {hasattr(products, 'products')}") + except Exception as e: + print(f" (Expected error for demo: {e})") + + +def demo_sync_usage(): + """Show how to use simple API in sync contexts.""" + print("\n=== Using Simple API in Sync Contexts ===\n") + + print("The simple API is async-only, but you can use asyncio.run() for sync contexts:") + print() + print(" # In a Jupyter notebook or sync function:") + print(" import asyncio") + print(" from adcp.testing import test_agent") + print() + print(" products = asyncio.run(test_agent.simple.get_products(brief='Coffee'))") + print(" print(f'Found {len(products.products)} products')") + print() + print(" # Or create an async function and run it:") + print(" async def my_function():") + print(" products = await test_agent.simple.get_products(brief='Coffee')") + print(" return products") + print() + print(" result = asyncio.run(my_function())") + print() + + +async def main(): + """Run all demos.""" + print("\n" + "=" * 60) + print("ADCP Python SDK - Simple API Demo") + print("=" * 60 + "\n") + + # Demo simple API + await demo_simple_api() + + # Show standard API for comparison + await demo_standard_api_comparison() + + # Show it works on any client + await demo_production_client() + + # Show sync usage pattern + demo_sync_usage() + + print("\n" + "=" * 60) + print("Key Differences:") + print("=" * 60) + print("\nSimple API (client.simple.*):") + print(" ✓ Kwargs instead of request objects") + print(" ✓ Direct data return (no unwrapping)") + print(" ✓ Raises exceptions on errors") + print(" ✓ Available on ALL ADCPClient instances") + print(" ✓ Use asyncio.run() for sync contexts") + print(" → Best for: documentation, examples, quick testing, notebooks") + print("\nStandard API (client.*):") + print(" ✓ Explicit request objects (type-safe)") + print(" ✓ TaskResult wrapper (full status info)") + print(" ✓ Explicit error handling") + print(" → Best for: production code, complex workflows, webhooks") + print("\n") + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/src/adcp/client.py b/src/adcp/client.py index f1c5c5f..f1cbb08 100644 --- a/src/adcp/client.py +++ b/src/adcp/client.py @@ -86,6 +86,11 @@ def __init__( else: raise ValueError(f"Unsupported protocol: {agent_config.protocol}") + # Initialize simple API accessor (lazy import to avoid circular dependency) + from adcp.simple import SimpleAPI + + self.simple = SimpleAPI(self) + def get_webhook_url(self, task_type: str, operation_id: str) -> str: """Generate webhook URL for a task.""" if not self.webhook_url_template: diff --git a/src/adcp/exceptions.py b/src/adcp/exceptions.py index 0c52451..36e6ffd 100644 --- a/src/adcp/exceptions.py +++ b/src/adcp/exceptions.py @@ -119,3 +119,37 @@ def __init__(self, message: str = "Invalid webhook signature", agent_id: str | N " Webhook signatures use HMAC-SHA256 for security." ) super().__init__(message, agent_id, None, suggestion) + + +class ADCPSimpleAPIError(ADCPError): + """Error from simplified API (.simple accessor). + + Raised when a simple API method fails. The underlying error details + are available in the message. For more control over error handling, + use the standard API (client.method()) instead of client.simple.method(). + """ + + def __init__( + self, + operation: str, + error_message: str | None = None, + agent_id: str | None = None, + ): + """Initialize simple API error. + + Args: + operation: The operation that failed (e.g., "get_products") + error_message: The underlying error message from TaskResult + agent_id: Optional agent ID for context + """ + message = f"{operation} failed" + if error_message: + message = f"{message}: {error_message}" + + suggestion = ( + f"For more control over error handling, use the standard API:\n" + f" result = await client.{operation}(request)\n" + f" if not result.success:\n" + f" # Handle error with full TaskResult context" + ) + super().__init__(message, agent_id, None, suggestion) diff --git a/src/adcp/simple.py b/src/adcp/simple.py new file mode 100644 index 0000000..75ea975 --- /dev/null +++ b/src/adcp/simple.py @@ -0,0 +1,347 @@ +"""Simplified API accessor for ADCPClient. + +Provides an ergonomic API with: +- Kwargs instead of request objects +- Direct return values (no TaskResult unwrapping) +- Raises exceptions on errors + +Usage: + client = ADCPClient(config) + + # Standard API: full control + result = await client.get_products(GetProductsRequest(brief="Coffee")) + if result.success: + print(result.data.products) + + # Simple API: ergonomic + products = await client.simple.get_products(brief="Coffee") + print(products.products) +""" + +from __future__ import annotations + +from typing import TYPE_CHECKING, Any + +from adcp.exceptions import ADCPSimpleAPIError +from adcp.types.generated import ( + ActivateSignalRequest, + ActivateSignalResponse, + GetMediaBuyDeliveryRequest, + GetMediaBuyDeliveryResponse, + GetProductsRequest, + GetProductsResponse, + GetSignalsRequest, + GetSignalsResponse, + ListAuthorizedPropertiesRequest, + ListAuthorizedPropertiesResponse, + ListCreativeFormatsRequest, + ListCreativeFormatsResponse, + ListCreativesRequest, + ListCreativesResponse, + PreviewCreativeRequest, + PreviewCreativeResponse, + ProvidePerformanceFeedbackRequest, + ProvidePerformanceFeedbackResponse, + SyncCreativesRequest, + SyncCreativesResponse, +) + +if TYPE_CHECKING: + from adcp.client import ADCPClient + + +class SimpleAPI: + """Simplified API accessor for ergonomic usage. + + Provides kwargs-based methods that return unwrapped response data + and raise exceptions on errors. + + This is intended for: + - Quick prototyping and testing + - Documentation and examples + - Simple scripts and notebooks + + For production code with complex error handling, use the standard + client API which returns TaskResult wrappers. + """ + + def __init__(self, client: ADCPClient): + """Initialize simple API accessor. + + Args: + client: The ADCPClient instance to wrap + """ + self._client = client + + async def get_products( + self, + **kwargs: Any, + ) -> GetProductsResponse: + """Get advertising products (simplified). + + This is a convenience wrapper around client.get_products() that: + - Accepts kwargs instead of GetProductsRequest + - Returns unwrapped GetProductsResponse + - Raises ADCPSimpleAPIError on failures + + For full control over error handling, use client.get_products() instead. + + Args: + **kwargs: Arguments for GetProductsRequest (brief, brand_manifest, etc.) + + Returns: + GetProductsResponse directly (no TaskResult wrapper) + + Raises: + ADCPSimpleAPIError: If request fails. Use standard API for detailed error handling. + + Example: + products = await client.simple.get_products( + brief='Coffee subscription service' + ) + print(f"Found {len(products.products)} products") + """ + request = GetProductsRequest(**kwargs) + result = await self._client.get_products(request) + if not result.success or not result.data: + raise ADCPSimpleAPIError( + operation="get_products", + error_message=result.error, + agent_id=self._client.agent_config.id, + ) + return result.data + + async def list_creative_formats( + self, + **kwargs: Any, + ) -> ListCreativeFormatsResponse: + """List supported creative formats. + + Args: + **kwargs: Arguments passed to ListCreativeFormatsRequest + + Returns: + ListCreativeFormatsResponse with formats list + + Raises: + Exception: If the request fails + + Example: + formats = await client.simple.list_creative_formats() + print(f"Found {len(formats.formats)} formats") + """ + request = ListCreativeFormatsRequest(**kwargs) + result = await self._client.list_creative_formats(request) + if not result.success or not result.data: + raise ADCPSimpleAPIError( + operation="list_creative_formats", + error_message=result.error, + agent_id=self._client.agent_config.id, + ) + return result.data + + async def preview_creative( + self, + **kwargs: Any, + ) -> PreviewCreativeResponse: + """Preview creative manifest. + + Args: + **kwargs: Arguments passed to PreviewCreativeRequest + + Returns: + PreviewCreativeResponse with preview data + + Raises: + Exception: If the request fails + + Example: + preview = await client.simple.preview_creative( + manifest={'format_id': 'banner_300x250', 'assets': {...}} + ) + print(f"Preview: {preview.previews[0]}") + """ + request = PreviewCreativeRequest(**kwargs) + result = await self._client.preview_creative(request) + if not result.success or not result.data: + raise ADCPSimpleAPIError( + operation="preview_creative", + error_message=result.error, + agent_id=self._client.agent_config.id, + ) + return result.data + + async def sync_creatives( + self, + **kwargs: Any, + ) -> SyncCreativesResponse: + """Sync creatives. + + Args: + **kwargs: Arguments passed to SyncCreativesRequest + + Returns: + SyncCreativesResponse + + Raises: + Exception: If the request fails + """ + request = SyncCreativesRequest(**kwargs) + result = await self._client.sync_creatives(request) + if not result.success or not result.data: + raise ADCPSimpleAPIError( + operation="sync_creatives", + error_message=result.error, + agent_id=self._client.agent_config.id, + ) + return result.data + + async def list_creatives( + self, + **kwargs: Any, + ) -> ListCreativesResponse: + """List creatives. + + Args: + **kwargs: Arguments passed to ListCreativesRequest + + Returns: + ListCreativesResponse + + Raises: + Exception: If the request fails + """ + request = ListCreativesRequest(**kwargs) + result = await self._client.list_creatives(request) + if not result.success or not result.data: + raise ADCPSimpleAPIError( + operation="list_creatives", + error_message=result.error, + agent_id=self._client.agent_config.id, + ) + return result.data + + async def get_media_buy_delivery( + self, + **kwargs: Any, + ) -> GetMediaBuyDeliveryResponse: + """Get media buy delivery. + + Args: + **kwargs: Arguments passed to GetMediaBuyDeliveryRequest + + Returns: + GetMediaBuyDeliveryResponse + + Raises: + Exception: If the request fails + """ + request = GetMediaBuyDeliveryRequest(**kwargs) + result = await self._client.get_media_buy_delivery(request) + if not result.success or not result.data: + raise ADCPSimpleAPIError( + operation="get_media_buy_delivery", + error_message=result.error, + agent_id=self._client.agent_config.id, + ) + return result.data + + async def list_authorized_properties( + self, + **kwargs: Any, + ) -> ListAuthorizedPropertiesResponse: + """List authorized properties. + + Args: + **kwargs: Arguments passed to ListAuthorizedPropertiesRequest + + Returns: + ListAuthorizedPropertiesResponse + + Raises: + Exception: If the request fails + """ + request = ListAuthorizedPropertiesRequest(**kwargs) + result = await self._client.list_authorized_properties(request) + if not result.success or not result.data: + raise ADCPSimpleAPIError( + operation="list_authorized_properties", + error_message=result.error, + agent_id=self._client.agent_config.id, + ) + return result.data + + async def get_signals( + self, + **kwargs: Any, + ) -> GetSignalsResponse: + """Get signals. + + Args: + **kwargs: Arguments passed to GetSignalsRequest + + Returns: + GetSignalsResponse + + Raises: + Exception: If the request fails + """ + request = GetSignalsRequest(**kwargs) + result = await self._client.get_signals(request) + if not result.success or not result.data: + raise ADCPSimpleAPIError( + operation="get_signals", + error_message=result.error, + agent_id=self._client.agent_config.id, + ) + return result.data + + async def activate_signal( + self, + **kwargs: Any, + ) -> ActivateSignalResponse: + """Activate signal. + + Args: + **kwargs: Arguments passed to ActivateSignalRequest + + Returns: + ActivateSignalResponse + + Raises: + Exception: If the request fails + """ + request = ActivateSignalRequest(**kwargs) + result = await self._client.activate_signal(request) + if not result.success or not result.data: + raise ADCPSimpleAPIError( + operation="activate_signal", + error_message=result.error, + agent_id=self._client.agent_config.id, + ) + return result.data + + async def provide_performance_feedback( + self, + **kwargs: Any, + ) -> ProvidePerformanceFeedbackResponse: + """Provide performance feedback. + + Args: + **kwargs: Arguments passed to ProvidePerformanceFeedbackRequest + + Returns: + ProvidePerformanceFeedbackResponse + + Raises: + Exception: If the request fails + """ + request = ProvidePerformanceFeedbackRequest(**kwargs) + result = await self._client.provide_performance_feedback(request) + if not result.success or not result.data: + raise ADCPSimpleAPIError( + operation="provide_performance_feedback", + error_message=result.error, + agent_id=self._client.agent_config.id, + ) + return result.data diff --git a/src/adcp/testing/__init__.py b/src/adcp/testing/__init__.py index 1e56ed2..9c05f39 100644 --- a/src/adcp/testing/__init__.py +++ b/src/adcp/testing/__init__.py @@ -1,6 +1,21 @@ """Test helpers for AdCP client library. Provides pre-configured test agents for examples and quick testing. + +All test agents include a `.simple` accessor for ergonomic usage: + +- **Standard API** (client methods): Full TaskResult with error handling +- **Simple API** (client.simple methods): Direct returns, raises on error + +Example: + # Standard API - explicit control + result = await test_agent.get_products(GetProductsRequest(brief='Coffee')) + if result.success: + print(result.data.products) + + # Simple API - ergonomic + products = await test_agent.simple.get_products(brief='Coffee') + print(products.products) """ from __future__ import annotations diff --git a/tests/test_simple_api.py b/tests/test_simple_api.py new file mode 100644 index 0000000..b593f49 --- /dev/null +++ b/tests/test_simple_api.py @@ -0,0 +1,183 @@ +"""Tests for the simplified API accessor.""" + +from __future__ import annotations + +from unittest.mock import AsyncMock, patch + +import pytest + +from adcp.testing import test_agent +from adcp.types.core import TaskResult, TaskStatus +from adcp.types.generated import ( + GetProductsResponse, + ListCreativeFormatsResponse, + PreviewCreativeResponse, + Product, +) + + +@pytest.mark.asyncio +async def test_get_products_simple_api(): + """Test client.simple.get_products with kwargs.""" + # Create mock response (using model_construct to bypass validation for test data) + mock_product = Product.model_construct( + product_id="prod_1", + name="Test Product", + description="A test product", + ) + mock_response = GetProductsResponse.model_construct(products=[mock_product]) + mock_result = TaskResult[GetProductsResponse]( + status=TaskStatus.COMPLETED, data=mock_response, success=True + ) + + # Mock the client's get_products method + with patch.object(test_agent, "get_products", new=AsyncMock(return_value=mock_result)): + # Call simplified API with kwargs + result = await test_agent.simple.get_products(brief="Coffee subscription service") + + # Verify it returns unwrapped data + assert isinstance(result, GetProductsResponse) + assert len(result.products) == 1 + assert result.products[0].product_id == "prod_1" + + # Verify the underlying call was made correctly + test_agent.get_products.assert_called_once() + call_args = test_agent.get_products.call_args[0][0] + assert call_args.brief == "Coffee subscription service" + + +@pytest.mark.asyncio +async def test_get_products_simple_api_failure(): + """Test client.simple.get_products raises exception on failure.""" + from adcp.exceptions import ADCPSimpleAPIError + + # Create mock failure response + mock_result = TaskResult[GetProductsResponse]( + status=TaskStatus.FAILED, data=None, success=False, error="Test error" + ) + + with patch.object(test_agent, "get_products", new=AsyncMock(return_value=mock_result)): + # Should raise ADCPSimpleAPIError on failure + with pytest.raises(ADCPSimpleAPIError, match="get_products failed"): + await test_agent.simple.get_products(brief="Test") + + +def test_simple_api_has_no_sync_methods(): + """Test that simple API only provides async methods. + + Users can wrap with asyncio.run() if they need sync behavior. + """ + # Verify simple API doesn't have sync methods + assert not hasattr(test_agent.simple, "get_products_sync") + assert hasattr(test_agent.simple, "get_products") + + +@pytest.mark.asyncio +async def test_list_creative_formats_simple_api(): + """Test client.simple.list_creative_formats with kwargs.""" + from adcp.types.generated import Format + + # Create mock response (using model_construct to bypass validation for test data) + mock_format = Format.model_construct( + format_id={"id": "banner_300x250"}, + name="Banner 300x250", + description="Standard banner", + ) + mock_response = ListCreativeFormatsResponse.model_construct(formats=[mock_format]) + mock_result = TaskResult[ListCreativeFormatsResponse]( + status=TaskStatus.COMPLETED, data=mock_response, success=True + ) + + with patch.object(test_agent, "list_creative_formats", new=AsyncMock(return_value=mock_result)): + # Call simplified API + result = await test_agent.simple.list_creative_formats() + + # Verify it returns unwrapped data + assert isinstance(result, ListCreativeFormatsResponse) + assert len(result.formats) == 1 + assert result.formats[0].format_id["id"] == "banner_300x250" + + +def test_simple_api_exists_on_client(): + """Test that all clients have a .simple accessor.""" + from adcp.testing import creative_agent, test_agent_a2a + + # All clients should have .simple + assert hasattr(test_agent, "simple") + assert hasattr(test_agent_a2a, "simple") + assert hasattr(creative_agent, "simple") + + # Should be SimpleAPI instance + from adcp.simple import SimpleAPI + + assert isinstance(test_agent.simple, SimpleAPI) + assert isinstance(test_agent_a2a.simple, SimpleAPI) + assert isinstance(creative_agent.simple, SimpleAPI) + + +def test_simple_api_on_freshly_constructed_client(): + """Test that .simple accessor works on freshly constructed ADCPClient.""" + from adcp import ADCPClient, AgentConfig, Protocol + from adcp.simple import SimpleAPI + + # Create a new client from scratch + client = ADCPClient( + AgentConfig( + id="test-agent", + agent_uri="https://test.example.com/mcp/", + protocol=Protocol.MCP, + auth_token="test-token", + ) + ) + + # Should have .simple accessor + assert hasattr(client, "simple") + assert isinstance(client.simple, SimpleAPI) + + # Should reference the same client + assert client.simple._client is client + + +@pytest.mark.asyncio +async def test_preview_creative_simple_api(): + """Test client.simple.preview_creative.""" + from adcp.testing import creative_agent + + mock_response = PreviewCreativeResponse( + previews=[{"url": "https://preview.example.com/123", "html": "..."}] + ) + mock_result = TaskResult[PreviewCreativeResponse]( + status=TaskStatus.COMPLETED, data=mock_response, success=True + ) + + with patch.object(creative_agent, "preview_creative", new=AsyncMock(return_value=mock_result)): + # Call simplified API + result = await creative_agent.simple.preview_creative( + manifest={"format_id": "banner_300x250", "assets": {}} + ) + + # Verify it returns unwrapped data + assert isinstance(result, PreviewCreativeResponse) + assert result.previews is not None + assert len(result.previews) == 1 + + +def test_simple_api_methods(): + """Test that SimpleAPI has all expected methods.""" + # Check all methods exist + assert hasattr(test_agent.simple, "get_products") + assert hasattr(test_agent.simple, "list_creative_formats") + assert hasattr(test_agent.simple, "preview_creative") + assert hasattr(test_agent.simple, "sync_creatives") + assert hasattr(test_agent.simple, "list_creatives") + assert hasattr(test_agent.simple, "get_media_buy_delivery") + assert hasattr(test_agent.simple, "list_authorized_properties") + assert hasattr(test_agent.simple, "get_signals") + assert hasattr(test_agent.simple, "activate_signal") + assert hasattr(test_agent.simple, "provide_performance_feedback") + + # Verify they're all async methods (not sync) + import inspect + + assert inspect.iscoroutinefunction(test_agent.simple.get_products) + assert inspect.iscoroutinefunction(test_agent.simple.list_creative_formats)