"""eCommerce supervisor agent (sample) for Amazon Bedrock AgentCore Runtime. Versions / setup: - Python 3.12+ - strands-agents >= 1.x - bedrock-agentcore Runtime HTTP contract (BedrockAgentCoreApp) - Model id below is a pin for docs; swap for your region/account This file routes intents to specialist Runtime ARNs (or local stubs). It does NOT call refund/cancel/inventory write tools directly — those stay behind Gateway Policy on the specialists. Demo only: fake routing; replace invoke_specialist with real Runtime invoke. """ from __future__ import annotations from harness import as_data, before_tool, bind_invocation, invocation_result, redact_text import os from typing import Any from bedrock_agentcore.runtime import BedrockAgentCoreApp from strands import Agent, tool from strands.models.bedrock import BedrockModel MODEL_ID = os.environ.get( "BEDROCK_MODEL_ID", "global.anthropic.claude-sonnet-5-5", ) # Map logical specialist → AgentCore Runtime ARN (set in deploy env) SPECIALIST_ARNS: dict[str, str] = { "sales": os.environ.get("SALES_AGENT_ARN", "arn:aws:bedrock-agentcore:us-east-1:000000000000:runtime/sales-demo"), "orders": os.environ.get("ORDERS_AGENT_ARN", "arn:aws:bedrock-agentcore:us-east-1:000000000000:runtime/orders-demo"), "support": os.environ.get("SUPPORT_AGENT_ARN", "arn:aws:bedrock-agentcore:us-east-1:000000000000:runtime/support-demo"), "inventory": os.environ.get("INVENTORY_AGENT_ARN", "arn:aws:bedrock-agentcore:us-east-1:000000000000:runtime/inventory-demo"), } MAX_SPECIALIST_HOPS = 2 app = BedrockAgentCoreApp() def invoke_specialist(name: str, user_message: str, session_ctx: dict[str, Any]) -> str: """Replace with bedrock-agentcore Runtime InvokeAgentRuntime (SigV4 or JWT). Dry-run returns a structured stub so local py_compile / unit tests work without AWS credentials. """ blocked = before_tool(f"route_to_{name}") if blocked: return blocked arn = SPECIALIST_ARNS[name] # The shopper message stays untrusted data. It is not a system prompt and not an approval. return as_data( specialist=name, runtime_arn=arn, untrusted_user_message=user_message, session=session_ctx, mode="dry-run", ) @tool def route_to_sales(user_message: str) -> str: """Route product discovery, cart, and availability questions to the sales agent.""" return invoke_specialist("sales", user_message, {"hop": 1}) @tool def route_to_orders(user_message: str) -> str: """Route order status, cancel, modify, and shipment tracking to the orders agent.""" return invoke_specialist("orders", user_message, {"hop": 1}) @tool def route_to_support(user_message: str) -> str: """Route returns, refunds, policy FAQs, and escalations to the support agent.""" return invoke_specialist("support", user_message, {"hop": 1}) @tool def route_to_inventory(user_message: str) -> str: """Route warehouse stock and merchandising ops to the inventory agent (associates).""" return invoke_specialist("inventory", user_message, {"hop": 1}) @tool def escalate_to_human(reason: str, user_message: str) -> str: """Hand off to a human associate when risk is high or specialists are ambiguous.""" blocked = before_tool("escalate_to_human") if blocked: return blocked return as_data( status="escalated", reason=reason, user_message=redact_text(user_message), queue="commerce-tier2", ) SYSTEM_PROMPT = f"""You are the eCommerce supervisor for a storefront assistant. Classify the shopper or associate request and call EXACTLY ONE route_* tool (or escalate_to_human). Do not invent order IDs or prices. Never call cancel/refund/inventory write APIs yourself — specialists + Gateway Policy own writes. If the request is ambiguous, ask one clarifying question instead of multi-routing. Max specialist hops per turn: {MAX_SPECIALIST_HOPS}. """ agent = Agent( model=BedrockModel(model_id=MODEL_ID), tools=[ route_to_sales, route_to_orders, route_to_support, route_to_inventory, escalate_to_human, ], system_prompt=SYSTEM_PROMPT, ) @app.entrypoint def invoke(payload: dict[str, Any]) -> dict[str, Any]: rejection = bind_invocation(payload) if rejection: return rejection prompt = payload.get("prompt") or payload.get("inputText") or "" result = agent(prompt) body = invocation_result(str(result)) body["max_specialist_hops"] = MAX_SPECIALIST_HOPS return body if __name__ == "__main__": # Local smoke: dry-run tool path without Runtime. Tenant is required. bind_invocation({"tenant_id": "demo-tenant"}) print(route_to_orders("Where is order ORD-1001?"))