diff --git a/MIGRATION_GUIDE.md b/MIGRATION_GUIDE.md new file mode 100644 index 00000000..19720799 --- /dev/null +++ b/MIGRATION_GUIDE.md @@ -0,0 +1,1843 @@ +# Migration Guide: MemoryClient and IdentityClient Deprecation + +This document provides detailed migration instructions for deprecated operations in `MemoryClient` and `IdentityClient`: + +- **MemoryClient**: All operations are deprecated. Migrate to `UnifiedBedrockAgentCoreClient` (control plane) and `MemorySessionManager` (data plane). +- **IdentityClient**: Simple boto3 wrapper methods are deprecated. **For OAuth flows (`get_token()`, `get_api_key()`), migrate to auth decorators** from `bedrock_agentcore.identity.auth` (`@requires_access_token`, `@requires_api_key`). +- **CodeInterpreter & BrowserClient**: ⚠️ **NOT deprecated** - see [Why CodeInterpreter and BrowserClient Are NOT Deprecated](#why-codeinterpreter-and-browserclient-are-not-deprecated) + +## Table of Contents + +**Legend:** +- ✅ **Good Migration** - Direct 1:1 replacement, simple parameter changes +- ⚠️ **Iffy Migration** - Requires manual work (polling, payload construction, helper functions) +- ❌ **No Alternative** - No direct replacement available + +--- + +- [Why CodeInterpreter and BrowserClient Are NOT Deprecated](#why-codeinterpreter-and-browserclient-are-not-deprecated) + +- [MemoryClient Migration](#memoryclient-migration) + - [Control Plane Operations](#control-plane-operations) + - ✅ [`create_memory()`](#create_memory) + - ⚠️ [`create_memory_and_wait()`](#create_memory_and_wait) + - ✅ [`get_memory()`](#get_memory) + - ✅ [`list_memories()`](#list_memories) + - ✅ [`update_memory()`](#update_memory) + - ✅ [`delete_memory()`](#delete_memory) + - ⚠️ [`delete_memory_and_wait()`](#delete_memory_and_wait) + - ⚠️ [`create_or_get_memory()`](#create_or_get_memory) + - [Data Plane Operations](#data-plane-operations) + - ✅ [`create_event()`](#create_event) + - ✅ [`get_event()`](#get_event) + - ✅ [`list_events()`](#list_events) + - ✅ [`delete_event()`](#delete_event) + - ✅ [`retrieve_memory_records()` / `retrieve_memories()`](#retrieve_memory_records--retrieve_memories) + - ✅ [`get_memory_record()`](#get_memory_record) + - ✅ [`list_memory_records()`](#list_memory_records) + - ✅ [`delete_memory_record()`](#delete_memory_record) + - [Strategy Management Operations](#strategy-management-operations) + - ✅ [`add_semantic_strategy()`](#add_semantic_strategy) + - ⚠️ [`add_semantic_strategy_and_wait()`](#add_semantic_strategy_and_wait) + - ✅ [`add_summary_strategy()`](#add_summary_strategy) + - ⚠️ [`add_summary_strategy_and_wait()`](#add_summary_strategy_and_wait) + - ✅ [`add_user_preference_strategy()`](#add_user_preference_strategy) + - ⚠️ [`add_user_preference_strategy_and_wait()`](#add_user_preference_strategy_and_wait) + - ✅ [`modify_strategy()`](#modify_strategy) + - ✅ [`delete_strategy()`](#delete_strategy) + - ✅ [`update_memory_strategies()`](#update_memory_strategies) + - ⚠️ [`update_memory_strategies_and_wait()`](#update_memory_strategies_and_wait) + - [Conversation Helper Operations](#conversation-helper-operations) + - ✅ [`save_conversation()`](#save_conversation) + - ✅ [`save_turn()`](#save_turn) + - ✅ [`process_turn_with_llm()`](#process_turn_with_llm) + - ✅ [`get_last_k_turns()`](#get_last_k_turns) + - ✅ [`list_branches()`](#list_branches) + - ❌ [`get_conversation_tree()`](#get_conversation_tree) + - ✅ [`fork_conversation()`](#fork_conversation) + - ⚠️ [`wait_for_memories()`](#wait_for_memories) +- [IdentityClient Migration](#identityclient-migration) + - [Workload Identity Operations](#workload-identity-operations) + - ✅ [`create_workload_identity()`](#create_workload_identity) + - ✅ [`get_workload_identity()`](#get_workload_identity) + - ✅ [`update_workload_identity()`](#update_workload_identity) + - ✅ [`delete_workload_identity()`](#delete_workload_identity) + - ✅ [`list_workload_identities()`](#list_workload_identities) + - [Credential Provider Operations](#credential-provider-operations) + - ✅ [`create_oauth2_credential_provider()`](#create_oauth2_credential_provider) + - ✅ [`create_api_key_credential_provider()`](#create_api_key_credential_provider) + - ✅ [`get_credential_provider()`](#get_credential_provider) + - ✅ [`list_credential_providers()`](#list_credential_providers) + - ✅ [`delete_credential_provider()`](#delete_credential_provider) + - [Token Operations](#token-operations) + - ✅ [`get_workload_access_token()`](#get_workload_access_token) + - ✅ [`get_api_key()`](#get_api_key) + - ⚠️ [`get_token()`](#get_token) +- [Summary Tables](#summary-tables) +- [Quick Reference: Helper Functions](#quick-reference-helper-functions) +- [Appendix: When OAuth Decorators Are NOT Sufficient](#appendix-when-oauth-decorators-are-not-sufficient) + +--- + +## Why CodeInterpreter and BrowserClient Are NOT Deprecated + +**`CodeInterpreter` and `BrowserClient` will remain fully supported and are NOT being deprecated.** + +### Why These Clients Are Different + +Unlike `MemoryClient` and `IdentityClient` (which are thin wrappers around boto3 clients), `CodeInterpreter` and `BrowserClient` are **stateful clients** that maintain active session state: + +**State Maintained:** +- `identifier` - Current interpreter/browser identifier +- `session_id` - Active session ID +- Additional session context (e.g., uploaded files in CodeInterpreter) + +### Stateful Workflow Example + +**With CodeInterpreter (Stateful):** +```python +client = CodeInterpreter('us-west-2') +client.start() # Creates session, stores state + +# All operations use stored session state automatically +client.execute_code("x = 5") # No need to pass identifier/session_id +client.execute_code("print(x)") # Variables persist (same session) +client.upload_file("data.csv", content) +result = client.execute_code("import pandas; df = pd.read_csv('data.csv')") + +client.stop() # Cleanup +``` + +**Without Stateful Client (using UnifiedClient):** +```python +# Customer must manually track and pass state everywhere +session = client.start_code_interpreter_session(codeInterpreterIdentifier="aws.codeinterpreter.v1") +identifier = session['codeInterpreterIdentifier'] +session_id = session['sessionId'] + +# Every call requires passing state +client.invoke_code_interpreter( + codeInterpreterIdentifier=identifier, # Must pass + sessionId=session_id, # Must pass + method="execute", + parameters={"code": "x = 5"} +) + +client.invoke_code_interpreter( + codeInterpreterIdentifier=identifier, # Must pass again + sessionId=session_id, # Must pass again + method="execute", + parameters={"code": "print(x)"} +) + +# Customer must remember to cleanup +client.stop_code_interpreter_session( + codeInterpreterIdentifier=identifier, + sessionId=session_id +) +``` + +### Key Features That Cannot Be Replaced + +**CodeInterpreter:** +1. **Session state management** - Automatic tracking of identifier/session_id +2. **Auto-start logic** - Automatically starts session if not active +3. **File I/O helpers** - Base64 encoding/decoding, file metadata tracking +4. **Package management** - `install_packages()` constructs pip install commands +5. **Context management** - `with code_session() as client:` for automatic cleanup +6. **Execution helpers** - `execute_code()`, `execute_command()` with language parameter handling + +**BrowserClient:** +1. **Session state management** - Automatic tracking of identifier/session_id +2. **WebSocket authentication** - `generate_ws_headers()` with SigV4 signing (~50 lines of complex logic) +3. **Presigned URLs** - `generate_live_view_url()` with SigV4QueryAuth (~40 lines) +4. **Stream control** - `take_control()` / `release_control()` helpers +5. **Context management** - `with browser_session() as client:` for automatic cleanup + +### Value Proposition + +These clients save customers **hundreds of lines of boilerplate** per application: +- ~150+ lines for session state management +- ~100+ lines for file I/O with encoding (CodeInterpreter) +- ~90+ lines for WebSocket SigV4 signing (BrowserClient) +- ~40+ lines for presigned URL generation (BrowserClient) + +**Recommendation: Continue using `CodeInterpreter` and `BrowserClient` as-is. No migration needed.** + +--- + +## MemoryClient Migration + +### Control Plane Operations + +#### `create_memory()` + +**Before (MemoryClient):** +```python +from bedrock_agentcore.memory import MemoryClient + +client = MemoryClient(region_name="us-west-2") +memory = client.create_memory( + name="my-memory", + strategies=[{"SEMANTIC": {"name": "facts", "namespaces": ["app/{actorId}"]}}], + event_expiry_duration=90 +) +``` + +**After (UnifiedBedrockAgentCoreClient):** +```python +from bedrock_agentcore import UnifiedBedrockAgentCoreClient + +client = UnifiedBedrockAgentCoreClient(region_name="us-west-2") +memory = client.create_memory( + name="my-memory", + memoryStrategies=[{"SEMANTIC": {"name": "facts", "namespaces": ["app/{actorId}"]}}], + eventExpiryDuration=90 +) +``` + +**Migration Notes:** +- Change parameter name: `strategies` → `memoryStrategies` +- Change parameter name: `event_expiry_duration` → `eventExpiryDuration` +- Direct 1:1 replacement + +--- + +#### `create_memory_and_wait()` + +**Before (MemoryClient):** +```python +memory = client.create_memory_and_wait( + name="my-memory", + strategies=[{"SEMANTIC": {"name": "facts", "namespaces": ["app/{actorId}"]}}], + max_wait=300 +) +``` + +**After (UnifiedBedrockAgentCoreClient + Helper):** +```python +from bedrock_agentcore import UnifiedBedrockAgentCoreClient +import time + +client = UnifiedBedrockAgentCoreClient(region_name="us-west-2") + +# Create memory +memory = client.create_memory( + name="my-memory", + memoryStrategies=[{"SEMANTIC": {"name": "facts", "namespaces": ["app/{actorId}"]}}] +) +memory_id = memory['memory']['id'] + +# Poll for ACTIVE status +def wait_for_active(client, memory_id, max_wait=300): + start = time.time() + while time.time() - start < max_wait: + resp = client.get_memory(memoryId=memory_id) + status = resp['memory']['status'] + if status == 'ACTIVE': + return resp + elif status == 'FAILED': + raise RuntimeError(f"Memory failed: {resp['memory'].get('failureReason')}") + time.sleep(10) + raise TimeoutError("Memory not ACTIVE in time") + +memory = wait_for_active(client, memory_id, max_wait=300) +``` + +**Migration Notes:** +- Need to implement polling logic yourself (~50 lines) +- Consider creating a reusable helper function +- Check for both ACTIVE and FAILED states + +--- + +#### `get_memory()` + +**Before (MemoryClient):** +```python +memory = client.get_memory(memory_id="mem-123") +``` + +**After (UnifiedBedrockAgentCoreClient):** +```python +memory = client.get_memory(memoryId="mem-123") +``` + +**Migration Notes:** +- Change parameter name: `memory_id` → `memoryId` +- Direct 1:1 replacement + +--- + +#### `list_memories()` + +**Before (MemoryClient):** +```python +memories = client.list_memories(max_results=10, next_token=None) +``` + +**After (UnifiedBedrockAgentCoreClient):** +```python +memories = client.list_memories(maxResults=10, nextToken=None) +``` + +**Migration Notes:** +- Change parameter name: `max_results` → `maxResults` +- Change parameter name: `next_token` → `nextToken` +- Direct 1:1 replacement + +--- + +#### `update_memory()` + +**Before (MemoryClient):** +```python +updated = client.update_memory( + memory_id="mem-123", + strategies_to_add=[{"SUMMARY": {"name": "summaries"}}], + strategies_to_modify=[...], + strategies_to_delete=["old-strategy"] +) +``` + +**After (UnifiedBedrockAgentCoreClient):** +```python +updated = client.update_memory( + memoryId="mem-123", + memoryStrategiesToAdd=[{"SUMMARY": {"name": "summaries"}}], + memoryStrategiesToModify=[...], + memoryStrategiesToDelete=["old-strategy"] +) +``` + +**Migration Notes:** +- Change parameter names to camelCase +- Direct 1:1 replacement + +--- + +#### `delete_memory()` + +**Before (MemoryClient):** +```python +client.delete_memory(memory_id="mem-123") +``` + +**After (UnifiedBedrockAgentCoreClient):** +```python +client.delete_memory(memoryId="mem-123") +``` + +**Migration Notes:** +- Change parameter name: `memory_id` → `memoryId` +- Direct 1:1 replacement + +--- + +#### `delete_memory_and_wait()` + +**Before (MemoryClient):** +```python +client.delete_memory_and_wait(memory_id="mem-123", max_wait=300) +``` + +**After (UnifiedBedrockAgentCoreClient + Helper):** +```python +client.delete_memory(memoryId="mem-123") + +# Poll to confirm deletion +def wait_for_deletion(client, memory_id, max_wait=300): + import time + from botocore.exceptions import ClientError + + start = time.time() + while time.time() - start < max_wait: + try: + client.get_memory(memoryId=memory_id) + time.sleep(5) + except ClientError as e: + if e.response['Error']['Code'] == 'ResourceNotFoundException': + return # Successfully deleted + raise + raise TimeoutError("Memory not deleted in time") + +wait_for_deletion(client, "mem-123", max_wait=300) +``` + +**Migration Notes:** +- Need to implement deletion polling yourself +- Check for ResourceNotFoundException +- Consider creating a reusable helper function + +--- + +#### `create_or_get_memory()` + +**Before (MemoryClient):** +```python +memory = client.create_or_get_memory( + name="my-memory", + strategies=[{"SEMANTIC": {"name": "facts", "namespaces": ["app/{actorId}"]}}] +) +``` + +**After (UnifiedBedrockAgentCoreClient + Logic):** +```python +from botocore.exceptions import ClientError + +def create_or_get_memory(client, name, strategies): + try: + # Try to create + return client.create_memory(name=name, memoryStrategies=strategies) + except ClientError as e: + if e.response['Error']['Code'] == 'ConflictException': + # Already exists, get it + memories = client.list_memories() + for mem in memories.get('memories', []): + if mem['name'] == name: + return client.get_memory(memoryId=mem['id']) + raise + +memory = create_or_get_memory( + client, + name="my-memory", + strategies=[{"SEMANTIC": {"name": "facts", "namespaces": ["app/{actorId}"]}}] +) +``` + +**Migration Notes:** +- Need to implement error handling logic +- Handle ConflictException for existing resources +- Consider creating a reusable helper function + +--- + +### Data Plane Operations + +#### `create_event()` + +**Before (MemoryClient):** +```python +event = client.create_event( + memory_id="mem-123", + actor_id="user-1", + session_id="sess-1", + payload=[{"conversational": {"content": {"text": "Hello"}, "role": "USER"}}] +) +``` + +**After (MemorySessionManager - RECOMMENDED):** +```python +from bedrock_agentcore.memory import MemorySessionManager +from bedrock_agentcore.memory.constants import ConversationalMessage, MessageRole + +manager = MemorySessionManager(memory_id="mem-123", region_name="us-west-2") +event = manager.add_turns( + actor_id="user-1", + session_id="sess-1", + messages=[ConversationalMessage("Hello", MessageRole.USER)] +) +``` + +**Alternative (UnifiedBedrockAgentCoreClient - Raw):** +```python +from datetime import datetime, timezone + +event = client.create_event( + memoryId="mem-123", + actorId="user-1", + sessionId="sess-1", + eventTimestamp=datetime.now(timezone.utc), + payload=[{"conversational": {"content": {"text": "Hello"}, "role": "USER"}}] +) +``` + +**Migration Notes:** +- **Prefer MemorySessionManager** for cleaner API with dataclasses +- UnifiedClient requires manual timestamp and payload construction +- MemorySessionManager handles memory_id injection automatically + +--- + +#### `get_event()` + +**Before (MemoryClient):** +```python +event = client.get_event(memory_id="mem-123", event_id="evt-456") +``` + +**After (MemorySessionManager - RECOMMENDED):** +```python +manager = MemorySessionManager(memory_id="mem-123", region_name="us-west-2") +event = manager.get_event(eventId="evt-456") +``` + +**Alternative (UnifiedBedrockAgentCoreClient):** +```python +event = client.get_event(memoryId="mem-123", eventId="evt-456") +``` + +**Migration Notes:** +- MemorySessionManager auto-injects memory_id +- UnifiedClient requires explicit memory_id + +--- + +#### `list_events()` + +**Before (MemoryClient):** +```python +events = client.list_events( + memory_id="mem-123", + actor_id="user-1", + session_id="sess-1", + max_results=50 +) +``` + +**After (MemorySessionManager - RECOMMENDED):** +```python +manager = MemorySessionManager(memory_id="mem-123", region_name="us-west-2") +events = manager.list_events( + actorId="user-1", + sessionId="sess-1", + maxResults=50 +) +``` + +**Alternative (UnifiedBedrockAgentCoreClient):** +```python +events = client.list_events( + memoryId="mem-123", + actorId="user-1", + sessionId="sess-1", + maxResults=50 +) +``` + +**Migration Notes:** +- MemorySessionManager returns typed Event objects +- MemorySessionManager handles pagination automatically + +--- + +#### `delete_event()` + +**Before (MemoryClient):** +```python +client.delete_event(memory_id="mem-123", event_id="evt-456") +``` + +**After (MemorySessionManager - RECOMMENDED):** +```python +manager = MemorySessionManager(memory_id="mem-123", region_name="us-west-2") +manager.delete_event(eventId="evt-456") +``` + +**Alternative (UnifiedBedrockAgentCoreClient):** +```python +client.delete_event(memoryId="mem-123", eventId="evt-456") +``` + +--- + +#### `retrieve_memory_records()` / `retrieve_memories()` + +**Before (MemoryClient):** +```python +records = client.retrieve_memories( + memory_id="mem-123", + namespace="app/user-1/sess-1", + query="search query", + top_k=5 +) +``` + +**After (MemorySessionManager - RECOMMENDED):** +```python +from bedrock_agentcore.memory.constants import RetrievalConfig + +manager = MemorySessionManager(memory_id="mem-123", region_name="us-west-2") +records = manager.search_long_term_memories( + searchQuery="search query", + namespace="app/user-1/sess-1", + topK=5 +) +``` + +**Alternative (UnifiedBedrockAgentCoreClient):** +```python +records = client.retrieve_memory_records( + memoryId="mem-123", + namespace="app/user-1/sess-1", + searchCriteria={"searchQuery": "search query", "topK": 5} +) +``` + +**Migration Notes:** +- MemorySessionManager has better method naming (`search_long_term_memories`) +- MemorySessionManager returns typed MemoryRecord objects +- UnifiedClient requires searchCriteria dict + +--- + +#### `get_memory_record()` + +**Before (MemoryClient):** +```python +record = client.get_memory_record( + memory_id="mem-123", + memory_record_id="rec-456" +) +``` + +**After (MemorySessionManager - RECOMMENDED):** +```python +manager = MemorySessionManager(memory_id="mem-123", region_name="us-west-2") +record = manager.get_memory_record(memoryRecordId="rec-456") +``` + +**Alternative (UnifiedBedrockAgentCoreClient):** +```python +record = client.get_memory_record(memoryId="mem-123", memoryRecordId="rec-456") +``` + +--- + +#### `list_memory_records()` + +**Before (MemoryClient):** +```python +records = client.list_memory_records( + memory_id="mem-123", + namespace="app/user-1" +) +``` + +**After (MemorySessionManager - RECOMMENDED):** +```python +manager = MemorySessionManager(memory_id="mem-123", region_name="us-west-2") +records = manager.list_long_term_memory_records(namespace="app/user-1") +``` + +**Alternative (UnifiedBedrockAgentCoreClient):** +```python +records = client.list_memory_records(memoryId="mem-123", namespace="app/user-1") +``` + +**Migration Notes:** +- MemorySessionManager handles pagination automatically +- MemorySessionManager returns typed MemoryRecord objects + +--- + +#### `delete_memory_record()` + +**Before (MemoryClient):** +```python +client.delete_memory_record( + memory_id="mem-123", + memory_record_id="rec-456" +) +``` + +**After (MemorySessionManager - RECOMMENDED):** +```python +manager = MemorySessionManager(memory_id="mem-123", region_name="us-west-2") +manager.delete_memory_record(memoryRecordId="rec-456") +``` + +**Alternative (UnifiedBedrockAgentCoreClient):** +```python +client.delete_memory_record(memoryId="mem-123", memoryRecordId="rec-456") +``` + +--- + +### Strategy Management Operations + +#### `add_semantic_strategy()` + +**Before (MemoryClient):** +```python +updated = client.add_semantic_strategy( + memory_id="mem-123", + strategy_name="facts", + namespaces=["app/{actorId}/{sessionId}"] +) +``` + +**After (UnifiedBedrockAgentCoreClient):** +```python +strategy = { + "SEMANTIC": { + "name": "facts", + "namespaces": ["app/{actorId}/{sessionId}"] + } +} +updated = client.update_memory( + memoryId="mem-123", + memoryStrategiesToAdd=[strategy] +) +``` + +**Migration Notes:** +- Need to construct strategy dict manually +- Use update_memory with memoryStrategiesToAdd + +--- + +#### `add_semantic_strategy_and_wait()` + +**Before (MemoryClient):** +```python +updated = client.add_semantic_strategy_and_wait( + memory_id="mem-123", + strategy_name="facts", + namespaces=["app/{actorId}/{sessionId}"], + max_wait=300 +) +``` + +**After (UnifiedBedrockAgentCoreClient + Helper):** +```python +strategy = { + "SEMANTIC": { + "name": "facts", + "namespaces": ["app/{actorId}/{sessionId}"] + } +} +client.update_memory( + memoryId="mem-123", + memoryStrategiesToAdd=[strategy] +) + +# Use wait_for_active helper from earlier +wait_for_active(client, "mem-123", max_wait=300) +``` + +**Migration Notes:** +- Combine update_memory with polling helper +- Check for ACTIVE status after strategy addition + +--- + +#### `add_summary_strategy()` + +**Before (MemoryClient):** +```python +updated = client.add_summary_strategy( + memory_id="mem-123", + strategy_name="summaries" +) +``` + +**After (UnifiedBedrockAgentCoreClient):** +```python +strategy = { + "SUMMARY": { + "name": "summaries" + } +} +updated = client.update_memory( + memoryId="mem-123", + memoryStrategiesToAdd=[strategy] +) +``` + +--- + +#### `add_summary_strategy_and_wait()` + +**Before (MemoryClient):** +```python +updated = client.add_summary_strategy_and_wait( + memory_id="mem-123", + strategy_name="summaries", + max_wait=300 +) +``` + +**After (UnifiedBedrockAgentCoreClient + Helper):** +```python +strategy = { + "SUMMARY": { + "name": "summaries" + } +} +client.update_memory( + memoryId="mem-123", + memoryStrategiesToAdd=[strategy] +) +wait_for_active(client, "mem-123", max_wait=300) +``` + +--- + +#### `add_user_preference_strategy()` + +**Before (MemoryClient):** +```python +updated = client.add_user_preference_strategy( + memory_id="mem-123", + strategy_name="preferences" +) +``` + +**After (UnifiedBedrockAgentCoreClient):** +```python +strategy = { + "USER_PREFERENCE": { + "name": "preferences" + } +} +updated = client.update_memory( + memoryId="mem-123", + memoryStrategiesToAdd=[strategy] +) +``` + +--- + +#### `add_user_preference_strategy_and_wait()` + +**Before (MemoryClient):** +```python +updated = client.add_user_preference_strategy_and_wait( + memory_id="mem-123", + strategy_name="preferences", + max_wait=300 +) +``` + +**After (UnifiedBedrockAgentCoreClient + Helper):** +```python +strategy = { + "USER_PREFERENCE": { + "name": "preferences" + } +} +client.update_memory( + memoryId="mem-123", + memoryStrategiesToAdd=[strategy] +) +wait_for_active(client, "mem-123", max_wait=300) +``` + +--- + +#### `modify_strategy()` + +**Before (MemoryClient):** +```python +updated = client.modify_strategy( + memory_id="mem-123", + strategy_name="facts", + new_config={"namespaces": ["new/namespace"]} +) +``` + +**After (UnifiedBedrockAgentCoreClient):** +```python +strategy_to_modify = { + "SEMANTIC": { + "name": "facts", + "namespaces": ["new/namespace"] + } +} +updated = client.update_memory( + memoryId="mem-123", + memoryStrategiesToModify=[strategy_to_modify] +) +``` + +--- + +#### `delete_strategy()` + +**Before (MemoryClient):** +```python +updated = client.delete_strategy( + memory_id="mem-123", + strategy_name="facts" +) +``` + +**After (UnifiedBedrockAgentCoreClient):** +```python +updated = client.update_memory( + memoryId="mem-123", + memoryStrategiesToDelete=["facts"] +) +``` + +--- + +#### `update_memory_strategies()` + +**Before (MemoryClient):** +```python +updated = client.update_memory_strategies( + memory_id="mem-123", + strategies_to_add=[{"SEMANTIC": {"name": "new-facts", "namespaces": ["app"]}}], + strategies_to_modify=[{"SUMMARY": {"name": "summaries"}}], + strategies_to_delete=["old-strategy"] +) +``` + +**After (UnifiedBedrockAgentCoreClient):** +```python +updated = client.update_memory( + memoryId="mem-123", + memoryStrategiesToAdd=[{"SEMANTIC": {"name": "new-facts", "namespaces": ["app"]}}], + memoryStrategiesToModify=[{"SUMMARY": {"name": "summaries"}}], + memoryStrategiesToDelete=["old-strategy"] +) +``` + +**Migration Notes:** +- Direct 1:1 replacement with parameter name changes + +--- + +#### `update_memory_strategies_and_wait()` + +**Before (MemoryClient):** +```python +updated = client.update_memory_strategies_and_wait( + memory_id="mem-123", + strategies_to_add=[{"SEMANTIC": {"name": "new-facts", "namespaces": ["app"]}}], + max_wait=300 +) +``` + +**After (UnifiedBedrockAgentCoreClient + Helper):** +```python +updated = client.update_memory( + memoryId="mem-123", + memoryStrategiesToAdd=[{"SEMANTIC": {"name": "new-facts", "namespaces": ["app"]}}] +) +wait_for_active(client, "mem-123", max_wait=300) +``` + +--- + +### Conversation Helper Operations + +#### `save_conversation()` + +**Before (MemoryClient):** +```python +event = client.save_conversation( + memory_id="mem-123", + actor_id="user-1", + session_id="sess-1", + messages=[ + ("What's the weather?", "USER"), + ("It's sunny!", "ASSISTANT") + ] +) +``` + +**After (MemorySessionManager - RECOMMENDED):** +```python +from bedrock_agentcore.memory import MemorySessionManager +from bedrock_agentcore.memory.constants import ConversationalMessage, MessageRole + +manager = MemorySessionManager(memory_id="mem-123", region_name="us-west-2") +event = manager.add_turns( + actor_id="user-1", + session_id="sess-1", + messages=[ + ConversationalMessage("What's the weather?", MessageRole.USER), + ConversationalMessage("It's sunny!", MessageRole.ASSISTANT) + ] +) +``` + +**Alternative (UnifiedBedrockAgentCoreClient - NOT RECOMMENDED):** +```python +from datetime import datetime, timezone + +payload = [] +for text, role in messages: + payload.append({ + "conversational": { + "content": {"text": text}, + "role": role.upper() + } + }) + +event = client.create_event( + memoryId="mem-123", + actorId="user-1", + sessionId="sess-1", + eventTimestamp=datetime.now(timezone.utc), + payload=payload +) +``` + +**Migration Notes:** +- **Strongly prefer MemorySessionManager** for this operation +- MemorySessionManager uses typed dataclasses instead of tuples +- UnifiedClient requires ~15 lines of manual payload construction + +--- + +#### `save_turn()` + +**Before (MemoryClient):** +```python +event = client.save_turn( + memory_id="mem-123", + actor_id="user-1", + session_id="sess-1", + user_message="What's the weather?", + assistant_message="It's sunny!" +) +``` + +**After (MemorySessionManager - RECOMMENDED):** +```python +from bedrock_agentcore.memory import MemorySessionManager +from bedrock_agentcore.memory.constants import ConversationalMessage, MessageRole + +manager = MemorySessionManager(memory_id="mem-123", region_name="us-west-2") +event = manager.add_turns( + actor_id="user-1", + session_id="sess-1", + messages=[ + ConversationalMessage("What's the weather?", MessageRole.USER), + ConversationalMessage("It's sunny!", MessageRole.ASSISTANT) + ] +) +``` + +**Migration Notes:** +- Use add_turns with two messages +- More flexible than save_turn (supports multiple turns) + +--- + +#### `process_turn_with_llm()` + +**Before (MemoryClient):** +```python +memories, response, event = client.process_turn_with_llm( + memory_id="mem-123", + actor_id="user-1", + session_id="sess-1", + user_input="What's the weather?", + llm_callback=my_llm_function, + retrieval_namespace="app/weather", + top_k=3 +) +``` + +**After (MemorySessionManager - RECOMMENDED):** +```python +from bedrock_agentcore.memory import MemorySessionManager +from bedrock_agentcore.memory.constants import RetrievalConfig + +manager = MemorySessionManager(memory_id="mem-123", region_name="us-west-2") +memories, response, event = manager.process_turn_with_llm( + actor_id="user-1", + session_id="sess-1", + user_input="What's the weather?", + llm_callback=my_llm_function, + retrieval_config={ + "app/weather": RetrievalConfig(top_k=3, relevance_score=0.5) + } +) + +# BONUS: Async version! +memories, response, event = await manager.process_turn_with_llm_async( + actor_id="user-1", + session_id="sess-1", + user_input="What's the weather?", + llm_callback=my_async_llm_function, + retrieval_config={ + "app/weather": RetrievalConfig(top_k=3) + } +) +``` + +**Alternative (UnifiedBedrockAgentCoreClient - NOT RECOMMENDED):** + +Not practical - requires ~30+ lines to implement: +1. Call retrieve_memory_records +2. Filter by relevance_score +3. Invoke LLM callback manually +4. Format response messages +5. Call create_event + +**Migration Notes:** +- **Must use MemorySessionManager** for this operation +- MemorySessionManager supports multi-namespace retrieval +- MemorySessionManager provides async version +- UnifiedClient requires complete re-implementation + +--- + +#### `get_last_k_turns()` + +**Before (MemoryClient):** +```python +turns = client.get_last_k_turns( + memory_id="mem-123", + actor_id="user-1", + session_id="sess-1", + k=5 +) +``` + +**After (MemorySessionManager - RECOMMENDED):** +```python +manager = MemorySessionManager(memory_id="mem-123", region_name="us-west-2") +turns = manager.get_last_k_turns( + actor_id="user-1", + session_id="sess-1", + k=5 +) +``` + +**Alternative (UnifiedBedrockAgentCoreClient - NOT RECOMMENDED):** + +Requires ~40 lines of implementation: +```python +# 1. List events +events = client.list_events(memoryId="mem-123", actorId="user-1", sessionId="sess-1") + +# 2. Group messages into turns (complex logic) +turns = [] +current_turn = [] +for event in reversed(events.get('events', [])): + for payload_item in event['payload']: + if 'conversational' in payload_item: + role = payload_item['conversational']['role'] + if role == 'USER' and current_turn: + turns.append(current_turn) + current_turn = [] + if len(turns) >= k: + break + current_turn.append(payload_item['conversational']) + if len(turns) >= k: + break + +# ... more complex logic +``` + +**Migration Notes:** +- **Must use MemorySessionManager** for this operation +- UnifiedClient requires complete re-implementation (~40 lines) + +--- + +#### `list_branches()` + +**Before (MemoryClient):** +```python +branches = client.list_branches( + memory_id="mem-123", + actor_id="user-1", + session_id="sess-1" +) +``` + +**After (MemorySessionManager - RECOMMENDED):** +```python +manager = MemorySessionManager(memory_id="mem-123", region_name="us-west-2") +branches = manager.list_branches( + actor_id="user-1", + session_id="sess-1" +) +``` + +**Alternative (UnifiedBedrockAgentCoreClient - NOT RECOMMENDED):** + +Requires ~60 lines of tree-building logic. + +**Migration Notes:** +- **Must use MemorySessionManager** for this operation +- UnifiedClient requires complete re-implementation + +--- + +#### `get_conversation_tree()` + +**Before (MemoryClient):** +```python +tree = client.get_conversation_tree( + memory_id="mem-123", + actor_id="user-1", + session_id="sess-1" +) +``` + +**After:** + +**NOT AVAILABLE** - This method is unique to MemoryClient and has no replacement. + +**Workaround (Complex):** +```python +# 1. Use list_branches from MemorySessionManager +manager = MemorySessionManager(memory_id="mem-123", region_name="us-west-2") +branches = manager.list_branches(actor_id="user-1", session_id="sess-1") + +# 2. Implement tree construction yourself (~80 lines) +# This is complex and requires understanding of the event/branch structure +``` + +**Migration Notes:** +- This is the ONLY method with no direct replacement +- Consider using list_branches() instead if tree structure is not critical +- May need to implement custom tree-building logic if required + +--- + +#### `fork_conversation()` + +**Before (MemoryClient):** +```python +forked_event = client.fork_conversation( + memory_id="mem-123", + actor_id="user-1", + session_id="sess-1", + parent_event_id="evt-456", + new_branch_id="branch-789" +) +``` + +**After (MemorySessionManager - RECOMMENDED):** +```python +manager = MemorySessionManager(memory_id="mem-123", region_name="us-west-2") +forked_event = manager.fork_conversation( + actor_id="user-1", + session_id="sess-1", + parentEventId="evt-456", + branchId="branch-789" +) +``` + +**Alternative (UnifiedBedrockAgentCoreClient):** +```python +from datetime import datetime, timezone + +forked_event = client.create_event( + memoryId="mem-123", + actorId="user-1", + sessionId="sess-1", + eventTimestamp=datetime.now(timezone.utc), + parentEventId="evt-456", + branchId="branch-789", + payload=[] +) +``` + +**Migration Notes:** +- MemorySessionManager provides helper method +- UnifiedClient requires manual event creation + +--- + +#### `wait_for_memories()` + +**Before (MemoryClient):** +```python +client.wait_for_memories( + memory_id="mem-123", + actor_id="user-1", + session_id="sess-1", + event_id="evt-456", + max_wait=60 +) +``` + +**After (UnifiedBedrockAgentCoreClient + Helper):** +```python +def wait_for_memory_extraction(client, memory_id, event_id, max_wait=60): + import time + start = time.time() + + while time.time() - start < max_wait: + event = client.get_event(memoryId=memory_id, eventId=event_id) + + # Check if memory records have been extracted + records = client.list_memory_records(memoryId=memory_id) + # Logic to check if extraction is complete... + + time.sleep(5) + + raise TimeoutError("Memory extraction not complete") + +wait_for_memory_extraction(client, "mem-123", "evt-456", max_wait=60) +``` + +**Migration Notes:** +- Need to implement custom polling logic +- Check memory records to verify extraction completion + +--- + +## IdentityClient Migration + +### Workload Identity Operations + +#### `create_workload_identity()` + +**Before (IdentityClient):** +```python +from bedrock_agentcore.services import IdentityClient + +client = IdentityClient(region="us-west-2") +identity = client.create_workload_identity( + name="my-identity", + allowed_resource_oauth_2_return_urls=["https://app.example.com/callback"] +) +``` + +**After (UnifiedBedrockAgentCoreClient):** +```python +from bedrock_agentcore import UnifiedBedrockAgentCoreClient + +client = UnifiedBedrockAgentCoreClient(region_name="us-west-2") +identity = client.create_workload_identity( + name="my-identity", + allowedResourceOAuth2ReturnUrls=["https://app.example.com/callback"] +) +``` + +**Migration Notes:** +- Change parameter name: `allowed_resource_oauth_2_return_urls` → `allowedResourceOAuth2ReturnUrls` +- Direct 1:1 replacement + +--- + +#### `get_workload_identity()` + +**Before (IdentityClient):** +```python +identity = client.get_workload_identity(name="my-identity") +``` + +**After (UnifiedBedrockAgentCoreClient):** +```python +identity = client.get_workload_identity(name="my-identity") +``` + +**Migration Notes:** +- Identical API - no changes needed + +--- + +#### `update_workload_identity()` + +**Before (IdentityClient):** +```python +identity = client.update_workload_identity( + name="my-identity", + allowed_resource_oauth_2_return_urls=[ + "https://app.example.com/callback", + "https://staging.example.com/callback" + ] +) +``` + +**After (UnifiedBedrockAgentCoreClient):** +```python +identity = client.update_workload_identity( + name="my-identity", + allowedResourceOAuth2ReturnUrls=[ + "https://app.example.com/callback", + "https://staging.example.com/callback" + ] +) +``` + +**Migration Notes:** +- Change parameter name to camelCase +- Direct 1:1 replacement + +--- + +#### `delete_workload_identity()` + +**Before (IdentityClient):** +```python +client.delete_workload_identity(name="my-identity") +``` + +**After (UnifiedBedrockAgentCoreClient):** +```python +client.delete_workload_identity(name="my-identity") +``` + +**Migration Notes:** +- Identical API - no changes needed + +--- + +#### `list_workload_identities()` + +**Before (IdentityClient):** +```python +identities = client.list_workload_identities(max_results=10) +``` + +**After (UnifiedBedrockAgentCoreClient):** +```python +identities = client.list_workload_identities(maxResults=10) +``` + +**Migration Notes:** +- Change parameter name: `max_results` → `maxResults` + +--- + +### Credential Provider Operations + +#### `create_oauth2_credential_provider()` + +**Before (IdentityClient):** +```python +provider = client.create_oauth2_credential_provider({ + "name": "github-oauth", + "authorizationUrl": "https://github.com/login/oauth/authorize", + "clientId": "my-client-id", + "clientSecretArn": "arn:aws:secretsmanager:...", + "oAuth2GrantType": "AUTHORIZATION_CODE", + "scopes": ["read:user", "repo"], + "tokenUrl": "https://github.com/login/oauth/access_token" +}) +``` + +**After (UnifiedBedrockAgentCoreClient):** +```python +provider = client.create_oauth2_credential_provider( + name="github-oauth", + authorizationUrl="https://github.com/login/oauth/authorize", + clientId="my-client-id", + clientSecretArn="arn:aws:secretsmanager:...", + oAuth2GrantType="AUTHORIZATION_CODE", + scopes=["read:user", "repo"], + tokenUrl="https://github.com/login/oauth/access_token" +) +``` + +**Migration Notes:** +- Change from dict parameter to keyword arguments +- Direct 1:1 replacement + +--- + +#### `create_api_key_credential_provider()` + +**Before (IdentityClient):** +```python +provider = client.create_api_key_credential_provider({ + "name": "external-api", + "apiKeySecretArn": "arn:aws:secretsmanager:..." +}) +``` + +**After (UnifiedBedrockAgentCoreClient):** +```python +provider = client.create_api_key_credential_provider( + name="external-api", + apiKeySecretArn="arn:aws:secretsmanager:..." +) +``` + +**Migration Notes:** +- Change from dict parameter to keyword arguments +- Direct 1:1 replacement + +--- + +#### `get_credential_provider()` + +**Before (IdentityClient):** +```python +provider = client.get_credential_provider(name="github-oauth") +``` + +**After (UnifiedBedrockAgentCoreClient):** +```python +provider = client.get_credential_provider(name="github-oauth") +``` + +**Migration Notes:** +- Identical API - no changes needed + +--- + +#### `list_credential_providers()` + +**Before (IdentityClient):** +```python +providers = client.list_credential_providers(max_results=10) +``` + +**After (UnifiedBedrockAgentCoreClient):** +```python +providers = client.list_credential_providers(maxResults=10) +``` + +**Migration Notes:** +- Change parameter name: `max_results` → `maxResults` + +--- + +#### `delete_credential_provider()` + +**Before (IdentityClient):** +```python +client.delete_credential_provider(name="github-oauth") +``` + +**After (UnifiedBedrockAgentCoreClient):** +```python +client.delete_credential_provider(name="github-oauth") +``` + +**Migration Notes:** +- Identical API - no changes needed + +--- + +### Token Operations + +#### `get_workload_access_token()` + +**Before (IdentityClient):** +```python +token = client.get_workload_access_token( + workload_name="my-identity", + user_id="user-123" +) +``` + +**After (UnifiedBedrockAgentCoreClient):** +```python +token = client.get_workload_access_token( + workloadName="my-identity", + userId="user-123" +) +``` + +**Migration Notes:** +- Change parameter names to camelCase +- Direct 1:1 replacement + +--- + +#### `get_api_key()` + +**Before (IdentityClient):** +```python +from bedrock_agentcore.services import IdentityClient + +client = IdentityClient(region="us-west-2") + +# Get API key with async support +api_key = await client.get_api_key( + provider_name="my-provider", + agent_identity_token=token +) +``` + +**After (Recommended - Use decorator):** +```python +from bedrock_agentcore.identity.auth import requires_api_key + +# API key with decorator +@requires_api_key( + provider_name="my-api-provider", + into="api_key" +) +def call_api(*, api_key: str): + # API key is automatically injected + print(f"Got key: {api_key}") +``` + +**Alternative (For imperative use cases - UnifiedBedrockAgentCoreClient):** +```python +from bedrock_agentcore import UnifiedBedrockAgentCoreClient + +client = UnifiedBedrockAgentCoreClient(region_name="us-west-2") + +# Use Case: Token caching, instance variables, dynamic providers, etc. +api_key = client.get_resource_api_key( + resourceCredentialProviderName="my-provider", + workloadIdentityToken="" +)["apiKey"] +``` + +**Migration Notes:** +- **For most use cases**: Use `@requires_api_key` decorator (simpler, cleaner) +- **For imperative use cases** (caching, instance variables, dynamic providers): Use `UnifiedBedrockAgentCoreClient.get_resource_api_key()` +- Both approaches fully supported - choose based on your needs + +--- + +#### `get_token()` + +**Before (IdentityClient):** +```python +from bedrock_agentcore.services import IdentityClient + +client = IdentityClient(region="us-west-2") + +# OAuth flow with automatic polling +token = await client.get_token( + workload_name="my-identity", + user_token="oauth-token", + on_auth_url=lambda url: print(f"Visit: {url}") +) +``` + +**After (Recommended - Use decorator):** +```python +from bedrock_agentcore.identity.auth import requires_access_token + +# OAuth flow with decorator +@requires_access_token( + provider_name="my-oauth-provider", + into="access_token", + scopes=["read:user"], + auth_flow="USER_FEDERATION", + on_auth_url=lambda url: print(f"Visit: {url}") +) +def my_function(*, access_token: str): + # Token is automatically injected + print(f"Got token: {access_token}") +``` + +**Migration Notes:** +- **Preferred approach**: Use `@requires_access_token` decorator from `bedrock_agentcore.identity.auth` +- Decorator handles OAuth flow and token polling automatically +- Works with both sync and async functions + +**⚠️ Important:** Decorators work by injecting tokens as function parameters (declarative approach). For imperative use cases (token reuse, caching, instance variables, dynamic providers, background workers, etc.), see [Appendix: When OAuth Decorators Are NOT Sufficient](#appendix-when-oauth-decorators-are-not-sufficient) for details on migration options + +--- + +## Summary Tables + +### MemoryClient Migration Difficulty + +| Operation Type | Method Count | Migration Difficulty | Recommended Approach | +|---------------|--------------|---------------------|---------------------| +| **Control Plane (Simple)** | 5 | Easy | UnifiedBedrockAgentCoreClient | +| **Control Plane (With Polling)** | 3 | Medium | UnifiedClient + Helper Functions | +| **Data Plane (Simple)** | 6 | Easy | MemorySessionManager (preferred) or UnifiedClient | +| **Data Plane (Conversations)** | 5 | Easy | MemorySessionManager (MUST USE) | +| **Strategy Management** | 11 | Medium | UnifiedClient + Manual Strategy Construction | +| **Advanced Helpers** | 5 | Hard | MemorySessionManager (MUST USE) | + +### IdentityClient Migration Difficulty + +| Operation Type | Method Count | Migration Difficulty | Recommended Approach | +|---------------|--------------|---------------------|---------------------| +| **Workload Identity (Simple)** | 5 | Easy | UnifiedBedrockAgentCoreClient | +| **Credential Providers** | 5 | Easy | UnifiedBedrockAgentCoreClient | +| **Token Operations (Simple)** | 1 | Easy | UnifiedBedrockAgentCoreClient | +| **OAuth Flow (get_token, get_api_key)** | 2 | Easy | **Use auth decorators** (`@requires_access_token`, `@requires_api_key`) | + +--- + +## Quick Reference: Helper Functions + +These helper functions ease migration for common patterns: + +```python +# helper_functions.py +from bedrock_agentcore import UnifiedBedrockAgentCoreClient +import time +from typing import Dict, Any + +def wait_for_memory_active( + client: UnifiedBedrockAgentCoreClient, + memory_id: str, + max_wait: int = 300, + poll_interval: int = 10 +) -> Dict[str, Any]: + """Poll until memory is ACTIVE.""" + start_time = time.time() + while time.time() - start_time < max_wait: + response = client.get_memory(memoryId=memory_id) + status = response['memory']['status'] + if status == 'ACTIVE': + return response + elif status == 'FAILED': + reason = response['memory'].get('failureReason', 'Unknown') + raise RuntimeError(f"Memory failed: {reason}") + time.sleep(poll_interval) + raise TimeoutError(f"Memory not ACTIVE within {max_wait}s") + +def create_memory_and_wait( + client: UnifiedBedrockAgentCoreClient, + name: str, + strategies: list, + max_wait: int = 300, + **kwargs +) -> Dict[str, Any]: + """Create memory and wait for ACTIVE status.""" + memory = client.create_memory( + name=name, + memoryStrategies=strategies, + **kwargs + ) + memory_id = memory['memory']['id'] + return wait_for_memory_active(client, memory_id, max_wait) + +def add_strategy_and_wait( + client: UnifiedBedrockAgentCoreClient, + memory_id: str, + strategy: Dict[str, Any], + max_wait: int = 300 +) -> Dict[str, Any]: + """Add strategy and wait for ACTIVE status.""" + client.update_memory( + memoryId=memory_id, + memoryStrategiesToAdd=[strategy] + ) + return wait_for_memory_active(client, memory_id, max_wait) +``` + +**Usage:** +```python +from bedrock_agentcore import UnifiedBedrockAgentCoreClient +from helper_functions import create_memory_and_wait, add_strategy_and_wait + +client = UnifiedBedrockAgentCoreClient(region_name="us-west-2") + +# Create and wait in one call +memory = create_memory_and_wait( + client, + name="my-memory", + strategies=[{"SEMANTIC": {"name": "facts", "namespaces": ["app/{actorId}"]}}] +) + +# Add strategy and wait +add_strategy_and_wait( + client, + memory_id=memory['memory']['id'], + strategy={"SUMMARY": {"name": "summaries"}} +) +``` + +--- + +## Appendix: When OAuth Decorators Are NOT Sufficient + +Decorators work by **injecting tokens as function parameters** (declarative approach). However, you need **imperative** `get_token()` calls when: + +### 1. Token Reuse Across Multiple Calls + +```python +# ❌ Decorator fetches token 3 times (inefficient) +@requires_access_token(provider_name="github", scopes=["read:user"]) +def get_user(): ... + +@requires_access_token(provider_name="github", scopes=["read:user"]) +def get_repos(): ... + +# ✅ Fetch once, reuse (need imperative approach) +token = await identity_client.get_token(provider_name="github", ...) +user = get_user_raw(token) +repos = get_repos_raw(token) +``` + +### 2. Storing Tokens in Instance Variables + +```python +class GitHubClient: + def __init__(self): + # Can't use decorator in __init__ + self.token = identity_client.get_token(provider_name="github", ...) + + def get_user(self): + return api_call(self.token) # Reuse instance token +``` + +### 3. Token Caching / Storage + +```python +# Store in cache/database for reuse +token = await identity_client.get_token(...) +cache.set(f"token:{user_id}", token, ttl=3600) +``` + +### 4. Dynamic Provider Selection + +```python +# Provider determined at runtime (can't hardcode in decorator) +provider = determine_provider(user) +token = await identity_client.get_token(provider_name=provider, ...) +``` + +### 5. Conditional Token Fetching + +```python +if requires_authentication: + token = await identity_client.get_token(...) + return call_with_auth(token) +else: + return call_without_auth() +``` + +### 6. Background Tasks / Workers + +```python +# No function decoration context +def worker(): + token = identity_client.get_token(...) + process_jobs(token) +``` + +### 7. Multiple Tokens for Different Users + +```python +for user in users: + # Can't parameterize decorator per iteration + token = identity_client.get_token(workload_name=user.workload, ...) + send_notification(user, token) +``` + +--- + +### ⚠️ Deprecation Impact + +Since `IdentityClient.get_token()` will be deprecated, **customers with these advanced use cases will lose functionality**. + +### Recommendations Before Deprecating + +**Option A: Don't deprecate `get_token()`** +- Keep this method available for advanced use cases where decorators don't work +- Decorators are great for 80% of use cases +- Imperative method needed for remaining 20% (token reuse, caching, dynamic providers) + +**Option B: Provide imperative helper function** + +If deprecating IdentityClient, add utility function: + +```python +# In bedrock_agentcore.identity.auth module +async def get_oauth_token(provider_name: str, scopes: List[str], ...) -> str: + """Get OAuth token imperatively (not decorator). + + Wraps get_resource_oauth2_token with polling logic. + """ + ... +``` + +**Option C: Accept limited functionality** +- Document that these advanced patterns are not supported after deprecation + +--- + +### Current Recommendation + +- **For most use cases**: Migrate to `@requires_access_token` decorator +- **For imperative use cases**: Migration path TBD - depends on deprecation decision above diff --git a/UNIFIED_CLIENT_README.md b/UNIFIED_CLIENT_README.md new file mode 100644 index 00000000..0bfbe11a --- /dev/null +++ b/UNIFIED_CLIENT_README.md @@ -0,0 +1,249 @@ +# UnifiedBedrockAgentCoreClient + +## What is this? + +A thin wrapper that lets you call **any** AWS Bedrock AgentCore operation without worrying about which service to use. + +## The Problem + +AWS Bedrock AgentCore has two services: +- `bedrock-agentcore-control` - Resource management (create/delete/update resources) +- `bedrock-agentcore` - Runtime operations (sessions, events, tokens) + +You used to need: +```python +import boto3 + +# Two clients, and you need to know which one for each operation +control = boto3.client("bedrock-agentcore-control", region_name="us-west-2") +data = boto3.client("bedrock-agentcore", region_name="us-west-2") + +# Which client do I use for GetWorkloadIdentity? 🤔 +identity = control.get_workload_identity(...) # Correct! + +# Which client for getting a token? 🤔 +token = data.get_workload_access_token(...) # Different client! +``` + +## The Solution + +```python +from bedrock_agentcore import UnifiedBedrockAgentCoreClient + +# One client for everything! +client = UnifiedBedrockAgentCoreClient(region_name="us-west-2") + +# Just call the operations - automatic routing! +identity = client.get_workload_identity(...) # ✨ Routed to control plane +token = client.get_workload_access_token(...) # ✨ Routed to data plane +``` + +## Quick Start + +```python +from bedrock_agentcore import UnifiedBedrockAgentCoreClient + +# Initialize once +client = UnifiedBedrockAgentCoreClient(region_name="us-west-2") + +# Use for any operation - the client handles routing +memory = client.create_memory(name="my-memory", ...) +event = client.create_event(memoryId="...", ...) +interpreter = client.create_code_interpreter(name="my-interp", ...) +session = client.start_code_interpreter_session(...) +identity = client.get_workload_identity(name="my-identity") +``` + +## Examples + +### GetWorkloadIdentity Example + +```python +from bedrock_agentcore import UnifiedBedrockAgentCoreClient + +client = UnifiedBedrockAgentCoreClient(region_name="us-west-2") + +# Create workload identity +identity = client.create_workload_identity( + name="my-agent", + allowedResourceOAuth2ReturnUrls=["https://app.example.com/callback"] +) + +# Get workload identity (GetWorkloadIdentity command) +details = client.get_workload_identity(name="my-agent") +print(f"Identity: {details['name']}") +print(f"Status: {details['status']}") +print(f"ARN: {details['workloadIdentityArn']}") + +# Get access token (different service, same client!) +token = client.get_workload_access_token( + workloadName="my-agent", + userId="user-123" +) +print(f"Token: {token['accessToken']}") +``` + +### Memory Operations + +```python +# Create memory resource (control plane) +memory = client.create_memory( + name="chat-memory", + eventExpiryDuration=90, + memoryStrategies=[{ + "SEMANTIC": { + "name": "facts", + "namespaces": ["app/{actorId}/{sessionId}"] + } + }] +) + +# Create event (data plane) +event = client.create_event( + memoryId=memory['memory']['id'], + actorId="user-123", + sessionId="session-456", + payload=[{ + "conversational": { + "content": {"text": "Hello!"}, + "role": "USER" + } + }] +) + +# Search memories (data plane) +results = client.retrieve_memory_records( + memoryId=memory['memory']['id'], + namespace="app/user-123/session-456", + searchCriteria={"searchQuery": "greeting", "topK": 5} +) +``` + +### Code Interpreter Operations + +```python +# Create interpreter (control plane) +interpreter = client.create_code_interpreter( + name="my-interpreter", + executionRoleArn="arn:aws:iam::123456789012:role/InterpreterRole", + networkConfiguration={"networkMode": "PUBLIC"} +) + +# Start session (data plane) +session = client.start_code_interpreter_session( + codeInterpreterIdentifier="aws.codeinterpreter.v1" +) + +# Execute code (data plane) +result = client.invoke_code_interpreter( + codeInterpreterIdentifier="aws.codeinterpreter.v1", + sessionId=session['sessionId'], + method="execute", + parameters={"code": "print('Hello!')"} +) +``` + +## How It Works + +The unified client: +1. Creates boto3 clients **only when needed** (lazy initialization) +2. Checks control plane client first when you call a method +3. Falls back to data plane client if not found on control plane +4. Raises helpful error if operation doesn't exist on either + +**No hardcoded operation lists** - works automatically with any new AWS operations! + +## All Supported Operations + +The unified client supports **all operations** from both services: + +**Control Plane (bedrock-agentcore-control):** +- Memory: `create_memory`, `get_memory`, `list_memories`, `update_memory`, `delete_memory` +- Code Interpreter: `create_code_interpreter`, `get_code_interpreter`, `list_code_interpreters`, `delete_code_interpreter` +- Browser: `create_browser`, `get_browser`, `list_browsers`, `delete_browser` +- Identity: `create_workload_identity`, `get_workload_identity`, `list_workload_identities`, `update_workload_identity`, `delete_workload_identity` +- Credentials: `create_oauth2_credential_provider`, `create_api_key_credential_provider`, `get_credential_provider`, `list_credential_providers`, `delete_credential_provider` + +**Data Plane (bedrock-agentcore):** +- Memory: `create_event`, `get_event`, `list_events`, `delete_event`, `retrieve_memory_records`, `list_memory_records` +- Code Interpreter: `start_code_interpreter_session`, `stop_code_interpreter_session`, `invoke_code_interpreter` +- Browser: `start_browser_session`, `stop_browser_session`, `invoke_browser`, `update_browser_stream` +- Identity: `get_workload_access_token`, `get_api_key` + +## More Examples + +See the `examples/` directory: +- [`unified_client_quickstart.py`](examples/unified_client_quickstart.py) - Simplest possible example +- [`unified_client_usage.py`](examples/unified_client_usage.py) - Complete usage guide +- [`unified_client_identity_example.py`](examples/unified_client_identity_example.py) - Identity operations in detail +- [`get_workload_identity_comparison.py`](examples/get_workload_identity_comparison.py) - Before/after comparison + +## Documentation + +See [`docs/unified_client.md`](docs/unified_client.md) for complete documentation including: +- Architecture details +- Migration guide +- Advanced usage +- FAQ + +## Benefits + +✅ **Simpler code** - One client instead of two +✅ **Less to remember** - Don't need to know which service has which operation +✅ **Automatic routing** - Client figures out where to send each call +✅ **Lazy initialization** - Only creates clients when needed +✅ **Future-proof** - No hardcoded lists, works with new AWS operations automatically +✅ **Backward compatible** - Works exactly like boto3 clients + +## Installation + +The unified client is included in the bedrock-agentcore SDK: + +```bash +pip install bedrock-agentcore +``` + +Then import and use: + +```python +from bedrock_agentcore import UnifiedBedrockAgentCoreClient + +client = UnifiedBedrockAgentCoreClient(region_name="us-west-2") +``` + +## Questions? + +**Q: Does this add overhead?** +A: Negligible (microseconds) - just simple attribute checks. Clients are cached after creation. + +**Q: Can I still use the old boto3 clients?** +A: Yes! The unified client doesn't replace them, it just makes them easier to use. + +**Q: What if AWS adds new operations?** +A: They'll work automatically - no hardcoded operation lists means no updates needed. + +**Q: Does this work with all boto3 features?** +A: The unified client currently focuses on direct method calls. For advanced features (paginators, waiters), access the underlying clients via `client.control_plane_client` and `client.data_plane_client`. + +## Summary + +**Traditional approach:** +```python +control = boto3.client("bedrock-agentcore-control", region="us-west-2") +data = boto3.client("bedrock-agentcore", region="us-west-2") + +memory = control.create_memory(...) # Which client? 🤔 +event = data.create_event(...) # Which client? 🤔 +identity = control.get_workload_identity(...) # Which client? 🤔 +``` + +**Unified approach:** +```python +client = UnifiedBedrockAgentCoreClient(region_name="us-west-2") + +memory = client.create_memory(...) # ✨ Automatic routing! +event = client.create_event(...) # ✨ Automatic routing! +identity = client.get_workload_identity(...) # ✨ Automatic routing! +``` + +**That's it! One client, automatic routing, simpler code.** 🎉 diff --git a/docs/unified_client.md b/docs/unified_client.md new file mode 100644 index 00000000..4228ccdc --- /dev/null +++ b/docs/unified_client.md @@ -0,0 +1,397 @@ +# Unified Bedrock AgentCore Client + +## Overview + +The `UnifiedBedrockAgentCoreClient` is a thin wrapper that provides a single, unified interface for all AWS Bedrock AgentCore operations. It automatically routes API calls to the appropriate underlying service without requiring you to know which service handles which operation. + +## The Problem It Solves + +AWS Bedrock AgentCore has two separate services: + +1. **bedrock-agentcore-control** (Control Plane): Handles resource management operations like creating, updating, and deleting resources +2. **bedrock-agentcore** (Data Plane): Handles runtime and data operations like creating events, starting sessions, and executing code + +Traditionally, you would need to: +- Know which service handles each operation +- Create and manage separate boto3 clients for each service +- Remember which client to use for each API call + +**Example of the traditional approach:** +```python +import boto3 + +# Need to create two clients +control_client = boto3.client("bedrock-agentcore-control", region_name="us-west-2") +data_client = boto3.client("bedrock-agentcore", region_name="us-west-2") + +# Need to remember which client to use +memory = control_client.create_memory(...) # Control plane +event = data_client.create_event(...) # Data plane +``` + +## The Solution + +The unified client simplifies this: + +```python +from bedrock_agentcore import UnifiedBedrockAgentCoreClient + +# Single client for everything +client = UnifiedBedrockAgentCoreClient(region_name="us-west-2") + +# Automatically routed to the correct service +memory = client.create_memory(...) # Automatically uses control plane +event = client.create_event(...) # Automatically uses data plane +``` + +## Key Features + +### 1. Automatic Routing +The client automatically determines which underlying service (control plane or data plane) handles each operation and routes calls appropriately. + +### 2. Lazy Initialization +Boto3 clients are only created when first needed, minimizing resource usage: +```python +# No boto3 clients created yet +client = UnifiedBedrockAgentCoreClient(region_name="us-west-2") + +# Control plane client created on first control operation +client.create_memory(...) + +# Data plane client created on first data operation +client.create_event(...) +``` + +### 3. No Hardcoded Operations +The client uses dynamic routing with no hardcoded operation lists, ensuring it always works with the latest AWS API updates. + +### 4. Full boto3 Compatibility +All boto3 client methods are available through the unified client with identical signatures. + +## Usage Examples + +### Memory Operations + +```python +from bedrock_agentcore import UnifiedBedrockAgentCoreClient + +client = UnifiedBedrockAgentCoreClient(region_name="us-west-2") + +# Control plane: Create memory resource +memory = client.create_memory( + name="my-memory", + eventExpiryDuration=90, + memoryStrategies=[{ + "SEMANTIC": { + "name": "facts", + "namespaces": ["app/{actorId}/{sessionId}"] + } + }] +) + +# Data plane: Create events (conversation turns) +event = client.create_event( + memoryId=memory['memory']['id'], + actorId="user-123", + sessionId="session-456", + payload=[{ + "conversational": { + "content": {"text": "Hello!"}, + "role": "USER" + } + }] +) + +# Data plane: Retrieve memory records +records = client.retrieve_memory_records( + memoryId=memory['memory']['id'], + namespace="app/user-123/session-456", + searchCriteria={"searchQuery": "greeting", "topK": 3} +) + +# Control plane: Clean up +client.delete_memory(memoryId=memory['memory']['id']) +``` + +### Code Interpreter Operations + +```python +# Control plane: Create code interpreter resource +interpreter = client.create_code_interpreter( + name="my-interpreter", + executionRoleArn="arn:aws:iam::123456789012:role/InterpreterRole", + networkConfiguration={"networkMode": "PUBLIC"} +) + +# Data plane: Start session +session = client.start_code_interpreter_session( + codeInterpreterIdentifier="aws.codeinterpreter.v1" +) + +# Data plane: Execute code +result = client.invoke_code_interpreter( + codeInterpreterIdentifier="aws.codeinterpreter.v1", + sessionId=session['sessionId'], + method="execute", + parameters={"code": "print('Hello, World!')"} +) + +# Data plane: Stop session +client.stop_code_interpreter_session( + codeInterpreterIdentifier="aws.codeinterpreter.v1", + sessionId=session['sessionId'] +) + +# Control plane: Clean up +client.delete_code_interpreter( + codeInterpreterId=interpreter['codeInterpreterId'] +) +``` + +### Browser Operations + +```python +# Control plane: Create browser resource +browser = client.create_browser( + name="my-browser", + executionRoleArn="arn:aws:iam::123456789012:role/BrowserRole", + networkConfiguration={"networkMode": "PUBLIC"}, + recording={ + "enabled": True, + "s3Location": { + "bucket": "my-recordings", + "keyPrefix": "sessions/" + } + } +) + +# Data plane: Start browser session +session = client.start_browser_session( + browserIdentifier="aws.browser.v1" +) + +# Data plane: Automate browser +result = client.invoke_browser( + browserIdentifier="aws.browser.v1", + sessionId=session['sessionId'], + method="navigate", + parameters={"url": "https://example.com"} +) + +# Data plane: Stop session +client.stop_browser_session( + browserIdentifier="aws.browser.v1", + sessionId=session['sessionId'] +) + +# Control plane: Clean up +client.delete_browser(browserId=browser['browserId']) +``` + +## Advanced Usage + +### Checking Which Client Handles an Operation + +If you need to know which underlying client handles a specific operation (useful for debugging): + +```python +# Get the underlying boto3 client for an operation +control_client = client.get_client_for_operation("create_memory") +print(type(control_client)) # + +data_client = client.get_client_for_operation("create_event") +print(type(data_client)) # +``` + +### Direct Access to Underlying Clients + +If you need direct access to the underlying boto3 clients: + +```python +# Access control plane client directly +control_plane = client.control_plane_client + +# Access data plane client directly +data_plane = client.data_plane_client +``` + +## Migration from Multiple Clients + +If you're currently using separate boto3 clients, migration is simple: + +**Before:** +```python +import boto3 + +control_client = boto3.client("bedrock-agentcore-control", region_name="us-west-2") +data_client = boto3.client("bedrock-agentcore", region_name="us-west-2") + +memory = control_client.create_memory(...) +event = data_client.create_event(...) +``` + +**After:** +```python +from bedrock_agentcore import UnifiedBedrockAgentCoreClient + +client = UnifiedBedrockAgentCoreClient(region_name="us-west-2") + +memory = client.create_memory(...) # Same method, no need to pick client +event = client.create_event(...) # Same method, no need to pick client +``` + +## Error Handling + +The unified client provides helpful error messages when operations are not found: + +```python +try: + client.non_existent_operation() +except AttributeError as e: + print(e) + # AttributeError: 'UnifiedBedrockAgentCoreClient' object has no attribute 'non_existent_operation'. + # Operation 'non_existent_operation' was not found on either the control plane + # (bedrock-agentcore-control) or data plane (bedrock-agentcore) client. +``` + +## Performance Considerations + +### Lazy Initialization +The unified client uses lazy initialization for boto3 clients: +- Control plane client is only created when you first call a control plane operation +- Data plane client is only created when you first call a data plane operation +- Once created, clients are cached and reused + +This means there's **zero performance overhead** if you only use one type of operation. + +### Routing Overhead +The routing mechanism uses simple `hasattr` checks: +- First checks control plane client +- If not found, checks data plane client +- The overhead is negligible (microseconds) + +## Complete Example + +See [examples/unified_client_usage.py](../examples/unified_client_usage.py) for a complete working example demonstrating all features. + +## API Reference + +### UnifiedBedrockAgentCoreClient + +```python +class UnifiedBedrockAgentCoreClient: + def __init__(self, region_name: Optional[str] = None) +``` + +#### Parameters +- `region_name` (str, optional): AWS region to use. If not provided, uses the default boto3 session region or falls back to `us-west-2`. + +#### Properties +- `region_name` (str): The AWS region being used +- `control_plane_client`: The underlying boto3 client for control plane operations (lazy initialized) +- `data_plane_client`: The underlying boto3 client for data plane operations (lazy initialized) + +#### Methods + +All boto3 client methods for both `bedrock-agentcore-control` and `bedrock-agentcore` are available as direct methods on the unified client. + +**Special method:** +```python +def get_client_for_operation(self, operation_name: str) -> boto3.Client +``` +Returns the underlying boto3 client that handles the specified operation. + +## Operation Reference + +### Control Plane Operations (bedrock-agentcore-control) + +**Memory:** +- `create_memory` +- `get_memory` +- `list_memories` +- `update_memory` +- `delete_memory` +- `list_memory_strategies` + +**Code Interpreter:** +- `create_code_interpreter` +- `delete_code_interpreter` +- `get_code_interpreter` +- `list_code_interpreters` + +**Browser:** +- `create_browser` +- `delete_browser` +- `get_browser` +- `list_browsers` + +**Identity:** +- `create_oauth2_credential_provider` +- `create_api_key_credential_provider` +- `delete_credential_provider` +- `get_credential_provider` +- `list_credential_providers` +- `update_credential_provider` +- `create_workload_identity` +- `update_workload_identity` +- `get_workload_identity` +- `delete_workload_identity` +- `list_workload_identities` + +### Data Plane Operations (bedrock-agentcore) + +**Memory:** +- `create_event` +- `get_event` +- `delete_event` +- `list_events` +- `retrieve_memory_records` +- `get_memory_record` +- `delete_memory_record` +- `list_memory_records` + +**Code Interpreter:** +- `start_code_interpreter_session` +- `stop_code_interpreter_session` +- `get_code_interpreter_session` +- `list_code_interpreter_sessions` +- `invoke_code_interpreter` + +**Browser:** +- `start_browser_session` +- `stop_browser_session` +- `get_browser_session` +- `list_browser_sessions` +- `update_browser_stream` +- `invoke_browser` + +**Runtime:** +- `generate_ws_connection` +- `generate_presigned_url` + +**Identity:** +- `get_workload_access_token` +- `get_api_key` + +## Frequently Asked Questions + +### Q: Does this add any overhead? +**A:** The overhead is negligible (microseconds per call) due to simple attribute checks. Boto3 clients are cached after first use. + +### Q: What happens if AWS adds new operations? +**A:** The unified client uses dynamic routing with no hardcoded operation lists, so it automatically supports new operations without any updates. + +### Q: Can I still use the underlying boto3 clients directly? +**A:** Yes! You can access them via `client.control_plane_client` and `client.data_plane_client`. + +### Q: Does this work with all boto3 features (paginators, waiters, etc.)? +**A:** The unified client currently focuses on direct method calls. For advanced boto3 features, access the underlying clients directly. + +### Q: Is this thread-safe? +**A:** Yes, the client uses boto3 clients which are thread-safe for making requests. + +## See Also + +- [AWS Bedrock AgentCore Control Documentation](https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/bedrock-agentcore-control.html) +- [AWS Bedrock AgentCore Data Documentation](https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/bedrock-agentcore.html) +- [Complete Usage Example](../examples/unified_client_usage.py) diff --git a/examples/get_workload_identity_comparison.py b/examples/get_workload_identity_comparison.py new file mode 100644 index 00000000..fb4c882d --- /dev/null +++ b/examples/get_workload_identity_comparison.py @@ -0,0 +1,231 @@ +""" +Comparison: GetWorkloadIdentity with and without UnifiedClient + +This example shows the difference between using separate boto3 clients +vs using the unified client for identity operations. +""" + +import boto3 +from bedrock_agentcore import UnifiedBedrockAgentCoreClient + +# ============================================================================ +# BEFORE: Traditional approach with separate boto3 clients +# ============================================================================ + +print("=" * 70) +print("BEFORE: Using separate boto3 clients (the hard way)") +print("=" * 70) +print() + +# Problem 1: You need to know which service to use +# GetWorkloadIdentity is on the CONTROL plane, but how would you know that? +print("Step 1: Create the correct client") +print(" → You need to know GetWorkloadIdentity is on bedrock-agentcore-control") +control_client = boto3.client("bedrock-agentcore-control", region_name="us-west-2") +print(" ✓ Created: bedrock-agentcore-control client") +print() + +# Step 2: Call the operation +print("Step 2: Call GetWorkloadIdentity") +try: + response = control_client.get_workload_identity(name="my-agent-identity") + print(f" ✓ Retrieved identity: {response['name']}") + print(f" Status: {response['status']}") + print(f" ARN: {response['workloadIdentityArn']}") +except Exception as e: + print(f" ⚠ Error (expected if identity doesn't exist): {type(e).__name__}") +print() + +# Problem 2: If you need data plane operations, you need ANOTHER client +print("Step 3: Need a token? Create ANOTHER client!") +print(" → GetWorkloadAccessToken is on the DATA plane (bedrock-agentcore)") +data_client = boto3.client("bedrock-agentcore", region_name="us-west-2") +print(" ✓ Created: bedrock-agentcore client") +print() + +try: + token_response = data_client.get_workload_access_token( + workloadName="my-agent-identity" + ) + print(f" ✓ Got access token: {token_response['accessToken'][:20]}...") +except Exception as e: + print(f" ⚠ Error (expected): {type(e).__name__}") +print() + +print("❌ Problems with this approach:") +print(" • You need to know which service has which operation") +print(" • You need to create and manage multiple boto3 clients") +print(" • Easy to use the wrong client and get confusing errors") +print() + +# ============================================================================ +# AFTER: Using UnifiedBedrockAgentCoreClient +# ============================================================================ + +print("=" * 70) +print("AFTER: Using UnifiedBedrockAgentCoreClient (the easy way)") +print("=" * 70) +print() + +# Solution: One client for everything! +print("Step 1: Create ONE unified client") +client = UnifiedBedrockAgentCoreClient(region_name="us-west-2") +print(" ✓ Created: UnifiedBedrockAgentCoreClient") +print() + +# Step 2: Just call the operation - routing is automatic +print("Step 2: Call GetWorkloadIdentity (automatically routed)") +try: + response = client.get_workload_identity(name="my-agent-identity") + print(f" ✓ Retrieved identity: {response['name']}") + print(f" Status: {response['status']}") + print(f" ARN: {response['workloadIdentityArn']}") + print(" → Automatically routed to control plane ✨") +except Exception as e: + print(f" ⚠ Error (expected if identity doesn't exist): {type(e).__name__}") + print(" → Still automatically routed to the correct service ✨") +print() + +# Step 3: Need a token? Use the SAME client! +print("Step 3: Need a token? Use the SAME client!") +try: + token_response = client.get_workload_access_token( + workloadName="my-agent-identity" + ) + print(f" ✓ Got access token: {token_response['accessToken'][:20]}...") + print(" → Automatically routed to data plane ✨") +except Exception as e: + print(f" ⚠ Error (expected): {type(e).__name__}") + print(" → Still automatically routed to the correct service ✨") +print() + +print("✅ Benefits of unified client:") +print(" • No need to know which service has which operation") +print(" • Single client for ALL operations") +print(" • Automatic routing - just call the method you need!") +print() + +# ============================================================================ +# COMPLETE WORKFLOW COMPARISON +# ============================================================================ + +print("=" * 70) +print("COMPLETE WORKFLOW COMPARISON") +print("=" * 70) +print() + +print("Traditional Approach (with separate clients):") +print("-" * 70) +print(""" +import boto3 + +# Need to know which service for each operation! +control_client = boto3.client("bedrock-agentcore-control", region="us-west-2") +data_client = boto3.client("bedrock-agentcore", region="us-west-2") + +# Create workload identity - use control client +identity = control_client.create_workload_identity(name="my-identity", ...) + +# Get workload identity - use control client +details = control_client.get_workload_identity(name="my-identity") + +# Get access token - use data client (different client!) +token = data_client.get_workload_access_token(workloadName="my-identity") + +# Update identity - back to control client +updated = control_client.update_workload_identity(name="my-identity", ...) +""") +print() + +print("Unified Client Approach:") +print("-" * 70) +print(""" +from bedrock_agentcore import UnifiedBedrockAgentCoreClient + +# One client for everything! +client = UnifiedBedrockAgentCoreClient(region_name="us-west-2") + +# All operations use the same client - automatic routing! +identity = client.create_workload_identity(name="my-identity", ...) +details = client.get_workload_identity(name="my-identity") +token = client.get_workload_access_token(workloadName="my-identity") +updated = client.update_workload_identity(name="my-identity", ...) + +# No need to remember which client to use - just call the method! +""") +print() + +# ============================================================================ +# REAL-WORLD EXAMPLE +# ============================================================================ + +print("=" * 70) +print("REAL-WORLD EXAMPLE: Complete Identity Setup") +print("=" * 70) +print() + +print("Using UnifiedBedrockAgentCoreClient:") +print() + +# Create unified client +client = UnifiedBedrockAgentCoreClient(region_name="us-west-2") + +print("1. Create workload identity") +try: + identity = client.create_workload_identity( + name="production-agent", + allowedResourceOAuth2ReturnUrls=["https://app.example.com/callback"] + ) + print(f" ✓ Created: {identity['name']}") +except Exception as e: + print(f" ⚠ {type(e).__name__}: Identity may already exist") +print() + +print("2. Get workload identity details (GetWorkloadIdentity)") +try: + details = client.get_workload_identity(name="production-agent") + print(f" ✓ Name: {details['name']}") + print(f" ✓ Status: {details['status']}") + print(f" ✓ ARN: {details['workloadIdentityArn']}") +except Exception as e: + print(f" ⚠ {type(e).__name__}") +print() + +print("3. List all workload identities") +try: + all_identities = client.list_workload_identities() + count = len(all_identities.get('workloadIdentities', [])) + print(f" ✓ Found {count} workload identities") +except Exception as e: + print(f" ⚠ {type(e).__name__}") +print() + +print("4. Get access token for agent") +try: + token = client.get_workload_access_token( + workloadName="production-agent", + userId="user-12345" + ) + print(f" ✓ Got access token: {token['accessToken'][:15]}...") +except Exception as e: + print(f" ⚠ {type(e).__name__}") +print() + +print("=" * 70) +print("SUMMARY") +print("=" * 70) +print() +print("✨ With UnifiedBedrockAgentCoreClient:") +print() +print(" • Write cleaner, simpler code") +print(" • Don't worry about control vs data plane") +print(" • Use ONE client for ALL operations") +print(" • Let the client handle the routing automatically") +print() +print(" Traditional: 5+ lines to set up clients") +print(" Unified: 1 line to set up client") +print() +print(" Traditional: Need to remember which client for each operation") +print(" Unified: Just call client.(...)") +print() +print("=" * 70) diff --git a/examples/unified_client_identity_example.py b/examples/unified_client_identity_example.py new file mode 100644 index 00000000..d7062770 --- /dev/null +++ b/examples/unified_client_identity_example.py @@ -0,0 +1,251 @@ +""" +Identity operations example using UnifiedBedrockAgentCoreClient. + +This example demonstrates how to use identity and authentication operations +with the unified client, including workload identity and credential providers. +""" + +from bedrock_agentcore import UnifiedBedrockAgentCoreClient + +# Initialize the unified client +client = UnifiedBedrockAgentCoreClient(region_name="us-west-2") + +print("=== Identity and Authentication Operations ===\n") + +# ============================================================================ +# WORKLOAD IDENTITY OPERATIONS +# ============================================================================ + +print("1. Creating Workload Identity (Control Plane)") +print("-" * 60) + +# Create a workload identity - automatically routed to control plane +workload_identity = client.create_workload_identity( + name="my-agent-identity", + allowedResourceOAuth2ReturnUrls=[ + "https://myapp.example.com/oauth/callback" + ] +) +print(f"✓ Created workload identity: {workload_identity['name']}") +print(f" Status: {workload_identity['status']}") +print(f" ARN: {workload_identity['workloadIdentityArn']}") +print() + +# ============================================================================ +# GET WORKLOAD IDENTITY (Control Plane) +# ============================================================================ + +print("2. Getting Workload Identity (Control Plane)") +print("-" * 60) + +# Get workload identity details - automatically routed to control plane +identity_details = client.get_workload_identity( + name="my-agent-identity" +) +print(f"✓ Retrieved workload identity: {identity_details['name']}") +print(f" Status: {identity_details['status']}") +print(f" Created: {identity_details['createdAt']}") +print(f" Last Updated: {identity_details['lastUpdatedAt']}") +print(f" Allowed OAuth URLs: {identity_details['allowedResourceOAuth2ReturnUrls']}") +print() + +# ============================================================================ +# LIST WORKLOAD IDENTITIES (Control Plane) +# ============================================================================ + +print("3. Listing Workload Identities (Control Plane)") +print("-" * 60) + +# List all workload identities - automatically routed to control plane +identities = client.list_workload_identities(maxResults=10) +print(f"✓ Found {len(identities.get('workloadIdentities', []))} workload identities:") +for identity in identities.get('workloadIdentities', []): + print(f" - {identity['name']} (Status: {identity['status']})") +print() + +# ============================================================================ +# UPDATE WORKLOAD IDENTITY (Control Plane) +# ============================================================================ + +print("4. Updating Workload Identity (Control Plane)") +print("-" * 60) + +# Update workload identity - automatically routed to control plane +updated_identity = client.update_workload_identity( + name="my-agent-identity", + allowedResourceOAuth2ReturnUrls=[ + "https://myapp.example.com/oauth/callback", + "https://myapp-staging.example.com/oauth/callback" # Added new URL + ] +) +print(f"✓ Updated workload identity: {updated_identity['name']}") +print(f" New OAuth URLs: {updated_identity['allowedResourceOAuth2ReturnUrls']}") +print() + +# ============================================================================ +# GET WORKLOAD ACCESS TOKEN (Data Plane) +# ============================================================================ + +print("5. Getting Workload Access Token (Data Plane)") +print("-" * 60) + +# Get access token for the workload - automatically routed to data plane +# Note: This requires proper IAM permissions and agent setup +try: + token_response = client.get_workload_access_token( + workloadName="my-agent-identity", + userId="user-12345", # Optional: specific user context + # userToken="" # Optional: user's OAuth token + # agentIdentityToken="" # Optional: agent identity token + ) + print(f"✓ Retrieved workload access token") + print(f" Token type: {token_response.get('tokenType', 'Bearer')}") + print(f" Expires in: {token_response.get('expiresIn', 'N/A')} seconds") + # Don't print the actual token for security + print(f" Access token: {token_response['accessToken'][:20]}...") +except Exception as e: + print(f"⚠ Note: Getting access token may fail without proper setup: {type(e).__name__}") +print() + +# ============================================================================ +# CREDENTIAL PROVIDER OPERATIONS +# ============================================================================ + +print("6. Creating OAuth2 Credential Provider (Control Plane)") +print("-" * 60) + +# Create OAuth2 credential provider - automatically routed to control plane +oauth_provider = client.create_oauth2_credential_provider( + name="github-oauth-provider", + authorizationUrl="https://github.com/login/oauth/authorize", + clientId="my-github-client-id", + clientSecretArn="arn:aws:secretsmanager:us-west-2:123456789012:secret:github-oauth-secret", + oAuth2GrantType="AUTHORIZATION_CODE", + scopes=["read:user", "repo"], + tokenUrl="https://github.com/login/oauth/access_token" +) +print(f"✓ Created OAuth2 credential provider: {oauth_provider['name']}") +print(f" Provider ARN: {oauth_provider['credentialProviderArn']}") +print() + +print("7. Creating API Key Credential Provider (Control Plane)") +print("-" * 60) + +# Create API key provider - automatically routed to control plane +apikey_provider = client.create_api_key_credential_provider( + name="external-api-provider", + apiKeySecretArn="arn:aws:secretsmanager:us-west-2:123456789012:secret:external-api-key" +) +print(f"✓ Created API key credential provider: {apikey_provider['name']}") +print(f" Provider ARN: {apikey_provider['credentialProviderArn']}") +print() + +print("8. Listing Credential Providers (Control Plane)") +print("-" * 60) + +# List credential providers - automatically routed to control plane +providers = client.list_credential_providers(maxResults=10) +print(f"✓ Found {len(providers.get('credentialProviders', []))} credential providers:") +for provider in providers.get('credentialProviders', []): + print(f" - {provider['name']} (Type: {provider['credentialProviderType']})") +print() + +print("9. Getting Credential Provider Details (Control Plane)") +print("-" * 60) + +# Get credential provider - automatically routed to control plane +provider_details = client.get_credential_provider( + name="github-oauth-provider" +) +print(f"✓ Retrieved credential provider: {provider_details['name']}") +print(f" Type: {provider_details['credentialProviderType']}") +print(f" Status: {provider_details['status']}") +print() + +print("10. Getting API Key (Data Plane)") +print("-" * 60) + +# Get API key from provider - automatically routed to data plane +try: + api_key_response = client.get_api_key( + providerName="external-api-provider", + # agentIdentityToken="" # Optional: agent identity token + ) + print(f"✓ Retrieved API key") + # Don't print the actual key for security + print(f" API key: {api_key_response['apiKey'][:10]}...") +except Exception as e: + print(f"⚠ Note: Getting API key may fail without proper setup: {type(e).__name__}") +print() + +# ============================================================================ +# COMPLETE AUTHENTICATION FLOW EXAMPLE +# ============================================================================ + +print("11. Complete Authentication Flow") +print("-" * 60) +print("Here's how these operations work together:\n") +print("Step 1: Create workload identity (control plane)") +print(" → client.create_workload_identity(...)") +print() +print("Step 2: Get workload identity details (control plane)") +print(" → client.get_workload_identity(name='my-identity')") +print() +print("Step 3: Get access token for agent (data plane)") +print(" → client.get_workload_access_token(workloadName='my-identity')") +print() +print("Step 4: Use token to access resources or get API keys") +print(" → client.get_api_key(providerName='my-provider', agentIdentityToken='...')") +print() + +# ============================================================================ +# CLEANUP +# ============================================================================ + +print("12. Cleanup (Control Plane)") +print("-" * 60) + +# Delete credential providers - automatically routed to control plane +client.delete_credential_provider(name="github-oauth-provider") +print("✓ Deleted OAuth2 credential provider") + +client.delete_credential_provider(name="external-api-provider") +print("✓ Deleted API key credential provider") + +# Delete workload identity - automatically routed to control plane +client.delete_workload_identity(name="my-agent-identity") +print("✓ Deleted workload identity") +print() + +# ============================================================================ +# KEY TAKEAWAYS +# ============================================================================ + +print("=" * 60) +print("✨ KEY TAKEAWAYS") +print("=" * 60) +print() +print("1. All identity operations work through the SAME unified client") +print("2. Control plane operations (create, get, update, delete) are") +print(" automatically routed to bedrock-agentcore-control") +print("3. Data plane operations (get tokens, get keys) are automatically") +print(" routed to bedrock-agentcore") +print("4. You don't need to know or care which service handles which operation!") +print() +print("Control Plane Operations (automatically routed):") +print(" • create_workload_identity") +print(" • get_workload_identity ← GetWorkloadIdentity command") +print(" • update_workload_identity") +print(" • delete_workload_identity") +print(" • list_workload_identities") +print(" • create_oauth2_credential_provider") +print(" • create_api_key_credential_provider") +print(" • get_credential_provider") +print(" • list_credential_providers") +print(" • delete_credential_provider") +print() +print("Data Plane Operations (automatically routed):") +print(" • get_workload_access_token") +print(" • get_api_key") +print() +print("=" * 60) diff --git a/examples/unified_client_quickstart.py b/examples/unified_client_quickstart.py new file mode 100644 index 00000000..46c7a950 --- /dev/null +++ b/examples/unified_client_quickstart.py @@ -0,0 +1,63 @@ +""" +Quick start example for UnifiedBedrockAgentCoreClient. + +This example shows the simplest way to get started with the unified client. +""" + +from bedrock_agentcore import UnifiedBedrockAgentCoreClient + +# Step 1: Create the unified client +# This is all you need - no need to create multiple clients! +client = UnifiedBedrockAgentCoreClient(region_name="us-west-2") + +# Step 2: Use any operation - the client automatically routes to the right service +# You don't need to know or care about control plane vs data plane + +# Create a memory (control plane operation) +memory = client.create_memory( + name="my-first-memory", + eventExpiryDuration=90, + memoryStrategies=[{ + "SEMANTIC": { + "name": "facts", + "namespaces": ["app/{actorId}/{sessionId}"] + } + }] +) +print(f"✓ Created memory: {memory['memory']['id']}") + +# Save a conversation (data plane operation) +event = client.create_event( + memoryId=memory['memory']['id'], + actorId="user-123", + sessionId="session-456", + payload=[ + { + "conversational": { + "content": {"text": "What's the weather today?"}, + "role": "USER" + } + }, + { + "conversational": { + "content": {"text": "It's sunny and 75°F!"}, + "role": "ASSISTANT" + } + } + ] +) +print(f"✓ Saved conversation: {event['event']['eventId']}") + +# Search memories (data plane operation) +results = client.retrieve_memory_records( + memoryId=memory['memory']['id'], + namespace="app/user-123/session-456", + searchCriteria={ + "searchQuery": "weather", + "topK": 5 + } +) +print(f"✓ Found {len(results.get('memoryRecordSummaries', []))} relevant memories") + +# That's it! The unified client handled routing everything automatically. +print("\n✨ Success! You used control and data plane operations without thinking about it.") diff --git a/examples/unified_client_usage.py b/examples/unified_client_usage.py new file mode 100644 index 00000000..b9ab26dc --- /dev/null +++ b/examples/unified_client_usage.py @@ -0,0 +1,234 @@ +""" +Example usage of UnifiedBedrockAgentCoreClient. + +This example demonstrates how to use the unified client to interact with +AWS Bedrock AgentCore services without worrying about which underlying +service (control plane vs data plane) handles each operation. +""" + +from bedrock_agentcore import UnifiedBedrockAgentCoreClient + +# ============================================================================ +# INITIALIZATION +# ============================================================================ + +# Initialize the unified client - it will automatically route operations +# to the appropriate service (bedrock-agentcore or bedrock-agentcore-control) +client = UnifiedBedrockAgentCoreClient(region_name="us-west-2") + +# ============================================================================ +# MEMORY OPERATIONS +# ============================================================================ + +print("\n=== Memory Operations ===\n") + +# 1. CREATE MEMORY (Control Plane) +# The client automatically routes this to bedrock-agentcore-control +memory_response = client.create_memory( + name="customer-support-memory", + eventExpiryDuration=90, + memoryStrategies=[ + { + "SEMANTIC": { + "name": "semantic-facts", + "namespaces": ["support/facts/{actorId}/{sessionId}"] + } + } + ], + description="Memory for customer support conversations" +) +print(f"✓ Created memory: {memory_response['memory']['id']}") +memory_id = memory_response['memory']['id'] + +# 2. GET MEMORY (Control Plane) +# Automatically routed to control plane +memory_details = client.get_memory(memoryId=memory_id) +print(f"✓ Retrieved memory status: {memory_details['memory']['status']}") + +# 3. CREATE EVENT (Data Plane) +# The client automatically routes this to bedrock-agentcore +event_response = client.create_event( + memoryId=memory_id, + actorId="customer-123", + sessionId="session-456", + payload=[ + { + "conversational": { + "content": {"text": "I need help with my order"}, + "role": "USER" + } + }, + { + "conversational": { + "content": {"text": "I'd be happy to help! What's your order number?"}, + "role": "ASSISTANT" + } + } + ] +) +print(f"✓ Created event: {event_response['event']['eventId']}") + +# 4. LIST EVENTS (Data Plane) +# Automatically routed to data plane +events = client.list_events( + memoryId=memory_id, + actorId="customer-123", + sessionId="session-456", + maxResults=10 +) +print(f"✓ Retrieved {len(events['events'])} events") + +# 5. RETRIEVE MEMORY RECORDS (Data Plane) +# Automatically routed to data plane - semantic search +records = client.retrieve_memory_records( + memoryId=memory_id, + namespace="support/facts/customer-123/session-456", + searchCriteria={ + "searchQuery": "order issue", + "topK": 3 + } +) +print(f"✓ Retrieved {len(records.get('memoryRecordSummaries', []))} memory records") + +# ============================================================================ +# CODE INTERPRETER OPERATIONS +# ============================================================================ + +print("\n=== Code Interpreter Operations ===\n") + +# 1. CREATE CODE INTERPRETER (Control Plane) +# Automatically routed to control plane +interpreter_response = client.create_code_interpreter( + name="data_analysis_interpreter", + executionRoleArn="arn:aws:iam::123456789012:role/CodeInterpreterRole", + networkConfiguration={ + "networkMode": "PUBLIC" + }, + description="Interpreter for data analysis tasks" +) +print(f"✓ Created code interpreter: {interpreter_response['codeInterpreterId']}") +interpreter_id = interpreter_response['codeInterpreterId'] + +# 2. GET CODE INTERPRETER (Control Plane) +# Automatically routed to control plane +interpreter_details = client.get_code_interpreter(codeInterpreterId=interpreter_id) +print(f"✓ Interpreter status: {interpreter_details['status']}") + +# 3. START SESSION (Data Plane) +# Automatically routed to data plane +session_response = client.start_code_interpreter_session( + codeInterpreterIdentifier="aws.codeinterpreter.v1" +) +print(f"✓ Started session: {session_response['sessionId']}") +session_id = session_response['sessionId'] + +# 4. INVOKE CODE INTERPRETER (Data Plane) +# Automatically routed to data plane +result = client.invoke_code_interpreter( + codeInterpreterIdentifier="aws.codeinterpreter.v1", + sessionId=session_id, + method="execute", + parameters={ + "code": "print('Hello from unified client!')" + } +) +print(f"✓ Code execution result: {result.get('output', 'Success')}") + +# 5. STOP SESSION (Data Plane) +# Automatically routed to data plane +client.stop_code_interpreter_session( + codeInterpreterIdentifier="aws.codeinterpreter.v1", + sessionId=session_id +) +print("✓ Stopped code interpreter session") + +# ============================================================================ +# BROWSER OPERATIONS +# ============================================================================ + +print("\n=== Browser Operations ===\n") + +# 1. CREATE BROWSER (Control Plane) +# Automatically routed to control plane +browser_response = client.create_browser( + name="web_automation_browser", + executionRoleArn="arn:aws:iam::123456789012:role/BrowserRole", + networkConfiguration={ + "networkMode": "PUBLIC" + }, + recording={ + "enabled": True, + "s3Location": { + "bucket": "my-browser-recordings", + "keyPrefix": "sessions/" + } + } +) +print(f"✓ Created browser: {browser_response['browserId']}") +browser_id = browser_response['browserId'] + +# 2. LIST BROWSERS (Control Plane) +# Automatically routed to control plane +browsers = client.list_browsers(maxResults=10) +print(f"✓ Retrieved {len(browsers.get('browsers', []))} browsers") + +# 3. START BROWSER SESSION (Data Plane) +# Automatically routed to data plane +browser_session = client.start_browser_session( + browserIdentifier="aws.browser.v1" +) +print(f"✓ Started browser session: {browser_session['sessionId']}") +browser_session_id = browser_session['sessionId'] + +# 4. INVOKE BROWSER (Data Plane) +# Automatically routed to data plane +browser_result = client.invoke_browser( + browserIdentifier="aws.browser.v1", + sessionId=browser_session_id, + method="navigate", + parameters={ + "url": "https://example.com" + } +) +print("✓ Browser navigation successful") + +# 5. STOP BROWSER SESSION (Data Plane) +# Automatically routed to data plane +client.stop_browser_session( + browserIdentifier="aws.browser.v1", + sessionId=browser_session_id +) +print("✓ Stopped browser session") + +# ============================================================================ +# CLEANUP +# ============================================================================ + +print("\n=== Cleanup ===\n") + +# Delete resources (all control plane operations) +client.delete_browser(browserId=browser_id) +print("✓ Deleted browser") + +client.delete_code_interpreter(codeInterpreterId=interpreter_id) +print("✓ Deleted code interpreter") + +client.delete_memory(memoryId=memory_id) +print("✓ Deleted memory") + +# ============================================================================ +# ADVANCED: Check which client handles an operation +# ============================================================================ + +print("\n=== Advanced: Client Introspection ===\n") + +# You can check which underlying client handles a specific operation +control_client = client.get_client_for_operation("create_memory") +print(f"✓ create_memory uses: {type(control_client).__name__}") + +data_client = client.get_client_for_operation("create_event") +print(f"✓ create_event uses: {type(data_client).__name__}") + +print("\n=== Complete! ===\n") +print("The unified client automatically routed all operations to the correct service.") +print("You didn't need to know about bedrock-agentcore vs bedrock-agentcore-control!") diff --git a/src/bedrock_agentcore/__init__.py b/src/bedrock_agentcore/__init__.py index a77472f5..f0ceff0b 100644 --- a/src/bedrock_agentcore/__init__.py +++ b/src/bedrock_agentcore/__init__.py @@ -2,10 +2,12 @@ from .runtime import BedrockAgentCoreApp, BedrockAgentCoreContext, RequestContext from .runtime.models import PingStatus +from .unified_client import UnifiedBedrockAgentCoreClient __all__ = [ "BedrockAgentCoreApp", "RequestContext", "BedrockAgentCoreContext", "PingStatus", + "UnifiedBedrockAgentCoreClient", ] diff --git a/src/bedrock_agentcore/unified_client.py b/src/bedrock_agentcore/unified_client.py new file mode 100644 index 00000000..78cd8739 --- /dev/null +++ b/src/bedrock_agentcore/unified_client.py @@ -0,0 +1,204 @@ +"""Unified client for AWS Bedrock AgentCore services. + +This module provides a thin wrapper around boto3 clients that automatically +routes API operations to the appropriate service (control plane or data plane) +without requiring users to know which service handles which operation. +""" + +import logging +from typing import Optional + +import boto3 + +logger = logging.getLogger(__name__) + + +class UnifiedBedrockAgentCoreClient: + """Unified client that transparently routes operations between services. + + This client provides a single interface for all Bedrock AgentCore operations, + automatically routing calls to the appropriate underlying boto3 client: + - bedrock-agentcore-control (control plane): Resource management operations + - bedrock-agentcore (data plane): Runtime and data operations + + The client uses lazy initialization - boto3 clients are only created when + first accessed, minimizing overhead for unused services. + + The routing is fully dynamic - no hardcoded operation lists are maintained. + The client will attempt to find the requested operation on the control plane + first, then the data plane, ensuring it always works with the latest AWS APIs. + + Attributes: + region_name (str): AWS region for all operations + + Example: + >>> client = UnifiedBedrockAgentCoreClient(region_name="us-west-2") + >>> + >>> # Memory control plane operation - automatically routed + >>> memory = client.create_memory( + ... name="my-memory", + ... eventExpiryDuration=90, + ... memoryStrategies=[...] + ... ) + >>> + >>> # Memory data plane operation - automatically routed + >>> event = client.create_event( + ... memoryId=memory['id'], + ... actorId="user-123", + ... sessionId="session-456", + ... payload=[...] + ... ) + >>> + >>> # Code interpreter control plane - automatically routed + >>> interpreter = client.create_code_interpreter( + ... name="my-interpreter", + ... executionRoleArn="arn:aws:iam::..." + ... ) + >>> + >>> # Code interpreter data plane - automatically routed + >>> session = client.start_code_interpreter_session( + ... codeInterpreterIdentifier="aws.codeinterpreter.v1" + ... ) + """ + + def __init__(self, region_name: Optional[str] = None): + """Initialize the unified client. + + Args: + region_name: AWS region to use. If not provided, uses the default + boto3 session region or falls back to us-west-2. + """ + self.region_name = region_name or boto3.Session().region_name or "us-west-2" + + # Lazy initialization - clients created on first access + self._control_plane_client = None + self._data_plane_client = None + + logger.info( + "Initialized UnifiedBedrockAgentCoreClient for region: %s", + self.region_name + ) + + @property + def control_plane_client(self): + """Get or create the control plane boto3 client (lazy initialization).""" + if self._control_plane_client is None: + self._control_plane_client = boto3.client( + "bedrock-agentcore-control", + region_name=self.region_name + ) + logger.debug( + "Created control plane client for region: %s", + self.region_name + ) + return self._control_plane_client + + @property + def data_plane_client(self): + """Get or create the data plane boto3 client (lazy initialization).""" + if self._data_plane_client is None: + self._data_plane_client = boto3.client( + "bedrock-agentcore", + region_name=self.region_name + ) + logger.debug( + "Created data plane client for region: %s", + self.region_name + ) + return self._data_plane_client + + def __getattr__(self, name: str): + """Dynamically route method calls to the appropriate boto3 client. + + This method enables transparent access to all boto3 client methods by + checking both clients to find where the operation is defined. The routing + is fully dynamic with no hardcoded operation lists. + + The search order is: + 1. Try control plane client first (bedrock-agentcore-control) + 2. If not found, try data plane client (bedrock-agentcore) + 3. If not found on either, raise AttributeError + + Args: + name: The method name being accessed + + Returns: + A callable method from the appropriate boto3 client + + Raises: + AttributeError: If the method doesn't exist on either client + + Example: + # Control plane operation + client.create_memory(name="test", ...) + + # Data plane operation + client.create_event(memoryId="mem-123", ...) + + # Browser control plane + client.create_browser(name="my-browser", ...) + + # Browser data plane + client.start_browser_session(browserIdentifier="...") + """ + # Try control plane first (resource management operations) + try: + if hasattr(self.control_plane_client, name): + method = getattr(self.control_plane_client, name) + logger.debug("Routing '%s' to control plane", name) + return method + except Exception as e: + logger.debug("Error checking control plane for '%s': %s", name, e) + + # Try data plane next (runtime/data operations) + try: + if hasattr(self.data_plane_client, name): + method = getattr(self.data_plane_client, name) + logger.debug("Routing '%s' to data plane", name) + return method + except Exception as e: + logger.debug("Error checking data plane for '%s': %s", name, e) + + # Method not found on either client + raise AttributeError( + f"'{self.__class__.__name__}' object has no attribute '{name}'. " + f"Operation '{name}' was not found on either the control plane " + f"(bedrock-agentcore-control) or data plane (bedrock-agentcore) client. " + f"\n\nPlease check the boto3 documentation for valid operations:\n" + f"- Control plane: https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/bedrock-agentcore-control.html\n" + f"- Data plane: https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/bedrock-agentcore.html" + ) + + def get_client_for_operation(self, operation_name: str): + """Get the underlying boto3 client that handles a specific operation. + + This method is useful for debugging or when you need direct access to + the underlying boto3 client for advanced use cases. + + Args: + operation_name: The name of the operation + + Returns: + The boto3 client that handles this operation + + Raises: + ValueError: If the operation is not found on either client + + Example: + >>> client = UnifiedBedrockAgentCoreClient(region_name="us-west-2") + >>> control_client = client.get_client_for_operation("create_memory") + >>> data_client = client.get_client_for_operation("create_event") + """ + # Check control plane first + if hasattr(self.control_plane_client, operation_name): + return self.control_plane_client + + # Check data plane next + if hasattr(self.data_plane_client, operation_name): + return self.data_plane_client + + # Not found on either + raise ValueError( + f"Operation '{operation_name}' not found on either client. " + f"Check the boto3 documentation for valid operations." + ) diff --git a/tests/unit/test_unified_client.py b/tests/unit/test_unified_client.py new file mode 100644 index 00000000..df9a7bd5 --- /dev/null +++ b/tests/unit/test_unified_client.py @@ -0,0 +1,315 @@ +"""Tests for UnifiedBedrockAgentCoreClient.""" + +from unittest.mock import Mock, PropertyMock, patch + +import pytest + +from bedrock_agentcore.unified_client import UnifiedBedrockAgentCoreClient + + +class TestUnifiedClientInit: + """Tests for UnifiedBedrockAgentCoreClient initialization.""" + + def test_init_with_explicit_region(self): + """Test initialization with explicit region.""" + client = UnifiedBedrockAgentCoreClient(region_name="us-east-1") + assert client.region_name == "us-east-1" + + @patch("boto3.Session") + def test_init_with_default_region_from_session(self, mock_session): + """Test initialization uses boto3 session default region.""" + mock_session_instance = Mock() + mock_session_instance.region_name = "eu-west-1" + mock_session.return_value = mock_session_instance + + client = UnifiedBedrockAgentCoreClient() + assert client.region_name == "eu-west-1" + + @patch("boto3.Session") + def test_init_fallback_to_us_west_2(self, mock_session): + """Test initialization falls back to us-west-2 when no region available.""" + mock_session_instance = Mock() + mock_session_instance.region_name = None + mock_session.return_value = mock_session_instance + + client = UnifiedBedrockAgentCoreClient() + assert client.region_name == "us-west-2" + + def test_clients_not_initialized_on_init(self): + """Test that boto3 clients are not created during initialization.""" + client = UnifiedBedrockAgentCoreClient(region_name="us-west-2") + assert client._control_plane_client is None + assert client._data_plane_client is None + + +class TestLazyClientInitialization: + """Tests for lazy initialization of boto3 clients.""" + + @patch("boto3.client") + def test_control_plane_client_lazy_init(self, mock_boto_client): + """Test control plane client is created on first access.""" + mock_client = Mock() + mock_boto_client.return_value = mock_client + + client = UnifiedBedrockAgentCoreClient(region_name="us-west-2") + + # Client should not be created yet + assert client._control_plane_client is None + + # Access the property + result = client.control_plane_client + + # Should have created the client + mock_boto_client.assert_called_once_with( + "bedrock-agentcore-control", + region_name="us-west-2" + ) + assert result == mock_client + assert client._control_plane_client == mock_client + + @patch("boto3.client") + def test_control_plane_client_cached(self, mock_boto_client): + """Test control plane client is cached after first access.""" + mock_client = Mock() + mock_boto_client.return_value = mock_client + + client = UnifiedBedrockAgentCoreClient(region_name="us-west-2") + + # Access twice + result1 = client.control_plane_client + result2 = client.control_plane_client + + # Should only create once + assert mock_boto_client.call_count == 1 + assert result1 == result2 + + @patch("boto3.client") + def test_data_plane_client_lazy_init(self, mock_boto_client): + """Test data plane client is created on first access.""" + mock_client = Mock() + mock_boto_client.return_value = mock_client + + client = UnifiedBedrockAgentCoreClient(region_name="us-west-2") + + # Client should not be created yet + assert client._data_plane_client is None + + # Access the property + result = client.data_plane_client + + # Should have created the client + mock_boto_client.assert_called_once_with( + "bedrock-agentcore", + region_name="us-west-2" + ) + assert result == mock_client + assert client._data_plane_client == mock_client + + @patch("boto3.client") + def test_data_plane_client_cached(self, mock_boto_client): + """Test data plane client is cached after first access.""" + mock_client = Mock() + mock_boto_client.return_value = mock_client + + client = UnifiedBedrockAgentCoreClient(region_name="us-west-2") + + # Access twice + result1 = client.data_plane_client + result2 = client.data_plane_client + + # Should only create once + assert mock_boto_client.call_count == 1 + assert result1 == result2 + + +class TestOperationRouting: + """Tests for automatic operation routing.""" + + @patch("boto3.client") + def test_routes_control_plane_operation(self, mock_boto_client): + """Test that control plane operations are routed correctly.""" + # Setup mock control plane client with create_memory operation + mock_control_client = Mock() + mock_control_method = Mock(return_value={"memory": {"id": "mem-123"}}) + mock_control_client.create_memory = mock_control_method + + # Setup mock data plane client + mock_data_client = Mock() + + def mock_client_factory(service, **kwargs): + if service == "bedrock-agentcore-control": + return mock_control_client + elif service == "bedrock-agentcore": + return mock_data_client + return Mock() + + mock_boto_client.side_effect = mock_client_factory + + client = UnifiedBedrockAgentCoreClient(region_name="us-west-2") + + # Call a control plane operation + result = client.create_memory(name="test-memory") + + # Should route to control plane + mock_control_method.assert_called_once_with(name="test-memory") + assert result == {"memory": {"id": "mem-123"}} + + @patch("boto3.client") + def test_routes_data_plane_operation(self, mock_boto_client): + """Test that data plane operations are routed correctly.""" + # Setup mock control plane client + mock_control_client = Mock() + mock_control_client.configure_mock(**{"create_event": None}) + + # Setup mock data plane client with create_event operation + mock_data_client = Mock() + mock_data_method = Mock(return_value={"event": {"eventId": "evt-123"}}) + mock_data_client.create_event = mock_data_method + + def mock_client_factory(service, **kwargs): + if service == "bedrock-agentcore-control": + return mock_control_client + elif service == "bedrock-agentcore": + return mock_data_client + return Mock() + + mock_boto_client.side_effect = mock_client_factory + + client = UnifiedBedrockAgentCoreClient(region_name="us-west-2") + + # Call a data plane operation + result = client.create_event(memoryId="mem-123", actorId="user-1") + + # Should route to data plane + mock_data_method.assert_called_once_with(memoryId="mem-123", actorId="user-1") + assert result == {"event": {"eventId": "evt-123"}} + + @patch("boto3.client") + def test_raises_error_for_unknown_operation(self, mock_boto_client): + """Test that unknown operations raise AttributeError.""" + mock_control_client = Mock(spec=[]) # No methods + mock_data_client = Mock(spec=[]) # No methods + + def mock_client_factory(service, **kwargs): + if service == "bedrock-agentcore-control": + return mock_control_client + elif service == "bedrock-agentcore": + return mock_data_client + return Mock() + + mock_boto_client.side_effect = mock_client_factory + + client = UnifiedBedrockAgentCoreClient(region_name="us-west-2") + + # Try to call non-existent operation + with pytest.raises(AttributeError) as exc_info: + client.non_existent_operation() + + assert "non_existent_operation" in str(exc_info.value) + assert "not found" in str(exc_info.value).lower() + + +class TestGetClientForOperation: + """Tests for get_client_for_operation method.""" + + @patch("boto3.client") + def test_returns_control_plane_for_control_operation(self, mock_boto_client): + """Test returns control plane client for control operations.""" + mock_control_client = Mock() + mock_control_client.create_memory = Mock() + mock_data_client = Mock() + + def mock_client_factory(service, **kwargs): + if service == "bedrock-agentcore-control": + return mock_control_client + elif service == "bedrock-agentcore": + return mock_data_client + return Mock() + + mock_boto_client.side_effect = mock_client_factory + + client = UnifiedBedrockAgentCoreClient(region_name="us-west-2") + result = client.get_client_for_operation("create_memory") + + assert result == mock_control_client + + @patch("boto3.client") + def test_returns_data_plane_for_data_operation(self, mock_boto_client): + """Test returns data plane client for data operations.""" + mock_control_client = Mock(spec=[]) + mock_data_client = Mock() + mock_data_client.create_event = Mock() + + def mock_client_factory(service, **kwargs): + if service == "bedrock-agentcore-control": + return mock_control_client + elif service == "bedrock-agentcore": + return mock_data_client + return Mock() + + mock_boto_client.side_effect = mock_client_factory + + client = UnifiedBedrockAgentCoreClient(region_name="us-west-2") + result = client.get_client_for_operation("create_event") + + assert result == mock_data_client + + @patch("boto3.client") + def test_raises_error_for_unknown_operation(self, mock_boto_client): + """Test raises ValueError for unknown operations.""" + mock_control_client = Mock(spec=[]) + mock_data_client = Mock(spec=[]) + + def mock_client_factory(service, **kwargs): + if service == "bedrock-agentcore-control": + return mock_control_client + elif service == "bedrock-agentcore": + return mock_data_client + return Mock() + + mock_boto_client.side_effect = mock_client_factory + + client = UnifiedBedrockAgentCoreClient(region_name="us-west-2") + + with pytest.raises(ValueError) as exc_info: + client.get_client_for_operation("unknown_operation") + + assert "unknown_operation" in str(exc_info.value) + assert "not found" in str(exc_info.value).lower() + + +class TestMultipleOperations: + """Tests for calling multiple operations on the same client.""" + + @patch("boto3.client") + def test_can_call_both_control_and_data_operations(self, mock_boto_client): + """Test that both control and data plane operations work on same client.""" + # Setup mocks + mock_control_client = Mock() + mock_control_client.create_memory = Mock(return_value={"memory": {"id": "mem-123"}}) + + mock_data_client = Mock() + mock_data_client.create_event = Mock(return_value={"event": {"eventId": "evt-123"}}) + + def mock_client_factory(service, **kwargs): + if service == "bedrock-agentcore-control": + return mock_control_client + elif service == "bedrock-agentcore": + return mock_data_client + return Mock() + + mock_boto_client.side_effect = mock_client_factory + + client = UnifiedBedrockAgentCoreClient(region_name="us-west-2") + + # Call control plane operation + memory_result = client.create_memory(name="test") + assert memory_result == {"memory": {"id": "mem-123"}} + + # Call data plane operation + event_result = client.create_event(memoryId="mem-123") + assert event_result == {"event": {"eventId": "evt-123"}} + + # Both should have been called + mock_control_client.create_memory.assert_called_once() + mock_data_client.create_event.assert_called_once()