Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 45 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
173 changes: 173 additions & 0 deletions examples/simple_api_demo.py
Original file line number Diff line number Diff line change
@@ -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())
5 changes: 5 additions & 0 deletions src/adcp/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
34 changes: 34 additions & 0 deletions src/adcp/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Loading